一、为什么 H7 的 Cache 会成为 DMA 的“隐形杀手”

STM32H7 基于 Cortex-M7,主频可达 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 L1 Cache(I-Cache 和 D-Cache)。D-Cache 默认是关闭的,但一旦开启,CPU 访问内存时会先查缓存,这就带来了经典问题:

  • CPU 写数据到内存:数据可能只停留在 D-Cache 中,并未真正写入 SRAM。此时启动 DMA 发送,DMA 从 SRAM 读到的还是旧数据。
  • DMA 写数据到内存:DMA 直接修改 SRAM,但 CPU 之前可能已经缓存了该区域,后续 CPU 读取时命中旧缓存,看不到 DMA 写入的新数据。

这就是 Cache 一致性(Coherency) 问题。STM32H7 没有硬件自动维护一致性的机制(如 ACP 端口),必须靠软件处理。

二、解决思路:MPU 配置 + Cache 维护

主要有两种手段,通常结合使用:

  1. MPU 配置:将 DMA 缓冲区所在的内存区域配置为 Non-CacheableWrite-Through,从根源上避免缓存不一致。
  2. Cache 维护操作:在 DMA 传输前后调用 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 手动同步。

对于频繁 DMA 传输的大块数据,推荐用 MPU 将缓冲区设为 Non-Cacheable,简单可靠;对于偶尔传输的小数据,可用 Cache 维护函数。

三、MPU 配置实战

3.1 确定内存区域

假设我们使用 D2 SRAM(地址 0x30000000)作为 DMA 缓冲区。先定义区域大小,例如 32KB。

3.2 配置 MPU

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 D2 SRAM 区域为 Non-Cacheable, Non-Shared
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    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_NOT_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);
}

注意:BaseAddressSize 必须对齐,32KB 区域要求基址 32KB 对齐。

3.3 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 必须在使能 Cache 之前配置
    SCB_EnableICache();
    SCB_EnableDCache();
    // ... 其他初始化
}

四、Cache 维护函数的使用

如果不想用 MPU,或者缓冲区是动态分配的,可以在 DMA 传输前后手动维护:

// 发送前:将 Cache 中的数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer, length);

// 接收前:无效化 Cache,确保 CPU 读取时从 SRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, length);

// 接收后:如果 CPU 要读数据,先无效化再读
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, length);

关键点length 必须是 32 字节的整数倍(Cache Line 大小),且地址要 32 字节对齐,否则可能误伤相邻数据。

五、完整示例:UART DMA 接收

#define RX_BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];

void UART_DMA_Init(void)
{
    // 假设已初始化 UART 和 DMA
    HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 无效化 Cache,确保读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, RX_BUFFER_SIZE);
        // 处理数据...
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
    }
}

如果 rxBuffer 已通过 MPU 配置为 Non-Cacheable,则无需调用 SCB_InvalidateDCache_by_Addr

六、常见失效现象与排查清单

  • 数据错乱:DMA 发送的数据部分正确、部分错误,通常是 Cache 未 Clean。
  • 偶发死机:DMA 写入时覆盖了 Cache 中的其他变量,导致指针异常。
  • 性能骤降:将整个 SRAM 都设为 Non-Cacheable,CPU 访问变慢。应只对 DMA 缓冲区区域做配置。
  • MPU 配置无效:忘记调用 HAL_MPU_Enable 或区域重叠导致优先级错误。

排查步骤

  1. 确认 D-Cache 是否开启(SCB->CCR 的 bit16)。
  2. 检查 DMA 缓冲区地址是否在 MPU 配置的 Non-Cacheable 区域内。
  3. 若使用 Cache 维护函数,检查地址和长度是否 32 字节对齐。
  4. 用调试器查看内存实际值,对比 CPU 读到的值。
  5. 临时关闭 D-Cache 验证问题是否消失。

七、注意事项

  • MPU 配置必须在使能 Cache 之前完成,否则可能不生效。
  • 多个 MPU 区域重叠时,编号小的优先级高,注意规划。
  • 使用 SCB_InvalidateDCache_by_Addr 前,确保该区域没有未写回的数据,否则会丢失。
  • DMA 描述符、控制结构体等也应放在 Non-Cacheable 区域或手动维护。
  • 对于以太网、USB 等高速外设,强烈建议用 MPU 将缓冲区设为 Non-Cacheable。

掌握这些,你就能在 STM32H7 上安心使用 Cache 和 DMA,既享受高性能,又避免数据一致性的坑。