STM32F4 168MHz 下 SPI-DMA 全双工传输的延迟抖动分析与规避策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列以 168MHz 主频运行时,SPI 与 DMA 的全双工传输常因总线仲裁引入不可预测的延迟抖动,影响实时性。本文深入剖析 Cortex-M4 总线矩阵的仲裁机制,量化抖动来源,并给出基于寄存器配置、DMA 优先级调整及双缓冲策略的精确规避方案,附完整代码示例,助你打造确定性传输。
# STM32F4 168MHz 下 SPI-DMA 全双工传输的延迟抖动分析与规避策略
## 一、问题本质:总线仲裁的“隐形时钟”
STM32F4 采用多主多从总线矩阵(BusMatrix),CPU、DMA1、DMA2、以太网等主设备可并行访问不同从设备(Flash、SRAM、APB 外设)。当 SPI 通过 DMA 进行全双工传输时,DMA 控制器需同时访问:
- **源地址**:如内存缓冲区(SRAM)
- **目标地址**:如 SPI->DR 寄存器(APB2 总线)
每次 DMA 传输需占用两次总线周期(读源+写目标)。若此时 CPU 正在执行 Flash 取指或访问 SRAM,总线矩阵会按优先级仲裁,导致 DMA 请求等待。这种等待时间随 CPU 活动状态波动,形成 0~多个时钟周期的抖动,直接影响 SPI 时钟的连续性和数据采样的确定性。
## 二、抖动量化:从理论到实测
在 168MHz 下,一个总线周期约 5.95ns。典型抖动场景:
- **CPU 密集运算**(如浮点计算):Flash 预取失效时,CPU 连续取指占用总线,DMA 等待可达 3~5 个周期(约 18~30ns)。
- **中断响应**:NVIC 响应中断时,CPU 压栈(多次内存访问),DMA 可能被阻塞 10 个周期以上。
- **DMA 内部竞争**:若同时使用 DMA1 和 DMA2 的多个通道,低优先级通道可能被高优先级通道抢占。
实测方法:在 SPI 传输完成中断中翻转 GPIO,用逻辑分析仪测量相邻传输间隔的波动。通常未优化时抖动可达 ±50ns,优化后可控制在 ±10ns 内。
## 三、精确规避策略:四层防护
### 1. 合理配置 DMA 优先级与通道映射
- 将 SPI 使用的 DMA 通道设为最高优先级(Very High),并确保同一 DMA 控制器内其他通道优先级低于它。
- 优先使用 DMA2,因为 DMA2 可访问所有内存(包括 SRAM1/SRAM2/Flash),而 DMA1 只能访问 SRAM1,减少总线冲突面。
### 2. 利用 FIFO 与突发模式
DMA 的 FIFO(4 字深)可缓冲数据,配合突发传输(Burst)一次占用总线传输 4/8/16 字节,减少总线仲裁次数。例如:
```c
// 配置 DMA2_Stream3(SPI1_TX)为双缓冲+突发
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_Channel = DMA_Channel_3;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)tx_buf;
DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStruct.DMA_BufferSize = 256;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single; // 内存侧单次,避免占用过多总线
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; // 外设侧单次
HAL_DMA_Init(&hdma_spi1_tx);
```
### 3. 调整 SPI 时钟相位与 DMA 请求时机
SPI 的 DMA 请求在 TXE(发送缓冲区空)或 RXNE(接收缓冲区非空)时产生。若设置 SPI->CR2 的 FRXTH 位(帧格式),可控制 DMA 请求的阈值。建议:
- 设置 FRXTH=1(8 位帧时,RXNE 在数据字节到达时立即置位),减少 DMA 等待。
- 使用 SPI 的“DMA 最后一次传输”中断(EOT)而非传输完成中断,避免因软件处理延迟影响下一帧。
### 4. 双缓冲与乒乓缓冲
使用 DMA 双缓冲模式,在后台填充下一帧数据,同时当前帧传输。这样即使 CPU 因中断延迟,DMA 也能从另一缓冲继续传输,消除软件介入的抖动。
```c
// 启用双缓冲
HAL_DMAEx_MultiBufferStart_IT(&hdma_spi1_tx, (uint32_t)tx_buf1, (uint32_t)tx_buf2, 256);
```
## 四、完整代码示例:确定性 SPI-DMA 全双工
以下代码基于 STM32F407,主频 168MHz,SPI1 全双工,DMA2 传输,使用双缓冲和中断。
```c
// SPI1 初始化(主模式,8位,CPOL=0,CPHA=0,速率 21MHz)
void SPI1_Init(void) {
SPI_HandleTypeDef hspi1 = {0};
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;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 168/8=21MHz
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
HAL_SPI_Init(&hspi1);
// 使能 SPI 的 DMA 请求
__HAL_SPI_ENABLE(&hspi1);
}
// DMA 双缓冲配置(TX 和 RX 各用两个缓冲)
void DMA_SPI_Config(uint8_t *tx1, uint8_t *tx2, uint8_t *rx1, uint8_t *rx2, uint16_t len) {
// TX 通道:DMA2_Stream3, Channel 3
hdma_spi1_tx.Instance = DMA2_Stream3;
hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_VERY_HIGH;
hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_spi1_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
hdma_spi1_tx.Init.MemBurst = DMA_MBURST_SINGLE;
hdma_spi1_tx.Init.PeriphBurst = DMA_PBURST_SINGLE;
HAL_DMA_Init(&hdma_spi1_tx);
// 链接 DMA 到 SPI1
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
// 启动双缓冲传输
HAL_DMAEx_MultiBufferStart_IT(&hdma_spi1_tx, (uint32_t)tx1, (uint32_t)tx2, len);
// RX 通道:DMA2_Stream0, Channel 3(类似配置,方向外设到内存)
// 省略具体代码,注意优先级设为 Very High,使用双缓冲
}
// 中断回调:处理传输完成,切换缓冲
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
// 在此更新缓冲指针或通知应用层
// 注意:双缓冲模式下,DMA 会自动切换,此回调仅用于状态同步
}
}
// 主函数示例
int main(void) {
HAL_Init();
SystemClock_Config(); // 配置 168MHz
SPI1_Init();
uint8_t tx_buf1[256], tx_buf2[256], rx_buf1[256], rx_buf2[256];
DMA_SPI_Config(tx_buf1, tx_buf2, rx_buf1, rx_buf2, 256);
while (1) {
// 填充 tx_buf1 和 tx_buf2 数据
// 传输由 DMA 自动进行,CPU 可执行其他任务
}
}
```
## 五、注意事项与进一步优化
- **内存对齐**:DMA 缓冲区建议 32 字节对齐(`__attribute__((aligned(32)))`),避免跨 SRAM 边界导致额外总线周期。
- **关闭预取或调整等待状态**:Flash 等待周期设为 5(168MHz 时),并开启预取缓冲,减少 CPU 取指对总线的占用。
- **使用 D-Cache**:若使用外部 SRAM,启用 D-Cache 并配置为写通模式,避免 DMA 与缓存一致性开销。
- **中断优先级**:将 DMA 中断优先级设为最高(抢占优先级 0),确保传输完成及时处理,但注意避免长时间中断阻塞。
- **实测验证**:使用逻辑分析仪测量 SPI 时钟的周期抖动,若仍超标,可考虑将 SPI 时钟降频(如 84MHz),或改用 SPI 的硬件 NSS 管理以简化时序。
通过以上策略,在 168MHz 主频下,SPI-DMA 全双工传输的延迟抖动可控制在 ±2 个总线周期(约 12ns)内,满足大多数实时通信需求。记住,确定性传输的关键在于减少总线竞争,而非单纯提高主频。