1. SPI协议基础:从一根线到四根线的故事

大家好,我是你们的讲师。今天咱们聊聊SPI——串行外设接口。说实话,这玩意儿在嵌入式系统里太常见了。我最早接触SPI是在做一款工业传感器采集板的时候,当时被时序问题折腾得够呛。后来才明白,很多坑其实都源于对基础理解不透。

SPI全称是Serial Peripheral Interface,由摩托罗拉在80年代推出。你想想看,一个80年代的技术到现在还在广泛使用,说明它确实有两把刷子。它的核心思想很简单:用四根线实现全双工通信。

1.1 SPI总线概述

SPI是一种同步、全双工、主从式的串行通信协议。同步,意味着有专门的时钟线;全双工,意味着发送和接收可以同时进行;主从式,意味着总线上只有一个设备负责控制通信节奏。

我个人的习惯是,把SPI想象成一个环形移位寄存器。主设备往线上推一个bit,从设备同时推回来一个bit。就这么简单,但细节里藏着魔鬼。

核心特点速览:

  • 同步通信:由主设备产生时钟信号
  • 全双工:数据发送和接收同时进行
  • 主从架构:一个主设备控制多个从设备
  • 高速率:典型速率从几MHz到几十MHz
  • 硬件简单:不需要复杂的协议栈

SPI的传输速率通常比I2C快得多。我在项目中用过50MHz的SPI时钟,跑图像传感器数据流,完全没问题。但要注意,速率越高,对PCB走线和信号完整性的要求也越高。

1.2 四线制信号定义

SPI的四根线,每根都有自己的脾气。咱们一个一个说。

信号线 全称 方向 作用
SCK Serial Clock 主→从 时钟信号,由主设备产生
MOSI Master Out Slave In 主→从 主设备发送数据给从设备
MISO Master In Slave Out 从→主 从设备发送数据给主设备
CS Chip Select 主→从 片选信号,低电平有效

SCK:时钟线,由主设备控制。它的频率决定了通信速率。我曾经遇到过一个问题,从设备手册上写支持20MHz,结果实际跑到15MHz就丢数据了。后来发现是PCB走线太长,信号反射造成的。嗯,这里要注意:时钟频率不是越高越好,得看实际链路质量。

MOSI:主设备输出,从设备输入。数据从主设备流向从设备。这条线上的数据,是在时钟的上升沿或下降沿被采样的,具体取决于SPI模式。

MISO:主设备输入,从设备输出。数据从从设备流回主设备。注意,当CS为高电平时,从设备的MISO引脚应该处于高阻态,否则多个从设备会打架。

CS:片选线,低电平有效。主设备通过拉低某个从设备的CS线来选择与之通信。我见过不少新手犯的错误:CS拉低后没有留足够的建立时间就开始发时钟,导致第一个bit采错了。

个人经验:调试SPI时,先用示波器看CS和SCK的时序关系。CS拉低后,至少要等半个时钟周期再开始发数据。很多从设备对建立时间有要求,手册里通常会写t_cs_setup这个参数。

1.3 全双工通信原理

全双工,说白了就是同时收发。SPI怎么做到的?因为它有独立的发送线和接收线。

你想想看,主设备往MOSI上推一个bit的同时,从设备往MISO上推一个bit。一个时钟周期内,两个方向各传输一个bit。这就是全双工的本质。

但有个细节很多人会忽略:SPI的收发是绑定的。也就是说,主设备想从从设备读数据,必须先往MOSI上发数据。发什么?通常是0x00或者0xFF,具体看从设备的协议。

// 典型的SPI读写函数示例
uint8_t spi_transfer(uint8_t tx_data) {
    // 发送一个字节,同时接收一个字节
    SPI_DR = tx_data;          // 写入发送寄存器
    while(!(SPI_SR & SPI_FLAG_TXE));  // 等待发送完成
    while(!(SPI_SR & SPI_FLAG_RXNE)); // 等待接收完成
    return SPI_DR;             // 读取接收寄存器
}

这段代码看起来简单,但我在项目中踩过坑。有些MCU的SPI外设,发送寄存器和接收寄存器是同一个地址。你写数据的时候,硬件自动把接收到的数据放到同一个寄存器里。读的时候要小心,别把发送的数据又读回来了。

