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 字节,减少总线仲裁次数。例如:

// 配置 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 也能从另一缓冲继续传输,消除软件介入的抖动。

// 启用双缓冲
HAL_DMAEx_MultiBufferStart_IT(&hdma_spi1_tx, (uint32_t)tx_buf1, (uint32_t)tx_buf2, 256);

四、完整代码示例:确定性 SPI-DMA 全双工

以下代码基于 STM32F407,主频 168MHz,SPI1 全双工,DMA2 传输,使用双缓冲和中断。

// 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)内,满足大多数实时通信需求。记住,确定性传输的关键在于减少总线竞争,而非单纯提高主频。