1. SPI协议基础认知
SPI(Serial Peripheral Interface)作为一种同步串行通信协议,在嵌入式系统中扮演着神经末梢的角色。我第一次接触SPI是在调试一块传感器模块时,当时用示波器捕捉到的那组规整的方波信号至今记忆犹新。这种四线制通信方案由摩托罗拉在1980年代提出,如今已成为连接微控制器与外围芯片的标准方式之一。
SPI最显著的特点是采用主从架构,通常由一个主设备(Master)和一个或多个从设备(Slave)构成。四根信号线中,SCK(Serial Clock)由主设备产生的时钟信号如同交响乐指挥的节拍器,MOSI(Master Out Slave In)和MISO(Master In Slave Out)则是双向数据通道,而SS(Slave Select)线则像点名册一样负责选择特定的从设备。这种全双工通信模式使得数据收发可以同步进行,效率明显高于半双工的I2C协议。
与UART、I2C等其他串行协议相比,SPI的最大优势在于其简洁性。它不需要复杂的地址分配机制,也没有繁琐的应答信号,通信速率通常可达几十MHz。记得在智能家居项目中,我通过SPI接口以15MHz的速率读取温湿度传感器数据时,其响应速度让采用I2C的同类型传感器望尘莫及。但这也带来明显的局限性——每个从设备都需要独立的片选线,当外设数量增多时,GPIO资源消耗会呈线性增长。
2. SPI硬件连接与信号解析
2.1 物理层连接规范
标准的SPI接口采用菊花链或星型拓扑结构。在最近开发的电机控制板项目中,我采用了如图所示的星型连接方式:主控MCU作为中心节点,分别连接Flash存储器、显示屏和ADC芯片。这里有个容易忽视的细节——所有从设备的SCK、MOSI、MISO都需要并联,而每个从设备的SS线必须独立控制。
重要提示:SPI信号线长度超过15cm时,需要考虑信号完整性问题。我曾遇到过因20cm排线导致的时序错乱,最终通过降低时钟频率至1MHz以下解决。
信号线上拉电阻的配置往往令初学者困惑。根据经验,MOSI/MISO线通常需要4.7kΩ上拉,而SS线则必须根据从设备特性决定。某次使用TI的ADC芯片时,就因未查阅手册直接添加上拉电阻,导致芯片始终处于复位状态。
2.2 时钟极性与时相配置
SPI最复杂的部分莫过于时钟配置,这涉及到CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数。通过组合它们可以得到四种工作模式:
| 模式 | CPOL | CPHA | 数据采样时刻 |
|---|---|---|---|
| 0 | 0 | 0 | SCK上升沿 |
| 1 | 0 | 1 | SCK下降沿 |
| 2 | 1 | 0 | SCK下降沿 |
| 3 | 1 | 1 | SCK上升沿 |
在调试Nordic的nRF24L01无线模块时,我花了三小时才意识到必须使用模式0。后来养成了习惯——拿到新器件首先用逻辑分析仪抓取信号,观察厂商Demo板的通信波形。这里分享一个技巧:当不确定模式时,可以尝试在初始化代码中循环测试四种组合:
c复制for(int mode=0; mode<4; mode++){
spi_set_mode(SPI_MODE_0 + mode);
if(check_device_response()) break;
}
3. SPI软件实现深度解析
3.1 寄存器级编程实践
以STM32F4系列为例,其SPI外设的CR1寄存器控制着核心参数。设置8位数据格式时,需配置DS[3:0]=0b0111,而BR[2:0]分频系数决定了SCK频率。这里有个坑:当APB2时钟为84MHz时,设置BR=0b000得到的不是42MHz而是21MHz,因为硬件会自动进行2分频。
发送数据时要注意TXE(发送缓冲区空)和RXNE(接收缓冲区非空)标志位的处理。我常用的中断服务例程模板如下:
c复制void SPI1_IRQHandler(void){
if(SPI1->SR & SPI_SR_TXE){
SPI1->DR = tx_buffer[tx_index++];
}
if(SPI1->SR & SPI_SR_RXNE){
rx_buffer[rx_index++] = SPI1->DR;
}
}
3.2 DMA传输优化技巧
对于高速数据采集(如ADC连续采样),必须启用DMA。在STM32CubeIDE中配置时,要注意设置DMA为循环模式,并正确配置数据宽度。某次项目中出现数据错位,最终发现是DMA源地址和目的地址的数据宽度设置不一致导致。
SPI DMA传输有个特殊现象:由于SPI是同步接口,即使只发送数据也会产生接收数据。这意味着接收DMA的计数器应该与发送保持一致。我曾通过以下方式解决内存溢出问题:
c复制hdma_spi_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi_rx.Init.Mode = DMA_CIRCULAR;
hdma_spi_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi_rx.Init.MemInc = DMA_MINC_ENABLE;
4. 典型应用场景与故障排查
4.1 Flash存储器操作实战
W25Q128FV这款128M-bit Flash芯片是SPI设备的典型代表。其页编程操作需要遵循特定时序:先发送0x02指令,接着是24位地址,然后是数据。关键点在于:跨页写入时不会自动回卷,而是覆盖当前页内容。这导致我早期项目中出现过数据丢失,后来通过以下封装函数解决:
c复制void flash_write_page(uint32_t addr, uint8_t *data, uint16_t len){
uint16_t page_remain = 256 - (addr % 256);
len = MIN(len, page_remain);
cs_low();
spi_transfer(0x06); // WREN
cs_high();
cs_low();
spi_transfer(0x02); // PAGE PROGRAM
spi_transfer(addr >> 16);
spi_transfer(addr >> 8);
spi_transfer(addr);
for(int i=0; i<len; i++){
spi_transfer(data[i]);
}
cs_high();
while(flash_is_busy());
}
4.2 常见故障诊断方法
当SPI通信失败时,系统化的排查流程至关重要。我的诊断工具箱通常包含:
- 逻辑分析仪:检查SCK频率是否符合预期,数据线是否正常翻转
- 万用表:测量各引脚电压,确认SS线电平状态
- 示波器:观察信号质量,检查过冲和振铃现象
上周刚解决一个典型案例:某工业HMI设备的触摸屏突然失灵。通过示波器发现MOSI信号在上升沿出现明显振荡,最终在信号线上串联33Ω电阻得以解决。这提醒我们:高速SPI信号(>10MHz)必须考虑传输线效应。
