1. SPI通信协议基础认知
第一次接触SPI总线是在2015年调试一个传感器模块时,当时被其简洁的四线制结构所吸引。与I2C相比,SPI没有繁琐的地址分配和应答机制,就像两个工程师直接通过电话沟通,一方说一方听,效率极高。SPI全称Serial Peripheral Interface,是摩托罗拉在1980年代提出的同步串行通信协议,如今已成为嵌入式领域最常用的短距离通信方案之一。
SPI采用主从架构,通常由一个主设备(Master)和一个或多个从设备(Slave)组成。其物理连接只需要四根线:
- SCLK:时钟信号,由主设备产生
- MOSI:主设备输出,从设备输入
- MISO:主设备输入,从设备输出
- SS/CS:片选信号(低电平有效)
实际布线时建议在SCLK上加22-100Ω的终端电阻,可有效抑制信号振铃。我在某次电机控制项目中因忽略这点导致通信误码率高达10%
SPI的工作模式有四种组合,由CPOL(时钟极性)和CPHA(时钟相位)决定:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样时刻 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 奇数边沿 |
| 1 | 0 | 1 | 低电平 | 偶数边沿 |
| 2 | 1 | 0 | 高电平 | 奇数边沿 |
| 3 | 1 | 1 | 高电平 | 偶数边沿 |
大多数SPI设备默认支持模式0和模式3,例如我常用的NOR Flash芯片W25Q128就工作在模式0。在STM32的HAL库中,通过以下结构体配置模式:
c复制hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPha = SPI_PHASE_1EDGE; // CPHA=0
2. 40位数据包的特殊设计
传统SPI通信通常以8的整数倍传输数据,但在某些特殊场景下需要非标准长度。去年在开发高精度电子秤时,就遇到了需要传输40位ADC数据的情况。这种设计主要出于以下考量:
- 精度需求:24位ADC已无法满足某些称重场景的0.001g级分辨率要求
- 效率优化:相比分多次传输32+8位,单次40位传输可节省20%时间
- 校验冗余:后8位可用于存放CRC校验码或状态标志位
实现40位传输需要解决三个关键问题:
2.1 时钟控制策略
STM32的SPI外设默认只支持8/16位数据长度,需要通过特殊配置实现40位传输。以STM32F4系列为例:
c复制// 在SPI初始化后动态修改数据长度
MODIFY_REG(hspi->Instance->CR2, SPI_CR2_DS, SPI_DATASIZE_32BIT);
// 实际发送时需要拆分为32+8位
HAL_SPI_Transmit(hspi, (uint8_t*)&data32, 1, timeout);
HAL_SPI_Transmit(hspi, &data8, 1, timeout);
实测发现连续传输时两次发送间隔需大于1us,否则从设备可能无法正确识别数据包边界
2.2 数据对齐处理
接收到的40位数据在内存中存储需要特殊处理。推荐使用联合体(union)结构:
c复制typedef union {
uint64_t full_data; // 整体访问
struct {
uint32_t low32; // 低32位
uint8_t high8; // 高8位
uint8_t reserved[3]; // 对齐填充
} parts;
} SPI40_Data;
2.3 校验机制设计
针对40位数据的校验建议采用分段CRC:
- 对前32位计算CRC8
- 对后8位直接作为校验和
- 接收方进行双重验证
c复制uint8_t CheckSPI40Data(SPI40_Data* pData) {
uint8_t crc = CRC8_Calculate(pData->parts.low32, 4);
uint8_t sum = pData->parts.high8;
return (crc == (sum & 0x0F)) && ((sum >> 4) == 0x0A);
}
3. STM32硬件SPI实现详解
3.1 CubeMX基础配置
- 在Connectivity选项卡启用SPIx
- 配置为Full-Duplex Master
- 参数设置建议:
- Clock Prescaler: 根据从设备要求选择(常用PCLK/8)
- Data Size: 默认8位(后续代码动态修改)
- First Bit: MSB First
- NSS Signal: Software
硬件NSS信号在40位传输中容易产生误触发,建议采用软件控制
3.2 中断+DMA优化方案
对于高速数据传输,推荐使用DMA模式。以下是关键配置代码:
c复制// DMA发送配置
hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
HAL_DMA_Init(&hdma_tx);
// 中断处理
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if(hspi->Instance == SPI1) {
// 处理32位传输完成
MODIFY_REG(hspi->Instance->CR2, SPI_CR2_DS, SPI_DATASIZE_8BIT);
// 启动后续8位传输
HAL_SPI_Transmit_DMA(hspi, &tx_buf[4], 1);
}
}
3.3 实测性能数据
在不同时钟分频下的传输耗时对比(基于STM32F407@168MHz):
| 预分频值 | 40位传输时间(us) | 理论速率(Mbps) |
|---|---|---|
| 2 | 1.19 | 33.6 |
| 4 | 2.38 | 16.8 |
| 8 | 4.76 | 8.4 |
| 16 | 9.52 | 4.2 |
实际项目中建议先以较低速率调试,稳定后再逐步提高频率
4. 常见问题排查手册
4.1 数据错位问题
现象:接收到的32位和8位数据位置颠倒
排查步骤:
- 检查SPI_CR2寄存器的FRXTH位:必须设置为0(RXFIFO阈值=32位)
- 验证DMA配置:Memory端必须设置为WORD对齐
- 用逻辑分析仪捕获波形,确认CS信号在40位传输期间保持低电平
4.2 时钟干扰问题
现象:高频率下数据出现随机错误
解决方案:
- 在SCLK线上串联33Ω电阻
- 将PCB走线长度控制在10cm以内
- 在STM32端配置SPI CRCCALC功能(即使不使用CRC)
c复制// 启用硬件CRC计算可改善信号质量
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_ENABLE;
hspi1.Init.CRCPolynomial = 7;
HAL_SPI_Init(&hspi1);
4.3 多从设备管理
当系统需要连接多个40位SPI设备时,建议采用如下架构:
-
硬件设计:
- 每个从设备独立CS线
- 共用SCK/MOSI/MISO线
- 在MISO线上加74HC125三态缓冲器
-
软件流程:
c复制void SPIx_SelectDevice(uint8_t dev_id) {
// 先关闭所有片选
HAL_GPIO_WritePin(GPIOA, CS1_Pin|CS2_Pin|CS3_Pin, GPIO_PIN_SET);
// 根据ID选择特定设备
switch(dev_id) {
case 1: HAL_GPIO_WritePin(GPIOA, CS1_Pin, GPIO_PIN_RESET); break;
case 2: HAL_GPIO_WritePin(GPIOA, CS2_Pin, GPIO_PIN_RESET); break;
case 3: HAL_GPIO_WritePin(GPIOA, CS3_Pin, GPIO_PIN_RESET); break;
}
// 延时确保建立时间
DWT_Delay(5);
}
5. 进阶优化技巧
5.1 利用TIMER精确控制时序
对于严格时序要求的设备,可配合定时器产生精确延时:
c复制void SPI_DelayUS(uint16_t us) {
__HAL_TIM_SET_COUNTER(&htim2, 0);
HAL_TIM_Base_Start(&htim2);
while(__HAL_TIM_GET_COUNTER(&htim2) < us);
HAL_TIM_Base_Stop(&htim2);
}
5.2 动态时钟调整技术
根据传输阶段自动切换时钟速度:
- 命令阶段:低速(<1MHz)
- 数据传输阶段:高速(最大支持频率)
- 空闲状态:关闭时钟输出
c复制void SPIx_SetSpeed(uint32_t prescaler) {
hspi1.Instance->CR1 &= ~SPI_CR1_SPE;
MODIFY_REG(hspi1.Instance->CR1, SPI_CR1_BR, prescaler << 3);
hspi1.Instance->CR1 |= SPI_CR1_SPE;
}
5.3 错误重传机制实现
针对工业环境设计自动重传方案:
c复制#define MAX_RETRY 3
SPI_Status SPIx_Transmit40(uint32_t data32, uint8_t data8) {
uint8_t retry = 0;
SPI40_Data tx_data = {.parts.low32=data32, .parts.high8=data8};
while(retry < MAX_RETRY) {
if(HAL_SPI_Transmit(&hspi1, (uint8_t*)&tx_data, 5, 100) == HAL_OK) {
return SPI_OK;
}
retry++;
SPIx_Reset(); // 复位SPI总线
}
return SPI_ERROR;
}
在最近的一个工业传感器项目中,这套机制将通信成功率从92%提升到99.99%。关键是在每次重传前执行完整的总线复位:
c复制void SPIx_Reset(void) {
HAL_SPI_DeInit(&hspi1);
MX_SPI1_Init(); // 重新初始化
__HAL_SPI_ENABLE(&hspi1);
}