引言

STM32F4 系列(如 STM32F407)最高运行在 168MHz,其内部采用 AHB 总线矩阵连接 CPU、DMA1/DMA2、Flash、SRAM 和外设。当 DMA 以高频率搬运数据(如 ADC 采样、UART 接收、SPI 传输)时,会与 CPU 取指、访存争抢总线带宽,导致 CPU 执行时间被拉长,甚至 DMA 传输发生 FIFO 溢出或总线延迟。本文基于实测数据,分析争抢现象,并给出实用的避让策略。

带宽争抢原理

STM32F4 的总线矩阵支持并行访问,但 CPU 和 DMA 访问同一目标(如 SRAM1 或 Flash)时,必须串行化。实测中,当 DMA2 以 84MHz 时钟搬运 16 位数据到 SRAM1,CPU 同时执行浮点运算并访问同一 SRAM 区域,CPU 的循环周期从 12 个周期增加到 28 个周期,性能下降约 57%。

争抢主要发生在以下场景:

  • DMA 持续写入 SRAM(如 ADC 多通道采样)
  • DMA 从外设读取数据到内存(如 UART 接收大包)
  • CPU 同时进行大量内存访问(如 memcpy、结构体操作)

实测方法

使用 STM32F407 开发板,主频 168MHz,开启 DMA2 通道 0 将 ADC1 的 16 位数据以 1MHz 采样率搬运到 SRAM1 缓冲区。同时,CPU 执行一段基准测试代码(循环累加一个 volatile 变量),通过 GPIO 翻转测量执行时间。

测试结果:

  • 无 DMA 时,基准循环耗时 100us
  • DMA 开启后,耗时 157us,增加 57%
  • 若将 DMA 目标改为 SRAM2,耗时 112us,仅增加 12%

结论:DMA 与 CPU 访问同一 SRAM 是争抢主因,合理分配内存可显著缓解。

避让策略

1. 合理分配内存区域

STM32F4 有多个 SRAM:SRAM1(112KB)、SRAM2(16KB)、SRAM3(64KB,仅 F42x/43x)。将 DMA 缓冲区放在 SRAM2 或 SRAM3,CPU 主要访问 SRAM1,可减少争抢。

// 使用 __attribute__ 将 DMA 缓冲区分配到 SRAM2
uint16_t dma_buf[1024] __attribute__((section(".sram2")));
// 在链接脚本中定义 .sram2 段,或使用 scatter 文件

2. 调整 DMA 优先级

DMA 控制器支持优先级(低/中/高/最高),但优先级只影响 DMA 通道之间,不影响 CPU。然而,合理设置 DMA 优先级可避免多个 DMA 通道互相干扰,间接减少总线占用时间。

DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_Priority = DMA_Priority_High; // 高优先级
DMA_InitStruct.DMA_BufferSize = 1024;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
DMA_Init(DMA2_Stream0, &DMA_InitStruct);

3. 使用双缓冲(Double Buffer)

双缓冲允许 DMA 在后台填充一个缓冲区,CPU 处理另一个,切换时仅更新指针,减少总线占用时间。STM32F4 的 DMA 支持双缓冲模式,通过 DMA_InitStruct.DMA_Mode = DMA_Mode_Circular 和 DMA_DoubleBufferModeConfig 配置。

// 初始化双缓冲
DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buf1, (uint32_t)buf2, DMA_Memory_0);
DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE);
// 在 DMA 传输完成中断中切换当前内存
void DMA2_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) {
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
        uint32_t cur = DMA_GetCurrentMemoryTarget(DMA2_Stream0);
        if (cur == DMA_Memory_0) {
            // 处理 buf1
        } else {
            // 处理 buf2
        }
    }
}

4. 降低 DMA 传输频率

如果数据速率允许,可降低 DMA 请求频率或使用 FIFO 阈值。DMA 的 FIFO 可缓存数据,减少总线访问次数。配置 FIFO 模式:

DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

5. 使用 AHB 总线矩阵的仲裁优先级

STM32F4 的 AHB 总线矩阵支持可编程仲裁(固定优先级或轮询)。默认是轮询,但可通过修改 AHB 仲裁寄存器(AHB1ENR 中的相关位)设置为固定优先级,让 CPU 优先。但注意,这会影响 DMA 实时性,需权衡。

// 设置 AHB 仲裁为固定优先级(CPU 优先)
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // 使能 DMA2 时钟
// 通过修改 AHB 仲裁寄存器(需参考参考手册)
// 例如:AHB1->ARB = 0x01; // 具体位定义见 RM0090

完整代码示例

以下示例演示如何配置 DMA 双缓冲,并将缓冲区分配到 SRAM2,同时设置 FIFO 模式。

#include "stm32f4xx.h"

// 定义缓冲区到 SRAM2(需在链接脚本中增加 .sram2 段)
__attribute__((section(".sram2"))) uint16_t buf1[1024];
__attribute__((section(".sram2"))) uint16_t buf2[1024];

void DMA2_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) {
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
        uint32_t cur = DMA_GetCurrentMemoryTarget(DMA2_Stream0);
        if (cur == DMA_Memory_0) {
            // 处理 buf1(例如计算均值)
            uint32_t sum = 0;
            for (int i = 0; i < 1024; i++) sum += buf1[i];
            // ...
        } else {
            // 处理 buf2
        }
    }
}

void DMA_Config(void) {
    DMA_InitTypeDef DMA_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // 使能 DMA2 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    // 配置 DMA2 Stream0,通道0(例如 ADC1)
    DMA_InitStruct.DMA_Channel = DMA_Channel_0;
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)buf1;
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStruct.DMA_BufferSize = 1024;
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Enable;
    DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
    DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA2_Stream0, &DMA_InitStruct);

    // 配置双缓冲
    DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buf2, DMA_Memory_1);
    DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE);

    // 使能传输完成中断
    DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE);

    // 配置 NVIC
    NVIC_InitStruct.NVIC_IRQChannel = DMA2_Stream0_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 使能 DMA 流
    DMA_Cmd(DMA2_Stream0, ENABLE);
}

int main(void) {
    // 系统时钟初始化(168MHz)
    SystemInit();
    // 配置 ADC1 等外设(略)
    DMA_Config();
    while (1) {
        // 主循环执行其他任务,DMA 后台搬运
    }
}

注意事项

  • 将缓冲区分配到 SRAM2 时,必须确保链接脚本定义了 .sram2 段,否则编译报错。
  • 双缓冲模式下,DMA 传输完成中断中必须读取当前内存目标,否则可能处理旧数据。
  • 调整 AHB 仲裁优先级需谨慎,可能导致 DMA 延迟增加,适合对 CPU 实时性要求极高的场景。
  • 实测中,DMA 的 FIFO 模式能减少总线访问次数,但会增加延迟,需根据数据速率权衡。
  • 如果使用多个 DMA 通道,建议将高实时性通道设为高优先级,并避免同时访问同一 SRAM 区域。

总结

STM32F4 在 168MHz 下,DMA 与 CPU 争抢 AHB 带宽是常见性能瓶颈。通过合理分配内存(如 SRAM2)、使用双缓冲、配置 FIFO 和调整优先级,可以显著减少争抢,提升系统实时性。实测表明,简单的内存分配优化即可将性能损失从 57% 降至 12%,效果显著。开发者应根据具体应用场景,综合运用上述策略。