在嵌入式开发中,SPI(串行外设接口)是连接传感器、Flash芯片、显示屏等外设的高速通道。本文将从实战角度出发,带你掌握STM32上SPI的核心配置、多从机扩展策略以及标准库与HAL库的实现细节。无论你是刚接触SPI的初学者,还是希望优化现有项目的开发者,这里都有你需要的干货。
1. SPI通信基础与实战价值
SPI是一种全双工、同步串行通信协议,通过四根线(SCK、MOSI、MISO、CS)实现主从设备间的数据交换。相比I2C,SPI速度更快(可达几十Mbps),且全双工特性使其在需要同时收发数据的场景下效率极高。常见应用包括:读取Flash芯片、驱动LCD屏幕、连接温度/气压传感器等。
SPI的缺点也很明显:每个从设备需要独立的片选线(CS),当设备增多时会占用大量GPIO。不过,通过硬件扩展方案(如74HC595)或软件策略(如菊花链),可以缓解这一瓶颈。
2. SPI工作模式深度解析
2.1 时钟极性与相位
CPOL(时钟极性)定义空闲时SCK的电平:CPOL=0为空闲低电平,CPOL=1为空闲高电平。CPHA(时钟相位)决定数据采样边沿:CPHA=0在第一个边沿采样,CPHA=1在第二个边沿采样。四种模式组合如下:
- 模式0:CPOL=0, CPHA=0
- 模式1:CPOL=0, CPHA=1
- 模式2:CPOL=1, CPHA=0
- 模式3:CPOL=1, CPHA=1
实战建议:模式0和模式3最为常用。模式0适合低速设备(如大多数传感器),模式3适合高速设备(如Flash芯片)。务必查阅从设备数据手册,模式不匹配会导致通信完全失败。例如,W25Q64 Flash支持模式0/3,而BMP280气压计只支持模式3。
3. 多从机扩展方案
3.1 独立片选方案
每个从设备使用独立的CS引脚,主设备通过拉低对应CS来选择通信对象。优点是简单直观、无干扰;缺点是GPIO消耗大。当从设备超过4个时,建议使用74HC595或GPIO扩展芯片,仅需3个GPIO即可控制8个片选信号。
3.2 菊花链连接方案
多个从设备串联,共享一个CS。数据从主设备发出,依次经过每个从设备,最后返回主设备。优点:节省GPIO(仅需1个CS);缺点:无法单独寻址、延迟增加、并非所有设备支持。适用于LED驱动芯片等可级联的场景。
⚠️ 注意:菊花链模式下,软件需处理数据包的转发与校验,复杂度较高。
4. 标准库配置详解
4.1 GPIO与SPI初始化
标准库需要手动配置GPIO模式:SCK和MOSI设为复用推挽输出,MISO设为浮空输入或上拉输入,CS设为推挽输出(软件控制)。以下为初始化代码示例:
void SPI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 配置SCK和MOSI为复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置MISO为上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置片选线为推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// SPI配置
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_High; // 模式3
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge; // 模式3
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStruct);
SPI_Cmd(SPI1, ENABLE);
}4.2 数据收发实现
标准库的收发需自行处理标志位等待和超时。以下是一个完整的单字节收发函数:
uint8_t SPI_TransmitReceive(uint8_t data)
{
// 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPI1, data);
// 等待接收完成
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收到的数据
return SPI_I2S_ReceiveData(SPI1);
}该函数通过轮询方式等待发送和接收完成。实际项目中,可扩展为多字节传输或加入超时重试机制。
5. HAL库配置实战
5.1 CubeMX图形化配置
使用STM32CubeMX可大幅简化配置:选择SPI接口,设置工作模式、数据大小(8位或16位)、时钟极性和相位。建议将CS设为软件控制(GPIO Output),以便灵活管理。CubeMX会自动配置SPI引脚的复用功能,你只需额外配置CS引脚。
时钟源来自APB总线,根据需求设置分频系数。例如,APB2时钟为72MHz时,分频系数设为4,则SPI速率可达18MHz。
5.2 HAL库函数使用技巧
HAL库提供了三大类SPI函数:
HAL_SPI_Transmit():阻塞模式发送/接收HAL_SPI_Receive():中断模式发送/接收HAL_SPI_TransmitReceive():DMA模式发送/接收
以下是一个阻塞模式收发示例(注意超时时间设置):
// 初始化SPI
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
// 数据传输示例
uint8_t txData[4] = {0x90, 0x00, 0x00, 0x00};
uint8_t rxData[4];
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 4, 1000);建议:超时时间应根据数据长度和SPI速率计算,例如传输100字节、速率10Mbps时,超时设为5ms即可。
6. 常见外设驱动开发
6.1 Flash存储器驱动(W25Q系列)
W25Q系列支持模式0和模式3。驱动开发需遵循以下步骤:
- 初始化序列:写使能 → 读ID → 确认设备
- 扇区擦除(4KB为单位) → 页编程(256字节为单位)
- 注意:写入前必须擦除,且不能跨页写入。
以下为Flash读ID和写操作的代码片段:
// Flash读ID示例
uint8_t Flash_ReadID(void)
{
uint8_t txData[4] = {0x90, 0x00, 0x00, 0x00};
uint8_t rxData[4];
CS_Low(); // 片选拉低
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 4, 100);
CS_High(); // 片选拉高
return rxData[3]; // 返回设备ID
}6.2 传感器驱动开发(BMP280)
BMP280要求SPI模式3(CPOL=1, CPHA=1)。读写操作通过寄存器地址进行:先发送寄存器地址(读操作时地址最高位为1),再接收数据。温度/压力数据为20位,需按手册公式转换为实际值。
以下为BMP280初始化与数据读取代码:
// 读取BMP280芯片ID
uint8_t BMP280_ReadID(void)
{
uint8_t txData[2] = {0xD0 | 0x80, 0x00}; // 读寄存器0xD0
uint8_t rxData[2];
CS_Low();
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 2, 100);
CS_High();
return rxData[1];
}7. 调试技巧与常见问题
7.1 调试工具与方法
- 逻辑分析仪:最推荐,可直观查看SCK、MOSI、MISO、CS时序。
- 示波器:适合观察波形质量(如上升沿、噪声)。
- 软件调试:先读取设备ID验证基本通信,再用GPIO模拟SPI时序排除硬件问题。
7.2 常见问题解决
- 模式不匹配:检查CPOL/CPHA是否与从设备数据手册一致。
- 时序问题:CS信号的建立/保持时间需满足设备要求。
- 电源噪声:在高速通信时,每个SPI设备电源引脚旁加0.1μF去耦电容。
- PCB布局:时钟线与数据线避免平行走线过长,减少串扰。
✅ 实用技巧:在代码中添加详细的调试日志(记录每次通信的参数、结果和时间戳),可大幅加速问题定位。
8. 性能优化与实战建议
8.1 DMA传输优化
对于大数据量传输(如Flash读写、LCD刷新),使用DMA可释放CPU。HAL库提供HAL_SPI_Transmit_DMA()和HAL_SPI_Receive_DMA()函数。建议采用双缓冲区机制:一个缓冲区用于DMA传输,另一个用于数据处理。
以下为DMA传输示例:
// DMA传输示例
uint8_t dmaBuffer[256];
HAL_SPI_Transmit_DMA(&hspi1, dmaBuffer, 256);
// DMA传输完成回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
// 处理传输完成事件
}8.2 实时性优化
- 中断方式优于轮询,但中断处理函数应简短(仅设置标志位)。
- 多从设备场景下,规划通信时序,避免某个设备长时间占用总线。
- 适当降低SPI速率(如从18MHz降至9MHz),可提升系统对其他事件的响应。
8.3 可靠性设计
- 增加CRC校验:虽然增加开销,但能有效检测数据错误。
- 超时重试机制:通信失败后自动重试3次,仍失败再报错。
- 电源管理:每个SPI设备电源引脚旁加去耦电容,特别是在高速通信时。
9. 进阶技巧:与高级语言协同开发
在嵌入式与上位机联合调试中,Python和JavaScript常用于编写测试工具。例如,用Python的spidev库模拟SPI主设备进行协议验证,或用Node.js(JavaScript)通过串口与STM32通信。Go语言和TypeScript也越来越多用于边缘计算设备的数据处理。掌握这些语言的数据解析能力,能让你在调试SPI外设时如虎添翼。
总结
SPI通信看似简单,但模式选择、多从机扩展、驱动开发中暗藏不少“坑”。本文从基础概念到HAL/标准库实现,再到调试与优化,为你梳理了一条清晰的实战路径。记住:读透数据手册是成功的第一步,用好逻辑分析仪是调试的利器,合理使用DMA是性能的关键。希望这些经验能帮你少走弯路,快速搞定STM32上的SPI通信。
浙公网安备 33010602011771号