SPI(Serial Peripheral Interface)作为嵌入式领域最常用的同步串行通信协议之一,其四线制的硬件设计在各类传感器、存储芯片和显示模块中广泛应用。我第一次接触SPI是在调试一块OLED屏幕时,当时被CLK线那规律的方波信号所震撼——原来硬件通信可以如此优雅。
SPI采用主从架构,通常由一个主设备(MCU)和一个或多个从设备(外设)组成。四根信号线各司其职:
实际布线时建议给每条信号线串联22Ω电阻,能有效抑制信号反射。我曾因省略这个电阻导致ILI9341显示屏出现雪花噪点,排查了整整两天。
SPI最让人困惑的莫过于时钟极性与相位的四种组合模式。通过示波器抓取不同模式下的波形后,我发现可以这样理解:
模式0(CPOL=0, CPHA=0)是最常见的配置,适用于大多数EEPROM和传感器。而模式3(CPOL=1, CPHA=1)则在某些RFID芯片中使用较多。调试ADXL345加速度计时,就因模式设置错误导致读取的数据全是0xFF。
当系统需要连接多个SPI设备时,有三种典型方案:
独立片选型(推荐方案):
c复制// STM32配置示例
void SPI_SelectDevice(GPIO_TypeDef* Port, uint16_t Pin) {
HAL_GPIO_WritePin(Port, Pin, GPIO_PIN_RESET);
HAL_Delay(1); // 保持至少1us的建立时间
}
每个从设备独占一个GPIO作为片选,布线清晰但占用IO口多。我在智能家居项目中用此法同时管理VS1053音频解码器和W25Q128闪存。
菊花链型:
数据像流水线一样从一个设备传到下一个,只需要一个片选信号。但需要所有设备支持该模式,且通信速率受限于最慢的设备。TLC5940 LED驱动芯片就采用这种设计。
复用型:
通过74HC138等译码器扩展片选信号,节省MCU引脚但增加电路复杂度。工业控制板常用此方案连接多个MAX6675热电偶模块。
以STM32F103为例,要充分发挥SPI性能需要深入理解CR1寄存器的每个bit:
c复制// SPI1初始化代码片段
SPI1->CR1 = SPI_CR1_SPE // 使能SPI
| SPI_CR1_BR_0 | SPI_CR1_BR_1 // 波特率预分频256
| SPI_CR1_MSTR // 主模式
| SPI_CR1_CPOL // 时钟极性高
| SPI_CR1_CPHA // 相位第二边沿采样
| SPI_CR1_SSM | SPI_CR1_SSI; // 软件控制NSS
关键参数计算公式:
code复制实际波特率 = APB2时钟 / (2^(BR[2:0]+1))
例如APB2=72MHz, BR=0b101时:
72MHz / (2^(5+1)) = 1.125MHz
调试NRF24L01时发现其要求SPI时钟不能超过10MHz,超出后通信立即失败。建议初始设置为1MHz,稳定后再逐步提升。
查询方式会阻塞CPU,实际项目推荐使用DMA传输:
c复制// STM32 HAL库DMA配置
hdma_spi1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_spi1);
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1);
HAL_SPI_Transmit_DMA(&hspi1, txData, size);
中断方式适合小数据量传输,需注意:
这款128Mbit的SPI Flash常用作固件存储器,其指令集很有代表性:
| 指令名称 | 操作码 | 说明 |
|---|---|---|
| READ | 0x03 | 普通读取 |
| FAST_READ | 0x0B | 快速读取(需加dummy字节) |
| PP | 0x02 | 页编程(256字节限制) |
| SE | 0xD8 | 扇区擦除(4KB) |
关键操作时序:
c复制// 读取芯片ID示例
uint8_t cmd[4] = {0x9F, 0xFF, 0xFF, 0xFF}; // JEDEC ID指令
HAL_SPI_TransmitReceive(&hspi1, cmd, idBuf, 4, 100);
// idBuf[1]为制造商ID(Winbond=0xEF)
// idBuf[2]为存储器类型(W25Q128=0x4018)
这款温湿度气压三合一传感器采用SPI模式3,其寄存器访问有特殊要求:
c复制uint8_t regAddr = 0xD0 | 0x80; // 读取ID寄存器
HAL_SPI_TransmitReceive(&hspi1, ®Addr, &id, 1, 100);
c复制uint8_t ctrlHum = 0x72; // 湿度控制寄存器地址
uint8_t data[2] = {ctrlHum, 0x01}; // 启用湿度测量
HAL_SPI_Transmit(&hspi1, data, 2, 100);
校准参数存储在0x88~0xA1和0xE1~0xF0,需要先读取并存储。我在智能气象站项目中发现,若不进行温度补偿,测量误差可达±3℃。
当SPI通信异常时,建议按以下步骤排查:
曾经遇到一个诡异问题:SPI通信随机出错。最终发现是电源纹波过大,在AMS1117稳压器输出端增加470μF电解电容后解决。
以W25Q128在Fast Read模式为例:
code复制APB2=84MHz, prescaler=2 → 42MHz
周期=23.8ns,高/低电平=11.9ns > 5ns
实际测试发现超过30MHz后读取错误率上升,最终稳定工作在20MHz。对于需要连接多个高速设备的系统,建议:
在医疗设备开发中,我们使用STM32H7的SPI1(100MHz)连接ADAS1128 24bit ADC,SPI2连接安全存储芯片ATECC608A,两者互不干扰。