引言

在 STM32F4 高性能系列中,Cortex-M4 内核集成了可选的 D-Cache(数据缓存)和 I-Cache,用于加速对慢速存储器(如外部 SDRAM)的访问。然而,D-Cache 的引入也带来了一个经典陷阱:当 DMA 控制器直接访问内存时,CPU 和 DMA 看到的数据可能不一致,导致数据错乱、校验失败等随机故障。本文从底层原理出发,手把手教你通过 MPU(内存保护单元)配置来规避此问题。

1. D-Cache 与 DMA 数据一致性问题的根源

1.1 D-Cache 的工作机制

D-Cache 是 CPU 与主存之间的高速缓存,以“行”(Line)为单位(STM32F4 为 32 字节)存储数据。当 CPU 读取内存时,若命中 Cache 则直接返回,否则从主存加载整行到 Cache;当 CPU 写入时,采用 write-back(写回)策略:数据先写入 Cache,标记为脏(Dirty),仅在缓存行被替换或显式 Clean 时才写回主存。

1.2 冲突场景

  • DMA 读取,CPU 写入:CPU 将数据写入 Cache(尚未写回主存),DMA 从主存读取旧数据,导致 DMA 拿到过期数据。
  • DMA 写入,CPU 读取:DMA 将新数据写入主存,但 CPU 的 Cache 中仍保留旧缓存行,CPU 读回旧数据。

这两种情况都会破坏数据一致性,尤其在网络、音频、图像等高频 DMA 传输中极易触发。

2. 解决方案概览

解决思路有两种:

  1. 彻底禁用 D-Cache:简单粗暴,但牺牲性能,不推荐用于大块数据场景。
  2. 通过 MPU 配置内存区域的 Cache 策略:将 DMA 涉及的缓冲区设置为 非缓存(Non-cacheable)写透(Write-through),既保证一致性,又保留部分缓存加速。

3. 实战配置:使用 MPU 设置非缓存区域

3.1 硬件环境

  • MCU:STM32F407(Cortex-M4,带 D-Cache)
  • 外部 SDRAM:起始地址 0xC0000000,大小 1MB
  • DMA:用于 ADC 采集数据到 SDRAM 缓冲区

3.2 MPU 配置步骤

  1. 使能 MPU:设置 MPU_CTRL 寄存器,使能 MPU 和默认内存映射。
  2. 配置区域:选择要设置为非缓存的区域(如 SDRAM 的某段),设置起始地址、大小、访问权限和 Cache 策略。
  3. 使能区域:将区域使能位置 1。

3.3 代码实现(基于 HAL 库)

#include "stm32f4xx_hal.h"

void MPU_Config_NonCacheable(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置 SDRAM 区域(0xC0000000,大小 1MB)为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
    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_SHAREABLE;        // 共享,便于 DMA
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 在 main 函数中调用
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 配置 MPU 前,确保 D-Cache 已使能(可选)
    SCB_EnableDCache();
    
    MPU_Config_NonCacheable();
    
    // 初始化 DMA 和 SDRAM...
    // 之后 DMA 与 CPU 访问 0xC0000000 区域时,数据直接读写主存,无缓存干扰
    
    while(1)
    {
        // 主循环
    }
}

3.4 关键点说明

  • IsBufferable 和 IsCacheable:两者都设为 NOT,即完全非缓存,CPU 每次读写都直接访问主存,保证一致性。
  • IsShareable:设为共享,允许 DMA 等其他总线主设备访问,避免总线仲裁问题。
  • MPU_TEX_LEVEL0:配合 TEX 字段,实现特定的内存属性,此处为常规外设内存。

4. 进阶:使用 Write-Through 策略平衡性能

如果不想完全放弃缓存,可将区域设置为 Write-Through(写透)策略:CPU 写操作同时更新 Cache 和主存,读操作仍可缓存。这样 DMA 读取时总能拿到最新数据,而 CPU 读性能得以保留。

配置修改如下:

// 修改 MPU 配置中的缓存属性
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;    // 允许缓存
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;         // TEX=1, C=1, B=0 表示 Write-Through

注意:Write-Through 下,CPU 写操作会直接写主存,但读操作仍可能命中旧缓存行(若 DMA 写入了新数据)。因此,仅适用于 CPU 写、DMA 读的场景。对于 DMA 写、CPU 读,仍需非缓存或手动 Clean/Invalidate。

5. 手动维护 Cache 的备用方案

若不想配置 MPU,也可在 DMA 传输前后手动操作 Cache:

  • DMA 读取前:调用 SCB_CleanDCache() 将 CPU 写入的数据写回主存。
  • DMA 写入后:调用 SCB_InvalidateDCache() 使 CPU 缓存行失效,强制重新从主存读取。

但这种方法需要精确控制时机,容易遗漏,且影响性能,仅适合小数据量。

6. 注意事项与避坑指南

  • MPU 配置必须在使能 D-Cache 之前或之后? 建议先配置 MPU 再使能 D-Cache,避免配置期间缓存干扰。
  • 区域大小必须对齐:MPU 区域大小必须是 2 的幂次,且起始地址对齐到区域大小。例如 1MB 区域,起始地址低 20 位必须为 0。
  • 默认内存映射:STM32F4 的默认内存映射中,外部 RAM 区域(0xC0000000-0xDFFFFFFF)默认是 Cacheable 的,因此必须显式覆盖。
  • 调试时:如果出现随机数据错误,优先检查 MPU 配置是否生效,可通过读取 MPU_RBAR 和 MPU_RASR 寄存器验证。
  • 多缓冲区:为不同用途的缓冲区分配不同 MPU 区域,例如 ADC 缓冲区非缓存,显示缓冲区可 Write-Through。

7. 总结

D-Cache 与 DMA 的数据一致性是 STM32F4 开发中的经典陷阱,但通过合理配置 MPU,可以精准控制内存区域的缓存策略,既保证数据正确性,又兼顾性能。本文提供了两种配置方案和完整代码,开发者可根据实际场景选择。记住:在嵌入式世界里,缓存不是免费的午餐,必须明确管理

希望本文能帮你避开这个“坑”,让你的 DMA 传输稳定可靠。如果你有更多实战经验,欢迎交流!