一、为什么 D-Cache 和 DMA 会打架?

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了匹配 CPU 的高速运算,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会优先从 Cache 中读取,写数据时也可能只写入 Cache 而不立即同步到 SRAM。

而 DMA(直接存储器访问)是独立于 CPU 的外设,它直接读写物理内存(SRAM),完全不知道 Cache 的存在。这就导致了经典的一致性问题:

  • CPU 写,DMA 读:CPU 把数据写入 Cache,但尚未写回 SRAM,DMA 从 SRAM 读到的就是旧数据。
  • DMA 写,CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据,看不到更新。

解决思路有两种:禁用 D-Cache(性能损失大,不推荐)或使用 MPU 配置内存属性 + 手动维护 Cache 一致性

二、MPU 配置:给 DMA 缓冲区划一块“安全区”

MPU(内存保护单元)可以按地址区域设置内存属性。对于 DMA 缓冲区,我们通常将其配置为 Non-Cacheable(非缓存)Write-Through(写透),从而避免一致性问题。

2.1 配置步骤

  1. 使能 MPU:调用 ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk)
  2. 定义区域结构体 ARM_MPU_Region_t,设置基地址、大小、属性。
  3. 调用 ARM_MPU_SetRegion()ARM_MPU_Enable()

2.2 关键属性选择

  • Non-Cacheable:读写都不经过 Cache,DMA 和 CPU 直接访问 SRAM,最简单安全,但 CPU 访问速度略降。
  • Write-Through:写操作同时更新 Cache 和 SRAM,读操作可缓存。适合 DMA 只读或只写的场景,但仍有风险。
  • Write-Back:默认属性,性能最好,但必须配合 clean/invalidate 操作。

推荐:对 DMA 缓冲区使用 Non-Cacheable 属性,一劳永逸。

2.3 代码示例:MPU 配置 DMA 缓冲区

#include "stm32h7xx.h"

// 假设 DMA 缓冲区位于 D1 SRAM,地址 0x24000000,大小 32KB
#define DMA_BUFFER_ADDR  0x24000000
#define DMA_BUFFER_SIZE  0x8000  // 32KB

void MPU_Config(void)
{
    ARM_MPU_Region_t mpu_region;

    // 禁用 MPU
    ARM_MPU_Disable();

    // 配置区域:基地址 DMA_BUFFER_ADDR,大小 32KB,属性 Non-Cacheable
    mpu_region.RBAR = ARM_MPU_RBAR(DMA_BUFFER_ADDR, ARM_MPU_SH_NON, 0, 1, 0);
    mpu_region.RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 0, 0, 0, 0, 0, ARM_MPU_REGION_SIZE_32KB);

    ARM_MPU_SetRegion(0, &mpu_region);

    // 使能 MPU,背景区域使用默认内存映射
    ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
}

注意:ARM_MPU_RBARARM_MPU_RASR 是 CMSIS 提供的宏,具体参数请参考 CMSIS 头文件。

三、手动维护 Cache:clean 和 invalidate

如果 DMA 缓冲区必须使用 Cacheable 属性(例如为了极致性能),则必须在 DMA 传输前后手动维护 Cache。

3.1 两个关键操作

  • Clean:将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 的场景。
  • Invalidate:将 Cache 中对应地址的数据标记为无效,下次读取时从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景。

3.2 正确调用顺序

发送数据(CPU→DMA)

  1. CPU 填充缓冲区。
  2. 执行 SCB_CleanDCache_by_Addr()
  3. 启动 DMA 发送。

接收数据(DMA→CPU)

  1. 启动 DMA 接收。
  2. 等待 DMA 完成。
  3. 执行 SCB_InvalidateDCache_by_Addr()
  4. CPU 读取缓冲区。

3.3 代码示例:DMA 发送与接收

#include "stm32h7xx.h"

#define BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];

// DMA 发送
void DMA_Send(uint8_t *data, uint32_t len)
{
    memcpy(tx_buffer, data, len);
    // 清理 D-Cache,确保数据写入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len);
    // 启动 DMA 发送(伪代码)
    HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->TDR, len);
}

// DMA 接收完成回调
void DMA_ReceiveComplete(uint32_t len)
{
    // 无效化 D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, len);
    // 此时 CPU 读取 rx_buffer 才能得到 DMA 写入的新数据
    ProcessData(rx_buffer, len);
}

四、实战避坑指南

  • 地址对齐SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,长度最好是 32 的倍数。否则可能误伤相邻数据。
  • 不要过度 invalidate:invalidate 会丢弃 Cache 中未写回的数据,如果 CPU 刚写过该区域,必须先 clean 再 invalidate。
  • DMA 描述符也要注意:如果使用链表式 DMA,描述符本身也可能被缓存,需同样处理。
  • 中断中慎用:Cache 维护操作耗时较长,避免在高速中断中频繁调用。
  • 多缓冲区场景:使用乒乓缓冲区时,每个缓冲区都要独立维护 Cache。
  • 调试时关闭 Cache:在调试 DMA 问题时,可临时关闭 D-Cache 验证是否为一致性问题。

五、总结

STM32H7 的 D-Cache 与 DMA 一致性是嵌入式开发中的经典难题。最稳妥的方案是用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,简单可靠。若追求性能而使用 Cacheable 内存,则必须严格遵循 clean/invalidate 的调用时机和对齐要求。理解原理、善用工具,才能让 H7 的性能与稳定性兼得。