STM32H7 的 D-Cache 与 DMA 一致性:用 MPU 配置 Non-Cacheable 区域的正确姿势

STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,当开发者启用 D-Cache 后,DMA 传输的数据却常常“不翼而飞”或“面目全非”。这背后是 Cache 与 DMA 的一致性问题。本文将带你从原理到实践,彻底解决这一痛点。

一、问题根源:Cache 与 DMA 的“视角差异”

CPU 通过 D-Cache 访问内存,而 DMA 直接访问物理内存。当 CPU 写数据到 Cache 后,若未及时回写到物理内存,DMA 读取的仍是旧数据;反之,DMA 写入新数据到物理内存,CPU 却可能从 Cache 中读到旧值。

  • 写穿透(Write-Through):CPU 写 Cache 时同步写内存,但 STM32H7 默认是写回(Write-Back),无法避免不一致。
  • 读分配(Read-Allocate):CPU 读内存时先加载到 Cache,DMA 更新内存后 Cache 不会自动失效。

传统解决方案是手动调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr(),但频繁调用影响性能且易遗漏。更优雅的方式是利用 MPU 将 DMA 缓冲区配置为 Non-Cacheable。

二、MPU 配置 Non-Cacheable 区域原理

MPU(Memory Protection Unit)允许将特定内存区域属性设置为 Non-Cacheable。当 CPU 访问该区域时,直接读写物理内存,不经过 D-Cache,从而天然保证与 DMA 的一致性。

STM32H7 的 MPU 支持 16 个区域,每个区域可独立配置基地址、大小、权限和内存属性。关键属性位:

  • TEX=0, C=0, B=0:Non-Cacheable,Normal memory。
  • S=1:共享属性,确保多主机(CPU 与 DMA)看到一致数据。

注意:MPU 区域必须按 32 字节对齐,且大小是 2 的幂(最小 32 字节)。

三、配置步骤

  1. 确定 DMA 缓冲区地址与大小:例如 uint8_t dma_buffer[256] __attribute__((aligned(32)));
  2. 禁用 MPU 并配置区域:调用 HAL_MPU_Disable(),然后填充 MPU_Region_InitTypeDef 结构体。
  3. 使能 MPU:调用 HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT)
  4. 确保缓冲区地址在配置区域内:通常放在 0x24000000(AXI SRAM)或 0x30000000(SRAM1-3)。

四、完整代码示例

以下代码将 AXI SRAM 的 0x24000000 起始的 64KB 区域配置为 Non-Cacheable,供 DMA 使用。

#include "stm32h7xx_hal.h"

// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[256];

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置区域 0:0x24000000 ~ 0x2400FFFF (64KB) 为 Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x24000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();

    // 将 dma_buffer 放置在 Non-Cacheable 区域
    // 方法:通过链接脚本或直接指定地址
    // 此处假设 dma_buffer 已被链接到 0x24000000 区域

    // 启动 DMA 传输
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dma_buffer, 256);

    while (1) {}
}

关键点dma_buffer 必须实际位于 0x24000000 区域。可通过修改链接脚本(.ld 文件)将 .dma_buffer 段定位到该地址,或使用 __attribute__((section(".dma_buffer"))) 并配置链接脚本。

五、注意事项

  • MPU 区域不能重叠:若多个区域覆盖同一地址,编号高的优先,但建议避免重叠。
  • Cache 维护操作:即使配置为 Non-Cacheable,若之前该区域被缓存过,仍需调用 SCB_InvalidateDCache_by_Addr() 清理旧数据。
  • 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区设为 Non-Cacheable,其他代码/数据仍走 Cache。
  • 对齐要求:缓冲区地址和大小必须严格对齐,否则 MPU 配置无效。
  • 多缓冲区管理:若使用多个 DMA 缓冲区,可合并到一个 MPU 区域,或分别配置。
  • 调试技巧:若数据仍异常,检查 MPU 配置是否生效(读取 MPU->RBAR 等寄存器),并确认链接脚本地址。

通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是从根源上解决 Cache 一致性问题的优雅方案。掌握这一技能,你的 STM32H7 应用将既拥有 Cache 的高性能,又具备 DMA 的可靠性。