STM32H7 的 D-Cache 与 DMA 一致性排查:从 Cache Line 对齐到 MPU 配置的完整避坑指南

一、问题现象与根源

在 STM32H7 上使用 DMA 搬运数据时,常遇到以下现象:

  • DMA 发送的数据与内存中实际数据不符;
  • DMA 接收的数据看似正确,但 CPU 读取时部分字节为旧值;
  • 数据偶尔正确,偶尔错误,与编译器优化等级、变量地址相关。

根源在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问同一块物理内存时,缺乏硬件一致性机制。DMA 直接读写物理内存,而 CPU 可能读写的是 Cache 中的副本。若 Cache 中的数据未及时写回(Clean)或失效(Invalidate),就会产生数据不一致。

二、Cache Line 对齐:一切的基础

Cortex-M7 的 D-Cache 行大小为 32 字节。DMA 传输缓冲区必须按 32 字节对齐,且长度最好是 32 的整数倍。否则,Clean/Invalidate 操作会波及相邻数据,导致意外丢失。

  • 对齐声明:使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。
  • 长度处理:若长度非 32 整数倍,需手动补齐或使用 MPU 将缓冲区配置为 Write-Through 模式。
// 定义 32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

三、MPU 配置:划定非缓存区域

最稳妥的方案是将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,避免 Cache 介入。STM32H7 的 MPU 支持 16 个区域,可针对特定地址范围设置属性。

配置步骤

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 禁用 MPU:MPU->CTRL = 0;
  3. 配置区域基址与属性:
    • 基址:MPU->RBAR = (uint32_t)buf & MPU_RBAR_ADDR_Msk;
    • 属性:MPU->RASR = (0x3 << MPU_RASR_AP_Pos) | (1 << MPU_RASR_XN_Pos) | (0x1 << MPU_RASR_TEX_Pos) | ...
  4. 使能 MPU 与 D-Cache:MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
void MPU_Config(void) {
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buf;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_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);
}

四、手动 Clean/Invalidate 操作

若无法使用 MPU,则必须在 DMA 传输前后手动维护 Cache:

  • 发送前:对发送缓冲区执行 Clean,将 Cache 数据写回内存。
  • 接收后:对接收缓冲区执行 Invalidate,丢弃 Cache 中的旧数据。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf, (uint32_t)&periph, DMA_BUF_SIZE);

// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);

注意:Invalidate 操作会丢弃未写回的数据,务必确保 CPU 不再需要这些数据。

五、完整代码示例:DMA 串口接收

#include "stm32h7xx_hal.h"

#define RX_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];

void UART_DMA_Init(void) {
    // 1. MPU 配置(将 rx_buf 区域设为 Non-Cacheable)
    MPU_Config();
    // 2. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 若未使用 MPU,需在此处 Invalidate
    // SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
    // 处理数据...
}

六、注意事项与避坑总结

  • 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,长度最好为 32 的倍数。
  • MPU 优先:能配置 MPU 就配置,一劳永逸,避免手动维护 Cache。
  • Clean 与 Invalidate 顺序:发送前 Clean,接收后 Invalidate,不可颠倒。
  • 避免跨区域:MPU 区域不要重叠,否则行为未定义。
  • 调试技巧:若数据错乱,先检查缓冲区地址是否对齐,再确认 MPU 配置是否生效。
  • 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,仅对 DMA 缓冲区使用。

通过以上步骤,可彻底解决 STM32H7 上 D-Cache 与 DMA 的一致性问题,让高性能与数据正确性兼得。