第4章 SPI驱动性能挖掘:时钟极性与相位、DMA双缓冲与片选控制
SPI这个接口,看起来简单——就四根线嘛。但真正要把吞吐量榨干,坑还真不少。我最早做一款高速数据采集卡时,SPI速率配到了18MHz,结果数据老是错位。查了三天,最后发现是时钟相位差了一个边沿。嗯,这一章咱们就把SPI的这几个关键点掰开揉碎了讲。
4.1 时钟极性与相位——配置不对,数据全废
SPI的时钟极性和相位,说白了就是决定数据在时钟的哪个边沿采样。CPOL决定时钟空闲时的电平,CPHA决定数据采样发生在第一个还是第二个边沿。
| 模式 | CPOL | CPHA | 空闲时钟电平 | 采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | 第一个边沿(上升沿) |
| 模式1 | 0 | 1 | 低电平 | 第二个边沿(下降沿) |
| 模式2 | 1 | 0 | 高电平 | 第一个边沿(下降沿) |
| 模式3 | 1 | 1 | 高电平 | 第二个边沿(上升沿) |
配置代码其实很简单,但容易忽略的是:修改SPI配置前必须先关闭SPI外设。否则配置不会生效,甚至可能造成总线冲突。
// 正确的配置方式
SPI_HandleTypeDef hspi1;
void SPI_Config_Mode0(void)
{
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_LOW; // CPOL = 0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0
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);
}
// 切换模式时,务必先DeInit再Init
void SPI_Switch_Mode(uint8_t newCPOL, uint8_t newCPHA)
{
HAL_SPI_DeInit(&hspi1); // 先关闭
hspi1.Init.CLKPolarity = newCPOL;
hspi1.Init.CLKPhase = newCPHA;
HAL_SPI_Init(&hspi1); // 重新初始化
}
4.2 DMA双缓冲模式——让吞吐量翻倍
单缓冲DMA有个问题:当缓冲区满了,你必须停下来处理数据,DMA也得暂停。这段时间总线是空闲的,浪费了。
双缓冲模式(也叫乒乓缓冲)就不一样了。两个缓冲区轮流工作:DMA往缓冲区A写数据时,CPU处理缓冲区B的数据;等A写满了,DMA自动切到B,CPU处理A。两边互不等待。
STM32的DMA支持双缓冲模式,但有个前提:必须使用DMA的循环模式(Circular Mode)。配置时指定两个内存地址,DMA会自动切换。
// DMA双缓冲配置示例
#define BUF_SIZE 512
uint8_t rxBuf0[BUF_SIZE];
uint8_t rxBuf1[BUF_SIZE];
volatile uint8_t activeBuf = 0; // 0: buf0, 1: buf1
void DMA_DoubleBuf_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_spi1_rx.Instance = DMA1_Channel2;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_CIRCULAR; // 必须循环模式
hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_spi1_rx);
// 关联SPI的RX DMA
__HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);
// 启动DMA,指定两个缓冲区
HAL_DMAEx_Start_IT(&hdma_spi1_rx,
(uint32_t)&SPI1->DR,
(uint32_t)rxBuf0,
(uint32_t)rxBuf1,
BUF_SIZE);
}
// 半传输中断:缓冲区0满了,可以处理缓冲区1
void DMA1_Channel2_IRQHandler(void)
{
if(__HAL_DMA_GET_FLAG(&hdma_spi1_rx, DMA_FLAG_HT))
{
__HAL_DMA_CLEAR_FLAG(&hdma_spi1_rx, DMA_FLAG_HT);
activeBuf = 1;
ProcessBuffer(rxBuf1, BUF_SIZE); // 处理缓冲区1
}
// 传输完成中断:缓冲区1满了,处理缓冲区0
if(__HAL_DMA_GET_FLAG(&hdma_spi1_rx, DMA_FLAG_TC))
{
__HAL_DMA_CLEAR_FLAG(&hdma_spi1_rx, DMA_FLAG_TC);
activeBuf = 0;
ProcessBuffer(rxBuf0, BUF_SIZE); // 处理缓冲区0
}
}
- 轮询方式:最大吞吐量约 2.5 MB/s(CPU占用率100%)
- 单缓冲DMA:最大吞吐量约 4.2 MB/s(CPU占用率40%)
- 双缓冲DMA:最大吞吐量约 8.0 MB/s(CPU占用率15%)
4.3 CS片选信号——软件控制 vs 硬件自动
片选信号看似简单,但选择不当会直接影响系统稳定性和吞吐量。
| 控制方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 软件GPIO控制 | 灵活,可任意控制时序 | 增加CPU负担,时序精度受中断影响 | 多从设备、非标准时序 |
| 硬件NSS控制 | 自动管理,零CPU开销 | 时序固定,灵活性差 | 单从设备、高速连续传输 |
我个人习惯:能用硬件NSS就用硬件NSS。为什么?因为软件控制CS时,每次传输前后都要操作GPIO,这个时间虽然短,但累积起来很可观。尤其是在高频传输时,软件CS的延迟会限制最大SPI时钟频率。
举个例子:SPI时钟18MHz,传输1字节需要约0.44μs。软件CS操作(GPIO置位+清零)大约需要0.1μs。看起来不多,但传输1000字节,额外开销就是100μs,相当于少传了约227字节。吞吐量直接打八折。
// 软件CS控制(不推荐用于高速场景)
#define CS_PORT GPIOA
#define CS_PIN GPIO_PIN_4
void SPI_CS_Low(void)
{
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
}
void SPI_CS_High(void)
{
HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
}
void SPI_Transmit_WithSoftCS(uint8_t *data, uint16_t len)
{
SPI_CS_Low();
HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
SPI_CS_High();
}
// 硬件NSS控制(推荐)
// 配置时设置 NSS = SPI_NSS_HARD
// 硬件自动在传输开始前拉低CS,传输结束后拉高CS
void SPI_Config_HardwareNSS(void)
{
hspi1.Init.NSS = SPI_NSS_HARD;
HAL_SPI_Init(&hspi1);
}
// 使用硬件NSS时,传输函数不需要手动控制CS
void SPI_Transmit_WithHardCS(uint8_t *data, uint16_t len)
{
HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
// CS由硬件自动管理
}
- 多个从设备共用SPI总线,需要分别片选
- 从设备要求CS在传输结束后保持低电平一段时间
- 需要CS信号与时钟边沿有特殊时序关系(比如CS必须在时钟第一个边沿之前提前拉低)
4.4 综合调优建议
把上面几个点串起来,一个高性能SPI驱动的调优流程大概是这样的:
- 确认从设备时序——看手册时序图,确定CPOL/CPHA,用逻辑分析仪验证
- 选择CS控制方式——单设备用硬件NSS,多设备或特殊时序用软件CS
- 配置DMA双缓冲——开启循环模式,注册半传输和传输完成中断
- 调整SPI时钟频率——从低速开始逐步提高,直到出现误码,然后降一档
- 优化中断优先级——DMA中断优先级要高于其他非关键中断,避免丢数据
SPI驱动调优,说白了就是平衡——在时序正确的前提下,尽可能减少CPU干预,让硬件自己跑起来。DMA双缓冲和硬件NSS就是帮你实现这个目标的工具。至于时钟极性和相位,那是基本功,配置错了后面再怎么优化都是白搭。