STM32H7 的 DCache 与 DMA 一致性坑:Clean/Invalidate 时机与 MPU 配置实战

STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。但许多开发者从 F1/F4 迁移过来后,常被 DMA 传输数据错乱、HardFault 等问题折磨——根源往往在于 DCache(数据缓存)与 DMA 的一致性冲突。本文将带你彻底搞懂原理,并给出可落地的解决方案。

一、为什么会有缓存一致性问题?

Cortex-M7 的 DCache 位于 CPU 和总线矩阵之间,CPU 访问内存时,数据可能只存在于 Cache 中,并未写回物理内存(SRAM)。而 DMA 控制器直接访问物理内存,它看不到 Cache 的内容。

  • CPU 写 → DMA 读:CPU 写入的数据还在 Cache 里(Write-Back 模式),DMA 从 SRAM 读到的却是旧数据。
  • DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的却是 Cache 中的旧副本。

解决思路只有两个:让 CPU 写的数据真正落到内存(Clean),让 CPU 读之前丢弃 Cache 中的旧副本(Invalidate)。

二、Clean 与 Invalidate 的正确时机

2.1 发送方向(CPU → 外设,如 UART TX、SPI TX)

  1. CPU 填充发送缓冲区。
  2. 执行 Clean 操作:将 Cache 中已修改的数据写回 SRAM。
  3. 启动 DMA 传输。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, tx_len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, tx_len);

2.2 接收方向(外设 → CPU,如 UART RX、ADC)

  1. 执行 Invalidate 操作:丢弃 Cache 中的旧数据,确保 CPU 后续读取时从 SRAM 加载。
  2. 启动 DMA 接收。
  3. DMA 传输完成中断中,再次 Invalidate(因为 DMA 期间 CPU 可能预取了 Cache 行)。
// 接收前 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);
HAL_UART_Receive_DMA(&huart1, rx_buf, rx_len);

// DMA 完成回调中再次 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);
    // 处理数据...
}

关键点:Invalidate 必须在 DMA 启动前和完成后各做一次,且地址必须 32 字节对齐(Cache Line 大小)。

三、MPU 配置:为 DMA 缓冲区开“后门”

频繁手动 Clean/Invalidate 容易遗漏,更优雅的方式是用 MPU 将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable。

3.1 配置步骤

  1. 使能 MPU:HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
  2. 定义区域:基地址为 DMA 缓冲区地址,大小需为 2 的幂。
  3. 设置属性:
    • Non-Cacheable:完全绕过 Cache,最简单但性能略低。
    • Write-Through:写操作同时更新 Cache 和 SRAM,读仍可缓存。
void MPU_Config(void) {
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:Non-Cacheable
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    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);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.2 链接脚本配合

将 DMA 缓冲区放到 MPU 配置的地址段,例如 D2 SRAM(0x30000000):

__attribute__((section(".dma_buffer"))) uint8_t rx_buf[1024];

并在链接脚本中定义 .dma_buffer 段到 0x30000000。

四、完整实战:UART DMA 收发

#define RX_LEN 128
__attribute__((section(".dma_buffer"), aligned(32))) uint8_t rx_buf[RX_LEN];

void UART_DMA_Init(void) {
    MPU_Config(); // 先配置 MPU
    SCB_EnableDCache();
    SCB_EnableICache();
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 若 MPU 已配 Non-Cacheable,此处无需 Invalidate
    // 否则必须:SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_LEN);
    process_data(rx_buf, RX_LEN);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}

五、注意事项与避坑指南

  • 地址对齐:Clean/Invalidate 的地址必须 32 字节对齐,长度建议向上取整到 32 字节倍数,否则可能破坏相邻数据。
  • 不要对栈变量操作:DMA 缓冲区必须是全局或静态变量,且避免与其他变量共享 Cache Line。
  • Invalidate 前先 Clean:如果缓冲区既有 CPU 写入又有 DMA 读取,先 Clean 再 Invalidate,防止丢失未写回的数据。
  • MPU 区域重叠:确保 DMA 缓冲区区域不被其他 MPU 配置覆盖,优先级高的区域会生效。
  • 性能权衡:Non-Cacheable 会降低 CPU 访问速度,若数据量大,可考虑 Write-Through 或手动维护。
  • 调试技巧:若出现 HardFault,检查是否因 Invalidate 了包含栈或代码的 Cache 行。

掌握 DCache 与 DMA 的一致性处理,是 STM32H7 高阶开发的必修课。合理运用 MPU 配置,既能保证数据正确,又能兼顾性能。希望本文能帮你填平这个“坑”,让 H7 的性能真正为你所用。