一、为什么 D-Cache 会成为 DMA 的“猪队友”?

STM32H7 的 Cortex-M7 内核带有一级 D-Cache(数据缓存),默认写回(Write-Back)策略。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 RAM。而 DMA 控制器直接访问物理 RAM,它看不到 Cache 里的新数据。

  • CPU 写,DMA 读:CPU 写入的数据还在 Cache 里,DMA 从 RAM 读到旧数据。
  • DMA 写,CPU 读:DMA 把新数据写入 RAM,但 CPU 读到的却是 Cache 中的旧数据。

这就是典型的数据一致性问题。解决思路有两种:

  1. 将 DMA 缓冲区所在内存区域配置为非缓存(Non-Cacheable),通过 MPU 实现。
  2. 在 DMA 传输前后手动执行 Clean(写回)和 Invalidate(无效化)操作。

二、MPU 配置:一劳永逸的非缓存方案

使用 MPU 将特定内存区域(如 SRAM 中的 DMA 缓冲区)设为 Non-Cacheable,可避免手动维护缓存。

2.1 配置步骤

  1. 使能 MPU:SCB_EnableMPU() 前需先 SCB_EnableICache() 和 SCB_EnableDCache()。
  2. 定义 MPU 区域:设置基地址、大小、属性。
  3. 关键属性:TEX=0, C=0, B=0, S=1 表示 Non-Cacheable, Shareable。

2.2 代码示例

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR  0x30000000  // AXI SRAM 起始地址
#define DMA_BUFFER_SIZE  0x10000     // 64KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 AXI SRAM 前 64KB 为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
    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_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 区域大小必须是 2 的幂,且基地址对齐。若缓冲区跨区域,需拆分配置。

三、手动 Clean/Invalidate:灵活但易错

若不想牺牲 Cache 性能,可在 DMA 传输前后手动操作。

3.1 核心 API

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 数据写回 RAM。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 数据,强制从 RAM 重新加载。

3.2 正确使用顺序

CPU 写 → DMA 读(发送):

  1. CPU 填充缓冲区。
  2. SCB_CleanDCache_by_Addr() 写回。
  3. 启动 DMA 发送。

DMA 写 → CPU 读(接收):

  1. 启动 DMA 接收。
  2. DMA 完成中断中调用 SCB_InvalidateDCache_by_Addr()。
  3. CPU 读取缓冲区。

3.3 代码示例

#define BUF_SIZE  128
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];

void DMA_Send_Data(void)
{
    // 填充数据
    for (int i = 0; i < BUF_SIZE; i++) {
        dma_tx_buf[i] = i;
    }
    // Clean:确保数据写入 RAM
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);
    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, BUF_SIZE);
}

void DMA_Receive_Data(void)
{
    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // Invalidate:丢弃 Cache 旧数据,从 RAM 重新加载
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
    // 此时读取才是 DMA 写入的新数据
    for (int i = 0; i < BUF_SIZE; i++) {
        printf("%02X ", dma_rx_buf[i]);
    }
}

四、踩坑记录与注意事项

4.1 地址对齐与大小

  • SCB_CleanDCache_by_Addr 的地址必须 32 字节对齐,大小建议为 32 的倍数。否则可能遗漏部分 Cache 行。
  • 使用 __attribute__((aligned(32))) 修饰缓冲区。

4.2 Invalidate 的陷阱

  • Invalidate 会丢弃未写回的数据。若 CPU 刚修改了缓冲区,紧接着 Invalidate,修改将丢失。务必先 Clean 再 Invalidate。
  • 对于 DMA 接收,Invalidate 前确保 CPU 没有未写回的数据。

4.3 多缓冲区与 Cache 行伪共享

  • 若两个缓冲区共享同一个 Cache 行(32 字节),Invalidate 一个可能影响另一个。建议缓冲区按 32 字节对齐且大小对齐。

4.4 MPU 配置的优先级

  • MPU 区域可重叠,高编号区域优先级更高。确保 DMA 缓冲区区域不被其他缓存区域覆盖。

4.5 性能权衡

  • 非缓存区域会降低 CPU 访问速度。若缓冲区访问频繁,建议仅对 DMA 缓冲区使用非缓存,其他内存保持缓存。

五、总结

STM32H7 的 D-Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cache,则严格遵循 Clean/Invalidate 顺序,并注意对齐与伪共享问题。掌握这些技巧,你的 H7 项目将既高效又稳定。