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

STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存)。CPU 访问内存时,数据可能只被缓存在 Cache 中,并未写回物理内存(SRAM)。而 DMA 控制器直接访问物理内存,它看不到 Cache 里的最新数据。

典型冲突场景:

  • CPU 写,DMA 读:CPU 写数据到缓冲区,数据还在 Cache 中(Write-Back 策略),DMA 启动后从 SRAM 读到旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读时命中了 Cache 中的旧数据,读到过期值。

根本原因:Cache 对 CPU 透明,但对 DMA 不透明。

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

2.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable

通过 MPU(Memory Protection Unit)将特定内存区域设为 Non-Cacheable,CPU 和 DMA 都直接访问物理内存,从根本上避免一致性问题。

优点:一劳永逸,无需手动维护 Cache。 缺点:该区域 CPU 访问性能下降。

2.2 方案二:保持 Cacheable,手动 Clean/Invalidate

在 DMA 传输前后调用 Cache 维护函数:

  • Clean:将 Cache 中已修改的数据写回 SRAM(CPU 写 → DMA 读之前)。
  • Invalidate:丢弃 Cache 中的旧数据,强制从 SRAM 重新加载(DMA 写 → CPU 读之前)。

优点:CPU 访问性能高。 缺点:容易遗漏或时机错误,且 Invalidate 有副作用(见注意事项)。

三、MPU 配置实战(以 Non-Cacheable 为例)

假设 DMA 缓冲区位于 0x30000000(D1 SRAM),大小 32KB。

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 D1 SRAM 区域为 Non-Cacheable, Non-Shareable
    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);
}

注意:MPU 配置必须在使能 Cache 之前完成,且区域不能与已有配置重叠。

四、手动 Clean/Invalidate 完整示例

若选择方案二,以 UART DMA 接收为例:

#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 1. Invalidate:丢弃 Cache 旧数据,确保 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);

    // 2. 此时可安全处理 dma_rx_buf 中的数据
    process_data(dma_rx_buf, DMA_BUF_SIZE);

    // 3. 重新启动 DMA 接收
    HAL_UART_Receive_DMA(huart, dma_rx_buf, DMA_BUF_SIZE);
}

// DMA 发送前
void uart_dma_send(uint8_t *data, uint16_t len)
{
    // 先 Clean:确保 CPU 写入的数据真正到达 SRAM
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

五、关键注意事项与排查技巧

  • 地址必须 32 字节对齐:SCB_CleanDCache_by_Addr 和 Invalidate 要求地址按 Cache Line(32 字节)对齐,否则可能误伤相邻数据。使用 __attribute__((aligned(32)))。
  • Invalidate 会丢弃未写回的数据:如果 CPU 刚写过缓冲区,直接 Invalidate 会丢失这些修改。正确顺序是:先 Clean,再 Invalidate。
  • DMA 传输期间不要访问缓冲区:否则可能触发 Cache 与 DMA 的竞争。
  • 使用 SCB_InvalidateDCache_by_Addr 而非 SCB_InvalidateDCache:后者会清空整个 D-Cache,影响系统性能。
  • 调试时关闭 Cache 对比:若关闭 D-Cache 后问题消失,基本可确定是一致性问题。
  • 注意 DMA 突发长度与 Cache Line 的关系:若 DMA 传输长度不是 32 字节整数倍,Invalidate 可能影响缓冲区外的数据,建议缓冲区大小按 32 字节对齐。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑位。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若追求性能,则必须严格遵循 Clean/Invalidate 的时机与对齐要求。理解原理、善用工具,才能让 H7 的高性能真正为我所用。