在嵌入式开发中,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个时,建议使用74HC595GPIO扩展芯片,仅需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设备电源引脚旁加去耦电容,特别是在高速通信时。
[AFFILIATE_SLOT_1]

9. 进阶技巧:与高级语言协同开发

在嵌入式与上位机联合调试中,Python和JavaScript常用于编写测试工具。例如,用Python的spidev库模拟SPI主设备进行协议验证,或用Node.js(JavaScript)通过串口与STM32通信。Go语言和TypeScript也越来越多用于边缘计算设备的数据处理。掌握这些语言的数据解析能力,能让你在调试SPI外设时如虎添翼。

[AFFILIATE_SLOT_2]

总结

SPI通信看似简单,但模式选择、多从机扩展、驱动开发中暗藏不少“坑”。本文从基础概念到HAL/标准库实现,再到调试与优化,为你梳理了一条清晰的实战路径。记住:读透数据手册是成功的第一步,用好逻辑分析仪是调试的利器,合理使用DMA是性能的关键。希望这些经验能帮你少走弯路,快速搞定STM32上的SPI通信。