前言
在嵌入式常用外设通信协议中,SPI 凭借全双工传输、速率高、协议简单的优势,广泛应用于 SPI Flash、OLED 屏幕、加速度传感器、ADC 芯片等场景。但很多初学者接触 SPI 时,总会被 CPOL、CPHA 的四种模式绕晕,也始终搞不懂为什么读操作一定要发 “哑元(Dummy)”。
本文结合完整时序波形图,从最底层的时钟极性、相位原理讲起,再到寄存器写操作、读操作的完整流程拆解,把 SPI 的核心逻辑彻底讲透。
一、SPI 基础:四根信号线的分工
SPI 是同步串行外设接口,采用典型的一主多从架构,标准 SPI 由 4 根核心信号线组成:
- SCLK:串行时钟,由主机产生,为数据传输提供同步节拍
- CS/SS:片选信号,低电平有效,主机拉低对应从机的 CS 引脚,才能与该从机建立通信
- MOSI(Master Out Slave In):主发从收通道,主机输出数据、从机接收数据
- MISO(Master In Slave Out):主收从发通道,从机输出数据、主机接收数据
SPI 是全双工通信:在 SCLK 的驱动下,MOSI 和 MISO 可以同时传输数据,发送和接收是同步进行的。这也是 SPI 读操作必须发送数据的根本原因。
二、核心难点:CPOL 与 CPHA 的本质
SPI 的四种工作模式,本质就是时钟极性(CPOL)和时钟相位(CPHA)的不同组合。这两个参数是 SPI 通信双方必须严格一致的配置,参数不匹配会导致数据完全错乱。
1. 时钟极性 CPOL
CPOL 决定了总线空闲状态下,SCLK 的电平:
- CPOL = 0:空闲时 SCLK 保持低电平
- CPOL = 1:空闲时 SCLK 保持高电平
简单理解:CPOL 就是时钟的 “初始状态”—— 通信未启动、CS 未拉低时,时钟线停驻的电平。
2. 时钟相位 CPHA
CPHA 决定了数据在第几个时钟边沿被采样:
- CPHA = 0:数据在 SCLK 产生后的第一个边沿被采样
- CPHA = 1:数据在 SCLK 产生后的第二个边沿被采样
这里的 “边沿” 是上升沿还是下降沿,由 CPOL 共同决定,这也是很多人死记硬背却容易混淆的地方。我们结合两种极性分别拆解:
情况 1:CPOL=0(空闲低电平)
时钟从低电平启动,第一个边沿是上升沿,第二个边沿是下降沿
- CPHA=0:上升沿采样数据,下降沿切换下一位数据
- CPHA=1:下降沿采样数据,上升沿切换下一位数据
情况 2:CPOL=1(空闲高电平)
时钟从高电平启动,第一个边沿是下降沿,第二个边沿是上升沿
- CPHA=0:下降沿采样数据,上升沿切换下一位数据
- CPHA=1:上升沿采样数据,下降沿切换下一位数据
3. 四种 SPI 模式总结
表格
| SPI 模式 | CPOL | CPHA | 空闲时钟电平 | 数据采样边沿 | 实际使用频率 |
|---|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿 | 最常用 |
| 模式 1 | 0 | 1 | 低电平 | 下降沿 | 较少用 |
| 模式 2 | 1 | 0 | 高电平 | 下降沿 | 较少用 |
| 模式 3 | 1 | 1 | 高电平 | 上升沿 | 较常用 |
开发小贴士:绝大多数外设默认支持模式 0 或模式 3,二者都是上升沿采样,区别仅在于空闲电平不同。
三、实战时序:SPI 寄存器读写全流程
搞懂时钟相位的逻辑,我们来看实际开发中最核心的场景:通过 SPI 读写外设内部寄存器。下面以最通用的模式 0(CPOL=0,CPHA=0)为例,完整拆解写操作与读操作的时序。
1. 写寄存器操作
写操作是主机主动向外设指定寄存器写入数据,全程有效数据由主机通过 MOSI 发出,MISO 上的数据无实际意义。
完整时序流程:
- 拉低 CS 片选:主机将对应从机的 CS 引脚拉低,宣告通信开始,从机开始监听 MOSI 上的数据。
- 发送寄存器地址:主机通过 MOSI 串行发送 8 位寄存器地址(通常最高位为读写标志位,0 表示写操作)。
- 发送写入数据:地址传输完成后,主机继续通过 MOSI 发送 8 位待写入的数据。
- 拉高 CS 结束通信:数据传输完成后,主机拉高 CS 引脚,本次写操作结束,从机停止响应。
全双工特性说明:写操作过程中,MISO 上也会出现电平变化,但这是从机输出的无效数据,主机不需要读取处理。
2. 读寄存器操作
读操作是主机读取外设指定寄存器的数据,这也是 SPI 最容易踩坑的地方:读操作必须发送哑元(Dummy)字节。
为什么必须有哑元?
因为 SPI 是同步全双工通信,没有时钟就没有数据传输。主机想要接收从机的数据,必须主动提供 SCLK 时钟;而提供时钟的同时,MOSI 上必然会同步输出数据。
完整读操作时序流程:
- 拉低 CS 片选:主机拉低 CS,启动本次通信。
- 发送寄存器地址:主机通过 MOSI 发送 8 位寄存器地址(最高位置 1,表示读操作)。
此时从机正在解析地址、准备数据,MISO 上的数据无效,主机忽略即可。
- 发送哑元,同步接收数据:地址发送完成后,主机发送 1 字节 Dummy 数据(任意值,通常为 0xFF),目的是提供 8 个 SCLK 时钟周期。
在这 8 个时钟周期里,从机将寄存器数据放到 MISO 线上,主机在采样边沿读取 MISO,得到真正的寄存器值。
- 拉高 CS 结束:数据接收完成,主机拉高 CS,通信结束。
一句话总结 SPI 读操作:发地址的时候收不到数据;发哑元的时候,才真正接收数据。这也是 SPI 和 I2C 读操作最核心的区别。
四、代码示例:硬件 SPI 读写寄存器
下面给出一份典型的 32 位 MCU 硬件 SPI 读写寄存器的代码框架,完全对应上面的时序逻辑。
#include "spi_hw.h" /* 片选控制宏,根据实际硬件修改 */ #define SPI_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define SPI_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) /* * @brief SPI底层发送一个字节(全双工,发的同时收一个字节) * @param data 待发送的字节 * @retval 接收到的字节 */ static uint8_t spi_send_byte(uint8_t data) { /* 等待发送缓冲区空 */ while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, data); /* 等待接收缓冲区非空 */ while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); } /* * @brief SPI写单个寄存器 * @param reg_addr 寄存器地址(7位有效,最高位为写标志0) * @param value 要写入的数据 */ void spi_write_reg(uint8_t reg_addr, uint8_t value) { SPI_CS_LOW(); // 拉低片选,启动通信 spi_send_byte(reg_addr & 0x7F); // 发送寄存器地址,最高位清0表示写 spi_send_byte(value); // 发送写入数据 SPI_CS_HIGH(); // 拉高片选,结束通信 } /* * @brief SPI读单个寄存器 * @param reg_addr 寄存器地址(7位有效,最高位为读标志1) * @retval 读取到的寄存器值 */ uint8_t spi_read_reg(uint8_t reg_addr) { uint8_t recv_val; SPI_CS_LOW(); // 拉低片选 spi_send_byte(reg_addr | 0x80); // 发送寄存器地址,最高位置1表示读 recv_val = spi_send_byte(0xFF); // 发送哑元字节,同步接收数据 SPI_CS_HIGH(); // 拉高片选 return recv_val; }核心说明:
spi_send_byte是全双工函数,发送一个字节的同时,会返回总线上接收到的字节。写操作时忽略返回值,读操作时利用哑元发送的返回值得到有效数据。
五、SPI 开发常见踩坑总结
- CPOL/CPHA 参数不匹配这是最常见的问题,表现为读出来的数据全是 0、全是 1,或者完全乱码。解决方法:严格对照外设数据手册,确认 SPI 工作模式,保证主机和从机参数完全一致。
- 读操作忘记发送哑元很多初学者读 SPI 寄存器,发完地址就直接读取结果,永远得不到正确数据。必须记住:SPI 读数据,地址之后一定要发送对应长度的哑元时钟。
- CS 时序错误
- 每次独立读写都必须完整执行 “拉低 CS→传输→拉高 CS”,不能多次读写共用一个低电平 CS(除非外设明确支持地址自增连续读写)
- CS 拉低后建议增加几微秒延时,保证从机准备就绪再开始传输
- 数据位宽与传输顺序错误
- 绝大多数 SPI 默认 8 位数据、MSB(高位)优先传输
- 16 位 / 32 位寄存器的外设,要特别注意高低字节的传输顺序
- 时钟速率过快初期调试建议将 SPI 时钟调低,确认通信正常后再逐步提高频率,避免硬件布线、电平兼容导致的高速传输错误。
结语
SPI 协议本身并不复杂,核心就是理解 “同步、全双工” 这两个本质。CPOL 和 CPHA 决定了数据采样的时机,哑元是全双工读操作的必然产物。 吃透底层时序原理,再去调试 SPI 驱动,就不会只会照搬例程,遇到问题也能快速定位根因。