避坑指南:我曾经在一个项目中,用STM32的SPI读传感器数据。代码逻辑没问题,但读出来的数据总是错位。后来发现是SPI的帧格式设置错了——传感器要求MSB first,我设成了LSB first。这种错误示波器上看波形很难发现,因为波形看起来是对的,只是bit顺序反了。

1.4 主从模式配置

SPI总线上只有一个主设备,可以有多个从设备。主设备负责产生时钟、控制片选、发起通信。从设备被动响应。

多从设备怎么接?有两种方式:

  • 独立片选:每个从设备有自己的CS线,主设备用多个GPIO分别控制
  • 菊花链:从设备串联,数据从第一个传到最后一个,再从最后一个传回来

独立片选最常见。菊花链用得少,但有些ADC和移位寄存器会用这种方案。

配置主从模式时,有几个关键参数必须设置对:

参数 说明 常见取值
时钟极性(CPOL) 空闲时SCK的电平 0=低电平空闲,1=高电平空闲
时钟相位(CPHA) 数据在哪个边沿采样 0=第一个边沿,1=第二个边沿
数据顺序 MSB还是LSB先发送 MSB first 或 LSB first
数据宽度 每次传输的bit数 8bit最常见,也有16bit

CPOL和CPHA组合起来就是SPI的四种模式。我建议你拿到从设备手册后,第一件事就是查它支持哪种模式。主设备和从设备的模式必须一致,否则通信会失败。

SPI四种模式速查:

  • 模式0:CPOL=0, CPHA=0 — 空闲低电平,第一个边沿采样
  • 模式1:CPOL=0, CPHA=1 — 空闲低电平,第二个边沿采样
  • 模式2:CPOL=1, CPHA=0 — 空闲高电平,第一个边沿采样
  • 模式3:CPOL=1, CPHA=1 — 空闲高电平,第二个边沿采样

大多数传感器和Flash芯片默认用模式0或模式3。

配置代码通常长这样:

// SPI主模式初始化示例(伪代码)
void spi_master_init(void) {
    // 1. 配置GPIO:SCK, MOSI, CS为输出,MISO为输入
    gpio_config(SCK_PIN, GPIO_MODE_AF_PP);
    gpio_config(MOSI_PIN, GPIO_MODE_AF_PP);
    gpio_config(MISO_PIN, GPIO_MODE_INPUT);
    gpio_config(CS_PIN, GPIO_MODE_OUTPUT);
    
    // 2. 配置SPI外设
    SPI_CR1 = 0;
    SPI_CR1 |= SPI_CR1_MSTR;      // 主模式
    SPI_CR1 |= SPI_CR1_BR_2;      // 时钟分频,这里设为8分频
    SPI_CR1 |= SPI_CR1_CPOL_0;    // CPOL=0
    SPI_CR1 |= SPI_CR1_CPHA_0;    // CPHA=0,模式0
    SPI_CR1 |= SPI_CR1_LSBFIRST;  // LSB first(根据需求选择)
    SPI_CR1 |= SPI_CR1_SPE;       // 使能SPI
    
    // 3. CS默认拉高
    gpio_write(CS_PIN, 1);
}

这段代码看着简单,但实际项目中我建议你加上错误处理。比如超时检测——如果SPI一直没收到数据,不能死等。

下面这张图展示了SPI单主多从的典型连接方式:

SPI单主多从连接结构图 主设备 (Master) MCU / 处理器 SCK MOSI MISO CS1 CS2 从设备1 (Slave 1) 传感器/Flash 从设备2 (Slave 2) ADC/显示屏 SCK(共享) MOSI(共享) MISO(共享) CS1(独立) CS2(独立) 说明 • SCK/MOSI/MISO 三线共享 • CS各自独立

从图上可以看得很清楚:SCK、MOSI、MISO三根线是共享的,所有从设备都挂在这三根线上。CS线则是每个从设备独享一根。主设备通过拉低某个从设备的CS来选择通信对象。

这里有个容易忽略的点:当CS为高电平时,从设备的MISO引脚必须处于高阻态。如果从设备不支持高阻态,那你就得在MISO线上加三态缓冲器,否则多个从设备会同时驱动MISO,造成信号冲突。

调试小技巧:如果你发现SPI通信时好时坏,先检查CS的时序。用示波器看CS拉低后到第一个SCK边沿的时间。很多从设备要求这个时间至少是半个SCK周期。另外,CS拉高后也要留一点时间,让从设备释放MISO总线。