# 引言 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,可减少争抢。 ```c // 使用 __attribute__ 将 DMA 缓冲区分配到 SRAM2 uint16_t dma_buf[1024] __attribute__((section(".sram2"))); // 在链接脚本中定义 .sram2 段,或使用 scatter 文件 ``` ## 2. 调整 DMA 优先级 DMA 控制器支持优先级(低/中/高/最高),但优先级只影响 DMA 通道之间,不影响 CPU。然而,合理设置 DMA 优先级可避免多个 DMA 通道互相干扰,间接减少总线占用时间。 ```c 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 配置。 ```c // 初始化双缓冲 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 模式: ```c 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 实时性,需权衡。 ```c // 设置 AHB 仲裁为固定优先级(CPU 优先) RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // 使能 DMA2 时钟 // 通过修改 AHB 仲裁寄存器(需参考参考手册) // 例如:AHB1->ARB = 0x01; // 具体位定义见 RM0090 ``` # 完整代码示例 以下示例演示如何配置 DMA 双缓冲,并将缓冲区分配到 SRAM2,同时设置 FIFO 模式。 ```c #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%,效果显著。开发者应根据具体应用场景,综合运用上述策略。