SPI 协议基础
SPI – 串行外设接口,说白了一根时钟线、两根数据线、再加一根片选线,就能让两个芯片之间全双工地聊起来。我这些年做嵌入式,从传感器读到 Flash 写,从 ADC 采集到 OLED 刷屏,最常用的就是 SPI。别看它简单,一旦时序没弄对,数据错位、通信失败那是家常便饭。
SPI 总线架构
SPI 是典型的主从架构,只有一个主设备(Master)负责产生时钟,一个或多个从设备(Slave)被动响应。主设备通过片选信号来选择要跟哪个从设备通信。你想想看,多从机场景里,所有从机的 SCK、MOSI、MISO 其实是并联在一起的,全靠各自的 SS 引脚来区分。
| 角色 | 时钟控制 | 数据驱动方向 | 片选控制 |
|---|---|---|---|
| 主设备 | 产生 SCK | MOSI 输出,MISO 输入 | 控制所有 SS 引脚 |
| 从设备 | 接收 SCK | MOSI 输入,MISO 输出 | 等待主设备拉低自己的 SS |
这里有个容易踩的坑 – 我刚开始做设计时以为从机的 MISO 可以一直输出,结果两个从机同时把数据推到同一条线上,烧了好几个引脚。其实从机只有在自己的 SS 被选中时才能驱动 MISO,其他时候必须输出高阻态。
信号线定义
SPI 一共四根信号线,名字你可能早就背熟了,但每条线的特性还是有些细节值得聊一聊。
- SCK(Serial Clock) – 时钟线,由主设备产生。所有数据都在 SCK 的边沿采样或改变。极性和相位可以通过 CPOL 和 CPHA 来配置,一共四种模式。
- MOSI(Master Out Slave In) – 主设备输出,从设备输入。数据从主设备发往从设备。
- MISO(Master In Slave Out) – 主设备输入,从设备输出。数据从从设备发往主设备。
- SS(Slave Select) – 片选信号,低电平有效。主设备拉低某个从机的 SS,表示选中它进行通信。
我曾经在某次产品测试时发现,SPI 通信偶尔会读出全零数据,折腾了一整天才找到原因 – 从机那边的 SS 引脚上拉电阻没焊,悬空状态下噪声抖动,导致从机频繁进出选通状态。从那以后我设计板子时,SS 引脚必定加一个 10kΩ 上拉到 VCC。
全双工通信原理
全双工?说白了就是主机发一个 bit 的同时,从机也发一个 bit 回来。这个机制依赖一个“移位寄存器”结构。主从双方各有一个 8 位(或 16 位、32 位)的移位寄存器,SCK 每来一个边沿,双方的寄存器同时左移一位,主机把最高位推到 MOSI 上,同时从机把最高位推到 MISO 上。8 个时钟之后,两边的寄存器内容就交换完成了。
我刚开始学的时候,以为全双工意味着一帧数据可以同时发和收,其实你每次发出去的字节和收到的字节在时间上是对齐的,而且收和发的数据是同步移位完成的。下面这个伪代码可以帮你理解:
// SPI 发送一个字节并接收一个字节
uint8_t spi_transfer(uint8_t tx_data) {
uint8_t rx_data = 0;
for (int i = 7; i >= 0; i--) {
// 主机将 tx_data 的第 i 位输出到 MOSI
MOSI = (tx_data >> i) & 0x01;
// 从机将数据输出到 MISO(由从机硬件自动完成)
// 等待一个 SCK 周期(上升沿或下降沿)
SCK = 1;
// 读取 MISO 上的 bit,存入 rx_data 对应位
rx_data |= (MISO << i);
SCK = 0;
}
return rx_data;
}
举个例子,你要从 SPI Flash 里读一个 ID 字节。通常流程是先发一个读命令(0x9F),Flash 会在接下来的 8 个时钟里把 ID 数据从 MISO 返回给你。注意,这里你发命令的时候,从机也在往 MISO 上推数据,只不过那些数据是前一次传输的残留或无效数据,别被它们误导就好。