一、为什么STM32H7的DMA会与Cache冲突?

STM32H7 的 Cortex-M7 内核包含 D-Cache(数据缓存),默认关闭。开启后,CPU 读写内存会先经过 Cache,而 DMA 直接访问物理内存(SRAM/SDRAM)。当两者访问同一块缓冲区时,会出现:

  • CPU 写数据到 Cache,但未写回内存 → DMA 读到旧数据。
  • DMA 写入新数据到内存,但 CPU 读的是 Cache 中的旧数据 → CPU 读到旧值。

这就是 Cache 一致性(Coherency)问题

二、MPU 配置:为 DMA 缓冲区设置正确的内存属性

STM32H7 的 MPU(Memory Protection Unit)可以按区域配置内存属性。对于 DMA 缓冲区,推荐两种策略:

  • Write-Through, no write allocate:CPU 写立即透传到内存,读可缓存。适合 DMA 读、CPU 写的场景。
  • Non-cacheable:完全绕过 Cache,最简单但性能有损失。

下面以将 0x30000000 开始的 64KB 区域配置为 Write-Through 为例。

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域0:DMA 缓冲区,地址 0x30000000,大小 64KB
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点

  • IsCacheable = MPU_ACCESS_CACHEABLEIsBufferable = MPU_ACCESS_NOT_BUFFERABLE 实现 Write-Through。
  • 若想完全 Non-cacheable,设置 IsCacheable = MPU_ACCESS_NOT_CACHEABLE 即可。

三、Cache 维护函数:何时用 Clean、Invalidate?

即使 MPU 配置为 Write-Through,仍建议在 DMA 传输前后调用维护函数,确保万无一失。CMSIS 提供以下函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回内存。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效。

使用时机

| 场景 | CPU 操作 | DMA 方向 | 维护操作 | |------|----------|----------|----------| | CPU 发数据给外设 | 写缓冲区 | 内存→外设 | 传输前 Clean | | 外设发数据给 CPU | 读缓冲区 | 外设→内存 | 传输后 Invalidate | | 双向 | 读写 | 双向 | 传输前 Clean,传输后 Invalidate |

四、完整实战代码:UART DMA 收发

以 UART 空闲中断 + DMA 接收为例,展示 Cache 维护。

#define DMA_BUF_SIZE  256
__attribute__((at(0x30000000))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

// 启动 DMA 接收前:无效化 Cache,确保 CPU 读到最新内存
void UART_DMA_StartReceive(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
}

// 空闲中断回调:处理接收数据
void UART_IdleCallback(void)
{
    uint32_t len = DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    // 无效化 Cache,保证读取到 DMA 写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, len);
    // 处理数据...
    process_data(dma_rx_buf, len);
}

// 发送数据前:Clean Cache,确保 DMA 读到内存中的新数据
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    memcpy(dma_tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

注意SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议按 32 字节向上取整。

五、注意事项与常见坑

  • 地址对齐:Cache 维护函数要求地址 32 字节对齐,否则触发 HardFault。
  • 缓冲区大小:若缓冲区跨 Cache 行(32 字节),Invalidate 可能丢弃相邻数据,建议缓冲区按 32 字节对齐且大小为 32 的倍数。
  • DMA 描述符:若使用链表模式,描述符本身也需放在 Non-cacheable 区域或手动维护。
  • 中断优先级:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
  • 调试技巧:若数据错乱,先关闭 D-Cache 验证是否为一致性问题。

六、总结

STM32H7 的 Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心思路:MPU 配置合理的内存属性 + 传输前后正确调用 Cache 维护函数。掌握本文的步骤,你就能在享受 H7 高性能的同时,避免数据错乱的坑。