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单主多从的典型连接方式:
从图上可以看得很清楚:SCK、MOSI、MISO三根线是共享的,所有从设备都挂在这三根线上。CS线则是每个从设备独享一根。主设备通过拉低某个从设备的CS来选择通信对象。
这里有个容易忽略的点:当CS为高电平时,从设备的MISO引脚必须处于高阻态。如果从设备不支持高阻态,那你就得在MISO线上加三态缓冲器,否则多个从设备会同时驱动MISO,造成信号冲突。
调试小技巧:如果你发现SPI通信时好时坏,先检查CS的时序。用示波器看CS拉低后到第一个SCK边沿的时间。很多从设备要求这个时间至少是半个SCK周期。另外,CS拉高后也要留一点时间,让从设备释放MISO总线。