STM32H7 的 DCache 与 DMA 一致性:MPU 配置与 clean/invalidate 的正确姿势

一、为什么 DCache 会让 DMA 数据错乱?

STM32H7 基于 Cortex-M7 内核,带有 16KB I-Cache 和 16KB D-Cache。开启 D-Cache 后,CPU 读写的是 Cache 而非直接访问 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存。两者若不同步,就会出现:

  • CPU 写数据 → DMA 发送:数据可能还在 Cache 中未写回 SRAM,DMA 读到旧数据。
  • DMA 接收数据 → CPU 读:DMA 已把新数据写入 SRAM,但 CPU 读到的是 Cache 中的旧数据。

这就是经典的 Cache 一致性问题。解决手段有两类:MPU 配置为 Write-Through/Non-Cacheable,或手动 clean/invalidate。

二、Cache 操作的本质

Cortex-M7 提供 CMSIS 函数:

  • SCB_CleanDCache_by_Addr(addr, size):把 Cache 中已修改的数据写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 重新加载。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。

关键原则:

  • CPU 写、DMA 读(发送)→ 在启动 DMA 前 clean。
  • DMA 写、CPU 读(接收)→ 在 DMA 完成后 invalidate。
  • 地址必须 32 字节对齐,长度向上取整到 32 字节,否则会误伤相邻数据。

三、MPU 配置:更优雅的方案

对于频繁 DMA 的缓冲区,推荐用 MPU 将其配置为 Non-Cacheable 或 Write-Through,从根源避免一致性问题。

#include "stm32h7xx_hal.h"

/* 将 DMA 缓冲区所在区域配置为 Non-Cacheable */
void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_Init = {0};
    MPU_Init.Enable           = MPU_REGION_ENABLE;
    MPU_Init.Number           = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress      = 0x30000000;   /* D2 SRAM 起始 */
    MPU_Init.Size             = MPU_REGION_SIZE_64KB;
    MPU_Init.SubRegionDisable = 0x00;
    MPU_Init.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_Init.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_Init.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_Init.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;  /* 关键 */
    MPU_Init.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_Init);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 配置必须在 SCB_EnableDCache() 之前完成,否则需先关闭 Cache。

四、手动 clean/invalidate 完整示例

若缓冲区无法用 MPU 隔离,则必须手动维护。以下以 UART DMA 发送/接收为例。

#define BUF_SIZE  256
/* 必须 32 字节对齐 */
alignas(32) uint8_t tx_buf[BUF_SIZE];
alignas(32) uint8_t rx_buf[BUF_SIZE];

/* 发送:CPU 填数据 → clean → 启动 DMA */
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

/* 接收完成回调:invalidate → 再读数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    /* 此时 rx_buf 中才是 DMA 写入的最新数据 */
    ProcessData(rx_buf, BUF_SIZE);
}

顺序不能颠倒:

  • 发送:先 clean,后启动 DMA。
  • 接收:先等 DMA 完成,后 invalidate,再访问数据。

五、常见坑与注意事项

  • 对齐问题:SCB_*DCache_by_Addr 要求地址 32 字节对齐,长度按 32 字节向上取整。建议用 alignas(32) 定义缓冲区。
  • invalidate 前不要读缓冲区:若 CPU 先读,会把旧数据重新载入 Cache,invalidate 后仍需再读一次才正确。
  • DMA 描述符/链表:若使用链式 DMA,描述符本身也需 clean。
  • 多缓冲区场景:每个缓冲区独立 clean/invalidate,不要跨区域操作。
  • 性能权衡:Non-Cacheable 区域访问变慢,适合大块 DMA;小数据可手动 clean/invalidate。
  • 调试陷阱:调试器读内存可能触发 Cache 行为,导致问题“消失”,建议用串口打印验证。

六、总结

STM32H7 的 DCache 是性能利器,但必须与 DMA 协同。牢记三句话:发送前 clean,接收后 invalidate,地址 32 字节对齐。对于高频 DMA 缓冲区,优先用 MPU 配置为 Non-Cacheable,从架构上消除隐患。掌握这些姿势,你的 H7 项目将既快又稳。