2. SPI 时序分析:采样、建立保持与时钟配置
SPI 通信,说白了就是主从设备之间的一场「握手舞」。
谁先动、什么时候采样、什么时候换数据,全由时钟和相位说了算。我刚开始调 SPI 的时候,总觉得时序图看着像天书,直到有一次在项目里把 ADC 采集的数据全读错了——嗯,从那以后我才老老实实把时序吃透。
2.1 SPI 数据采样与建立保持时间
先看一个最基础的问题:数据什么时候被采样?
SPI 是同步串行接口,数据在时钟边沿跳变时被采样。但这里有个关键——建立时间(tSU)和保持时间(tH)。
- 建立时间:数据必须在时钟采样边沿之前稳定下来的最短时间。
- 保持时间:数据在时钟采样边沿之后必须保持稳定的最短时间。
你想想看,如果数据在时钟边沿到来时还在跳变,那采到的值就是错的。我在一个 12MHz 的 SPI 项目里就吃过这个亏——从机响应慢,主机时钟太快,建立时间不够,读回来的 ADC 值全是乱码。
核心原则:主机时钟频率必须满足从机的最小建立时间 + 保持时间要求。
公式:Tclk ≥ tSU + tH + 数据线传输延迟
| 参数 | 典型值(3.3V 逻辑) | 说明 |
|---|---|---|
| tSU | 5 ~ 20 ns | 从机数据建立时间 |
| tH | 2 ~ 10 ns | 从机数据保持时间 |
| tdelay | 1 ~ 5 ns | PCB 走线延迟(每 10cm 约 0.5ns) |
我曾经踩过的坑:用逻辑分析仪看波形,数据线在时钟上升沿附近有毛刺。后来发现是 PCB 走线太长,信号反射导致建立时间不足。解决办法:降低时钟频率,或者在从机端加一个 10pF 的小电容滤波。
2.2 单字节与多字节传输时序
SPI 传输的基本单位是字节(8 bit)。但实际项目中很少只传一个字节——比如读一个 16 位 ADC 结果,或者写一串配置寄存器。
单字节传输
主机拉低片选(CS),发出 8 个时钟脉冲,同时 MOSI 送数据,MISO 收数据。8 个脉冲结束后,主机拉高片选。
就这么简单。但要注意:片选信号必须在整个传输过程中保持低电平。我见过有人把片选当使能信号用,每个 bit 都拉高拉低——那就不叫 SPI 了。
多字节传输
多字节传输就是连续发多个 8 位数据,片选一直保持低电平,直到所有字节传完。
// 伪代码:多字节 SPI 写操作
void SPI_WriteMulti(uint8_t *data, uint8_t len) {
CS_LOW(); // 拉低片选
for (uint8_t i = 0; i < len; i++) {
SPI_ExchangeByte(data[i]); // 发送一个字节
}
CS_HIGH(); // 拉高片选
}
多字节传输的关键点:
- 片选不能中间拉高,否则从机认为传输结束。
- 时钟必须连续,不能有超过半个时钟周期的停顿。
- 有些从机支持「自动地址递增」——连续读多个寄存器时,地址自动加 1。
个人习惯:我写多字节传输时,总会在循环前后加 CS 操作,而不是在循环里反复拉高拉低。这样既干净又高效。
2.3 SPI 时钟极性与相位配置实战
这是 SPI 里最容易搞混的地方。时钟极性(CPOL)和时钟相位(CPHA)组合出 4 种模式。
- CPOL:空闲时时钟电平。0 = 低电平,1 = 高电平。
- CPHA:数据采样边沿。0 = 第一个边沿采样,1 = 第二个边沿采样。
| 模式 | CPOL | CPHA | 采样边沿 | 数据变化边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 上升沿 | 下降沿 |
| 模式 1 | 0 | 1 | 下降沿 | 上升沿 |
| 模式 2 | 1 | 0 | 下降沿 | 上升沿 |
| 模式 3 | 1 | 1 | 上升沿 | 下降沿 |
我建议你记住一个口诀:「CPHA=0 时,第一个边沿采样;CPHA=1 时,第二个边沿采样」。至于 CPOL,它只决定空闲电平,不影响采样顺序。
实战经验:大多数 ADC 和传感器默认用模式 0(CPOL=0, CPHA=0)。但总有例外——比如某款温度传感器必须用模式 3。配置前一定先看数据手册的时序图,别猜。
配置示例:STM32 HAL 库
// SPI 句柄配置
SPI_HandleTypeDef hspi1;
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
你看,CPOL 和 CPHA 各占一个配置项。改模式时只需要改这两行,其他不用动。
我曾经犯过的错:在同一个 SPI 总线上挂了两个从机,一个用模式 0,一个用模式 3。结果每次切换从机都要重新初始化 SPI——后来老老实实加了一根额外的 GPIO 做片选,每个从机独立配置。
2.4 时序知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作 SPI 时序调试的「检查清单」。
2.5 调试建议
最后给你几条实在的建议:
- 先用逻辑分析仪看波形。别信代码,信眼睛。看 CS 是否干净、时钟是否有毛刺、数据是否在正确边沿稳定。
- 从低速开始调。我习惯先设 1MHz 以下,确认通信正常后再往上提频率。
- 读回验证。写一个已知值,再读回来对比。如果对不上,八成是时序配置错了。
- 片选别偷懒。软件控制 CS 时,记得在传输前后加一点延时(至少 1μs),给从机反应时间。
一个小技巧:如果你不确定从机用哪种模式,可以写个循环依次试模式 0~3,看哪个能读到正确的设备 ID 或状态寄存器。这招我在没有数据手册的「古董芯片」上用过好几次。