1. SPI通信协议基础解析
SPI(Serial Peripheral Interface)作为一种高速全双工同步串行通信协议,在嵌入式领域占据着重要地位。我第一次接触SPI是在2013年调试一个传感器模块时,当时被其简洁的四线制设计和高效的传输速率所吸引。相比I2C协议,SPI最大的特点是没有复杂的地址机制和应答信号,通过硬件片选线实现设备寻址,这使得它在点对点通信场景中表现出色。
SPI总线由四根基本信号线构成:
- SCK(Serial Clock):时钟信号线,由主机产生
- MOSI(Master Out Slave In):主机输出从机输入数据线
- MISO(Master In Slave Out):主机输入从机输出数据线
- NSS(Slave Select):从机选择信号线(低电平有效)
在实际项目中,我经常遇到工程师对SPI工作模式理解不透彻导致通信失败的情况。SPI有四种工作模式,由CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数决定:
- 模式0:CPOL=0,CPHA=0(时钟空闲为低电平,数据在第一个边沿采样)
- 模式1:CPOL=0,CPHA=1
- 模式2:CPOL=1,CPHA=0
- 模式3:CPOL=1,CPHA=1
重要提示:主从设备的工作模式必须完全一致!我在调试某款Flash存储器时,就曾因模式设置不匹配导致读取的数据全是0xFF。
2. STM32 SPI外设架构详解
STM32全系列都集成了至少一个SPI外设,以STM32F4系列为例,其SPI外设具有以下核心特性:
- 支持主/从模式切换
- 8位或16位数据帧格式
- 最高可达37.5MHz的通信速率(在72MHz系统时钟下)
- 硬件CRC校验功能
- 支持DMA传输
2.1 时钟树与波特率配置
SPI的时钟源通常来自APB总线,具体分频系数通过SPI_CR1寄存器的BR[2:0]位设置。计算实际波特率的公式为:
code复制f_PCLK / (2^(BR[2:0]+1))
例如APB1时钟为42MHz,BR设置为001(二分频),则实际波特率为21MHz。
我在项目中发现一个常见误区:很多人以为BR值就是直接分频系数。实际上BR=000表示2分频,BR=111表示256分频,这个非线性关系需要特别注意。
2.2 数据流与控制逻辑
STM32的SPI外设采用双缓冲结构,包含:
- 发送缓冲区(Tx Buffer)
- 接收缓冲区(Rx Buffer)
- 移位寄存器(Shift Register)
数据传输过程是这样的:
- 数据写入SPI_DR寄存器(实际进入Tx Buffer)
- 当移位寄存器空闲时,Tx Buffer的数据自动加载到移位寄存器
- 移位寄存器在SCK时钟驱动下逐位输出
- 同时接收到的数据存入移位寄存器
- 接收完成后,数据转入Rx Buffer,并可通过SPI_DR读取
调试技巧:通过监测SPI_SR寄存器的TXE(发送缓冲区空)和RXNE(接收缓冲区非空)标志位,可以精确控制数据传输节奏。
3. 关键寄存器深度剖析
3.1 控制寄存器1(SPI_CR1)
这个寄存器是SPI配置的核心,主要控制位包括:
- SPE:SPI使能位(必须置1才能工作)
- MSTR:主/从模式选择
- BR[2:0]:波特率控制
- CPOL/CPHA:时钟模式设置
- DFF:数据帧格式(0=8位,1=16位)
- LSBFIRST:数据位传输顺序(0=MSB first)
配置示例:
c复制SPI1->CR1 = SPI_CR1_SPE | SPI_CR1_MSTR | SPI_CR1_BR_0 | SPI_CR1_CPOL;
3.2 状态寄存器(SPI_SR)
这个寄存器反映了SPI的工作状态,关键标志位有:
- RXNE:接收缓冲区非空
- TXE:发送缓冲区空
- BSY:SPI忙状态
- OVR:溢出错误
我在调试中发现一个典型问题:很多开发者会忽略BSY标志,导致过早关闭SPI或切换模式。正确的做法是在修改重要配置前,必须等待BSY位清零。
3.3 数据寄存器(SPI_DR)
这个寄存器用于读写数据,但有个重要特性:它是"双重功能"寄存器。写入时操作的是发送缓冲区,读取时获取的是接收缓冲区数据。
4. 典型配置流程与实战技巧
4.1 初始化步骤
- 使能SPI时钟(RCC->APB2ENR)
- 配置GPIO为复用功能(SCK/MOSI/NSS)和输入(MISO)
- 设置SPI_CR1寄存器
- 使能SPI(设置SPE位)
经验之谈:NSS引脚可以配置为硬件模式(SPI_CR1的SSM=0)或软件模式(SSM=1)。在多点通信时,我推荐使用软件控制,因为硬件NSS有时会产生意外的片选信号。
4.2 数据传输优化
基础查询式传输代码:
c复制void SPI_WriteByte(uint8_t data)
{
while(!(SPI1->SR & SPI_SR_TXE)); // 等待发送缓冲区空
SPI1->DR = data;
while(SPI1->SR & SPI_SR_BSY); // 等待传输完成
}
对于高速传输场景,我有三个优化建议:
- 使用DMA:配置SPI_CR2的TXDMAEN和RXDMAEN位
- 采用16位模式提升吞吐量(设置DFF位)
- 合理预取数据减少等待时间
4.3 CRC校验配置
STM32的SPI内置CRC计算单元,配置步骤:
- 设置SPI_CR1的CRCEN位
- 写入SPI_CRCPR寄存器设置多项式
- 发送数据前先发送CRC长度(通过SPI_DR)
- 硬件自动计算并附加CRC
5. 常见问题排查指南
5.1 通信完全无响应
检查清单:
- 确认SPI时钟已使能(RCC寄存器)
- 验证GPIO模式配置正确(特别是复用功能)
- 检查NSS信号是否有效(示波器观察)
- 确认主从设备模式匹配(CPOL/CPHA)
5.2 数据错位或错误
可能原因:
- 位顺序不匹配(LSBFIRST设置)
- 时钟极性/相位错误
- 波特率过高导致信号失真
- 电源噪声干扰
解决方案:我通常会先用低速模式(如1MHz)测试基本通信,再逐步提高速率。
5.3 DMA传输异常
典型症状:
- 数据传输不完整
- DMA中断未触发
- 数据顺序错乱
排查要点:
- 检查DMA通道映射是否正确
- 确认DMA缓冲区地址对齐
- 验证DMA中断优先级设置
- 检查SPI和DMA的时钟是否同步
6. 进阶应用场景
6.1 多从机系统设计
在控制多个SPI设备时,我有两种常用方案:
- 独立片选法:每个从机使用单独的GPIO作为NSS
- 菊花链连接:将多个设备的MISO/MOSI串联,共用SCK和NSS
后者需要设备支持级联模式,优点是节省IO资源,但软件处理更复杂。
6.2 与外部存储器通信
以W25Q128 Flash为例,关键注意事项:
- 需要实现特定的命令序列
- 注意等待Flash就绪(读取状态寄存器)
- 写操作前必须使能写保护(WREN指令)
- 页编程时要考虑地址对齐
6.3 模拟SPI主机的技巧
当STM32的硬件SPI资源不足时,可以用GPIO模拟,要点包括:
- 精确控制时序(使用延时或定时器)
- 注意IO切换速度(影响最大波特率)
- 合理优化代码(用位操作提升效率)
我在某项目中用IO模拟实现了10MHz的SPI通信,关键是用寄存器级操作代替库函数:
c复制#define SCK_HIGH (GPIOB->BSRR = GPIO_BSRR_BS_13)
#define SCK_LOW (GPIOB->BSRR = GPIO_BSRR_BR_13)
最后分享一个调试心得:当SPI通信出现问题时,用逻辑分析仪捕获波形是最直接的诊断方法。我习惯先检查时钟信号是否正常,再对比MOSI/MISO数据是否符合预期,最后验证片选信号的时序关系。