STM32H7 的 D-Cache 与 DMA 数据一致性:按 Cache Line 对齐缓冲区的实战配置与踩坑记录

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

STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到 SRAM。而 DMA 控制器是独立于 CPU 的总线主设备,它直接访问物理 SRAM,完全绕过 Cache。

这就导致两个经典问题:

  • DMA 写入 SRAM 后,CPU 读到的却是 Cache 中的旧数据(因为 Cache 未更新)。
  • CPU 修改了 Cache 中的数据但未写回 SRAM,DMA 却从 SRAM 读到了旧数据。

更隐蔽的是,D-Cache 的操作以 Cache Line 为单位(Cortex-M7 为 32 字节)。即使你只修改了 1 个字节,Clean 操作也会把整个 32 字节写回 SRAM;Invalidate 则会丢弃整个 Line 的缓存内容。如果缓冲区没有按 32 字节对齐,一次 Invalidate 可能误伤相邻变量,导致程序跑飞。

二、核心原则:按 Cache Line 对齐 + 正确维护

解决思路分三步:

  1. 将 DMA 缓冲区强制对齐到 32 字节边界,并确保其大小是 32 字节的整数倍。
  2. 配置 MPU,将 DMA 缓冲区所在区域设为 Write-Through 或 Non-Cacheable,从根源避免一致性问题(推荐)。
  3. 若必须使用 Cacheable 内存,则在 DMA 传输前后正确执行 Clean 和 Invalidate 操作。

三、实战配置步骤

3.1 定义对齐的 DMA 缓冲区

使用 __attribute__((aligned(32))) 强制对齐,并用 __IO 防止编译器优化。

#include "stm32h7xx.h"

#define DMA_BUF_SIZE  256

// 接收缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// 发送缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];

3.2 配置 MPU 将缓冲区设为 Non-Cacheable

这是最省心的方案。以 STM32H743 为例,将 SRAM 中某块区域(如 0x30000000 起始的 64KB)配置为 Non-Cacheable。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区专用,Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;  // D2 SRAM 起始
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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);
}

将 DMA 缓冲区链接到该区域(修改链接脚本),或直接使用该地址定义指针。

3.3 若必须使用 Cacheable 内存:Clean 与 Invalidate

对于 CPU 写、DMA 读(发送):在启动 DMA 前 Clean 缓冲区,确保数据写入 SRAM。

// 发送前:将 Cache 数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, DMA_BUF_SIZE);

对于 DMA 写、CPU 读(接收):在 DMA 完成中断中 Invalidate 缓冲区,丢弃 Cache 旧数据,强制 CPU 从 SRAM 重新读取。

void DMA_RxComplete_Callback(void)
{
    // 接收完成:无效化 Cache,确保 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
    // 此时再处理 dma_rx_buf 中的数据
    ProcessData(dma_rx_buf, DMA_BUF_SIZE);
}

注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的倍数,否则会误伤相邻数据。

四、踩坑记录:一个字节引发的血案

坑 1:缓冲区未对齐导致 HardFault

早期项目定义 uint8_t rx_buf[100];,未加对齐属性。调用 SCB_InvalidateDCache_by_Addr 时,函数内部会向下对齐到 32 字节边界,导致 rx_buf 前面的变量被无效化。若该变量恰好是栈上的指针,后续解引用直接 HardFault。解决:所有 DMA 缓冲区必须 aligned(32),且长度设为 32 的倍数。

坑 2:DMA 传输中 CPU 访问缓冲区

在 DMA 传输进行中,CPU 若读取接收缓冲区,可能触发 Cache 预取,将旧数据填入 Cache。等 DMA 完成后再 Invalidate 虽然能解决,但若在传输中读取并使用了数据,就会得到错误值。解决:DMA 传输期间禁止 CPU 访问该缓冲区,或使用双缓冲区交替。

坑 3:忘记 Clean 导致发送数据错乱

使用 HAL_UART_Transmit_DMA 发送一个结构体,CPU 刚填充完数据,Cache 中还是脏的,DMA 直接从 SRAM 读到了旧内容。解决:发送前务必 SCB_CleanDCache_by_Addr。

坑 4:MPU 配置与链接脚本冲突

将 MPU 区域设为 Non-Cacheable 后,却忘记把缓冲区放到该地址,结果配置无效。解决:使用 __attribute__((section(".dma_buffer"))) 配合链接脚本,或直接定义指针指向 MPU 区域。

五、总结与最佳实践

  • 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
  • 若使用 Cacheable 内存,缓冲区必须 32 字节对齐,长度取 32 的倍数。
  • 发送前 Clean,接收后 Invalidate,顺序不能错。
  • 避免在 DMA 传输期间 CPU 访问同一缓冲区。
  • 使用 SCB_InvalidateDCache_by_Addr 时,地址和长度都要对齐,否则误伤无辜。

掌握这些要点,STM32H7 的 D-Cache 与 DMA 就能和谐共处,让你的高性能应用稳定驰骋。