1. SPI通信基础与模式解析
SPI(Serial Peripheral Interface)作为一种高速全双工同步串行通信协议,在嵌入式系统中扮演着重要角色。不同于I2C的两线制,SPI采用四线制通信:MOSI(主出从入)、MISO(主入从出)、SCLK(时钟线)和SS(片选线)。这种架构使得SPI在需要高速数据传输的场景(如存储器访问、显示屏驱动)中具有明显优势。
SPI的四种工作模式由时钟极性(CPOL)和时钟相位(CPHA)两个参数的组合决定:
- 模式0:CPOL=0(时钟空闲低电平),CPHA=0(数据在第一个时钟边沿采样)
- 模式1:CPOL=0,CPHA=1(数据在第二个时钟边沿采样)
- 模式2:CPOL=1(时钟空闲高电平),CPHA=0
- 模式3:CPOL=1,CPHA=1
实际项目中,模式0和模式3最为常见,例如Flash存储器多采用模式0,而某些传感器会使用模式3。选择错误的工作模式会导致数据采样错位,这是新手最容易踩的坑。
2. 软件模拟SPI的实现原理
在没有硬件SPI外设或需要灵活配置的场景下,软件模拟SPI成为实用解决方案。其核心是通过GPIO引脚的电平控制和时序管理来实现通信协议。以STM32为例,典型的实现包含以下组件:
- GPIO初始化:
c复制void SPI_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
// 配置SCK、MOSI、SS为推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
// MISO配置为上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
}
- 时序控制函数:
c复制// 模式0的字节发送函数
uint8_t SPI_SendByte(uint8_t byte) {
uint8_t i, receive = 0;
for(i=0; i<8; i++) {
SCK_LOW(); // CPOL=0
if(byte & 0x80) MOSI_HIGH();
else MOSI_LOW();
delay_ns(50); // 建立时间
SCK_HIGH(); // 上升沿采样(CPHA=0)
receive <<= 1;
if(MISO_READ()) receive |= 0x01;
byte <<= 1;
delay_ns(50); // 保持时间
}
return receive;
}
不同模式的关键差异体现在时钟初始状态和采样边沿:
- 模式0/1:初始SCK=0
- 模式2/3:初始SCK=1
- CPHA=0:在第一个边沿(上升或下降)采样
- CPHA=1:在第二个边沿采样
实测发现,软件SPI的极限速率受CPU主频和GPIO操作延迟限制。在STM32F103(72MHz)上,单字节传输时间约5μs(约200kbps),远低于硬件SPI的18Mbps。对于高速场景,必须使用硬件SPI或带FIFO的USART模拟方案。
3. 四种模式的时序实现与测试
3.1 模式0的完整实现
c复制void SPI_Mode0_Init(void) {
// 初始化时设置SCK=0, CPOL=0
SCK_LOW();
SS_HIGH(); // 默认不选中
}
uint8_t SPI_Mode0_Transfer(uint8_t data) {
uint8_t i, recv = 0;
SS_LOW(); // 启动传输
for(i=0; i<8; i++) {
// 设置MOSI
if(data & 0x80) MOSI_HIGH();
else MOSI_LOW();
data <<= 1;
// 产生上升沿(CPHA=0在此采样)
delay_ns(50);
SCK_HIGH();
// 读取MISO
recv <<= 1;
if(MISO_READ()) recv |= 1;
// 完成时钟周期
delay_ns(50);
SCK_LOW();
}
SS_HIGH();
return recv;
}
对应的时序图特征:
- 时钟空闲低电平
- 数据在SCK上升沿被采样
- MOSI在SCK上升沿前稳定
3.2 模式3的特殊处理
c复制void SPI_Mode3_Init(void) {
// 初始化时设置SCK=1, CPOL=1
SCK_HIGH();
SS_HIGH();
}
uint8_t SPI_Mode3_Transfer(uint8_t data) {
uint8_t i, recv = 0;
SS_LOW();
for(i=0; i<8; i++) {
// 先产生下降沿(CPHA=1在上升沿采样)
SCK_LOW();
// 设置MOSI
if(data & 0x80) MOSI_HIGH();
else MOSI_LOW();
data <<= 1;
delay_ns(50);
// 产生上升沿采样
SCK_HIGH();
recv <<= 1;
if(MISO_READ()) recv |= 1;
delay_ns(50);
}
SS_HIGH();
return recv;
}
模式3的时序特点:
- 时钟空闲高电平
- 数据在SCK上升沿采样
- MOSI在SCK下降沿后变化
调试技巧:用逻辑分析仪捕获时序时,注意设置正确的触发条件。例如模式3应设置为SCK下降沿触发,并检查采样点是否位于数据稳定窗口中央。曾遇到因毛刺导致采样错误的情况,通过增加10ns延时解决。
4. 测试方案与问题排查
4.1 交叉验证测试法
为确保软件SPI的正确性,建议采用三级验证:
- 自环测试:将MOSI与MISO短接,发送已知模式数据(如0xAA、0x55)验证收发一致性
- 设备验证:使用已知工作模式的器件(如Mode 0的W25Q32 Flash)进行实际读写
- 逻辑分析仪对比:捕获硬件SPI与软件SPI的波形进行逐位比对
4.2 典型问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 收到全0/全1 | 片选信号未生效 | 检查SS引脚电平及使能逻辑 |
| 数据错位 | CPHA设置错误 | 调整采样边沿位置 |
| 仅高位正确 | 字节移位方向错误 | 修改<<为>>或调整起始位 |
| 随机错误 | 时序不满足建立时间 | 增加操作之间的延时 |
| 从机无响应 | 模式不匹配 | 确认器件规格书的SPI模式要求 |
4.3 性能优化技巧
- 指令级优化:使用寄存器直接操作替代HAL库函数
c复制#define SCK_HIGH() (GPIOB->BSRR = GPIO_PIN_10)
#define SCK_LOW() (GPIOB->BRR = GPIO_PIN_10)
- 循环展开:减少for循环开销
c复制// 展开8次循环
if(data&0x80) MOSI_HIGH(); else MOSI_LOW(); data<<=1;
SCK_HIGH(); recv = (recv<<1) | MISO_READ(); SCK_LOW();
// 重复7次...
- 预取数据:提前准备下一个字节的发送数据
实测表明,通过上述优化可将传输速率提升3-5倍,在STM32F4上可达1Mbps以上。但要注意,过度优化可能降低代码可移植性,需根据项目需求权衡。
5. 扩展应用与进阶技巧
5.1 多从机管理方案
软件SPI的优势在于可灵活配置片选信号。实现多从机控制有两种方式:
- 独立片选法:每个从机使用单独的GPIO控制
c复制void SPI_SelectDevice(uint8_t dev_id) {
switch(dev_id) {
case 0: DEV0_SS_LOW(); break;
case 1: DEV1_SS_LOW(); break;
// ...
}
}
- 译码器方案:使用3-8译码器(如74HC138)通过3个GPIO控制8个设备
重要经验:切换设备时需确保SCK处于空闲电平,避免产生虚假时钟边沿。曾因快速切换导致ADXL345寄存器误写,现加入5μs延时解决。
5.2 异常处理机制
可靠的SPI通信应包含以下保护措施:
- 超时检测:
c复制uint8_t SPI_WaitReady(uint32_t timeout) {
while(timeout--) {
if(MISO_READ() == READY_STATE) return SUCCESS;
delay_us(1);
}
return TIMEOUT_ERROR;
}
- CRC校验:在传输数据包末尾添加CRC8校验码
- 重试机制:连续3次失败后触发硬件复位
5.3 混合模式通信
某些器件要求传输过程中切换模式(如初始化阶段用Mode 3,数据传输用Mode 0)。实现方案:
c复制void SPI_SwitchMode(uint8_t new_mode) {
// 确保SCK处于目标模式要求的空闲状态
switch(new_mode) {
case 0: case 1: SCK_LOW(); break;
case 2: case 3: SCK_HIGH(); break;
}
current_mode = new_mode;
}
uint8_t SPI_Transfer(uint8_t data) {
if(current_mode == 0 || current_mode == 2) {
// CPHA=0的处理逻辑
} else {
// CPHA=1的处理逻辑
}
}
在最近的一个物联网项目中,通过软件SPI成功驱动了三种不同模式的器件(温湿度传感器SHT30-Mode3、Flash存储器GD25Q64-Mode0、LCD屏ST7789-Mode0),关键是在每次传输前动态切换模式并确保时序严格符合各器件规格要求。