引言:一个诡异的 DMA 数据错误

在嵌入式开发中,当你使用 STM32F4 的 DMA 传输数据(例如从 ADC 或 UART 接收数据到内存),同时 CPU 也访问同一块内存时,可能会遇到数据时而正确、时而错误的现象。这通常不是逻辑问题,而是 D-Cache(数据缓存)与 DMA 之间的数据一致性问题。STM32F4 内核(Cortex-M4)带有 4KB 的 D-Cache,默认情况下,CPU 读写内存会经过 Cache,而 DMA 直接访问物理内存,两者可能看到不同的数据。

原理剖析:D-Cache 与 DMA 的冲突根源

1. D-Cache 的工作机制

  • D-Cache 是 CPU 与主存之间的高速缓存,以“行”(通常 32 字节)为单位存储数据。
  • 当 CPU 读内存时,若命中 Cache,则直接返回 Cache 中的数据;若未命中,则从主存加载到 Cache。
  • 当 CPU 写内存时,若采用“写回”(Write-back)策略,数据先写入 Cache,标记为“脏”,稍后才写回主存。

2. DMA 的访问路径

  • DMA 控制器直接通过总线访问物理内存(SRAM 或外部存储器),不经过 Cache。
  • 因此,DMA 写入内存时,CPU 可能从 Cache 中读到旧数据;CPU 写入内存后,DMA 可能从主存中读到旧数据。

3. 冲突场景示例

  • 场景 A:DMA 从外设接收数据到缓冲区,CPU 读取该缓冲区。若 CPU 之前已缓存了该区域的数据,则可能读到旧值。
  • 场景 B:CPU 填充缓冲区,然后启动 DMA 发送。若 CPU 写入的数据还停留在 Cache 中,DMA 会发送主存中的旧数据。

解决方案:MPU 配置内存属性

MPU(内存保护单元)允许你为不同内存区域设置访问权限和 Cache 策略。通过将 DMA 使用的内存区域配置为“非缓存”(或“写透”),可以强制 CPU 直接访问主存,从而避免一致性问题。

配置步骤

  1. 确定内存区域:通常使用 SRAM 的某一段(如 0x20000000 起始的 32KB),或者外部 SDRAM。
  2. 使能 MPU:在系统初始化时,使能 MPU 并配置区域。
  3. 设置区域属性:
    • 对于 DMA 缓冲区,建议配置为“非缓存”(TEX=1, C=0, B=0)或“写透”(TEX=0, C=1, B=0)。
    • 非缓存模式最简单,但性能稍低;写透模式读性能较好,但写操作直接写主存。
  4. 注意对齐:MPU 区域大小必须是 2 的幂,且起始地址对齐到区域大小。

完整代码示例

以下代码基于 STM32F407,使用 HAL 库,将 SRAM 的 0x20000000 开始的 32KB 配置为非缓存区域。

#include "stm32f4xx_hal.h"

// 定义 DMA 缓冲区(位于非缓存区域)
// 注意:需要将缓冲区放置到指定地址,使用 __attribute__((at())) 或链接脚本
uint8_t dma_buffer[1024] __attribute__((at(0x20000000)));

// MPU 配置函数
void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置区域 0:0x20000000,大小 32KB,非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 主函数示例
int main(void)
{
    HAL_Init();
    SystemClock_Config();

    // 配置 MPU(必须在启用 Cache 之前)
    MPU_Config();

    // 启用 D-Cache(如果之前未启用)
    SCB_EnableDCache();

    // 初始化 DMA 等外设...

    while (1)
    {
        // 使用 dma_buffer 进行 DMA 传输和 CPU 访问
    }
}

代码说明

  • dma_buffer 被强制放置在 0x20000000 地址,确保落在 MPU 配置的区域内。
  • MPU 配置中,IsCacheable 和 IsBufferable 均设为 NOT_,即非缓存、非缓冲,CPU 访问直接走主存。
  • 注意:MPU 配置必须在使能 D-Cache 之前完成,否则可能不生效。

调试技巧与注意事项

  • 检查 MPU 配置是否生效:可以通过读取 MPU 寄存器或使用调试器查看内存访问行为。
  • 使用 SCB_CleanDCache() 和 SCB_InvalidateDCache():如果不想配置 MPU,可以在 DMA 传输前后手动清理/无效化 Cache,但效率较低且容易遗漏。
  • 缓冲区对齐:DMA 缓冲区建议 32 字节对齐,避免跨 Cache 行导致部分一致性问题。
  • 性能权衡:非缓存区域访问速度较慢,仅将 DMA 缓冲区放在该区域,其他数据仍使用 Cache 以保持性能。
  • 多区域配置:如果使用外部 SDRAM,同样需要配置 MPU 区域,并注意总线带宽。

总结

通过 MPU 将 DMA 使用的内存区域配置为非缓存,是解决 STM32F4 上 DMA 与 CPU 数据一致性问题的标准方法。本文的原理分析和代码示例可以帮助你快速定位并修复此类问题。记住,在嵌入式开发中,Cache 一致性是高性能与正确性之间的平衡,理解底层机制才能写出健壮的代码。