一、为什么 DMA 遇上 D-Cache 会出错?

STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了匹配高速内核与相对较慢的存储器,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将数据从 SRAM 加载到 Cache 中,后续读写直接操作 Cache,从而大幅提升性能。

但 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM),不经过 D-Cache。这就导致了经典的一致性矛盾:

  • CPU 写,DMA 读:CPU 写入的数据可能还停留在 Cache 中(Write-Back 模式),未同步到 SRAM,DMA 读到的是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着该地址的旧数据,CPU 读到的仍是旧值。

解决这一问题的核心手段有两个:MPU 配置内存属性手动维护 Cache 一致性(Clean/Invalidate)

二、MPU 配置:为 DMA 缓冲区划定“安全区”

MPU(Memory Protection Unit)允许我们将特定内存区域配置为不同的访问属性。对于 DMA 缓冲区,通常有两种策略:

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

这是最简单粗暴的方法。将该区域标记为不可缓存,CPU 和 DMA 都直接访问 SRAM,从根本上避免一致性问题。缺点是 CPU 访问速度会下降。

// MPU 配置示例:将 0x30000000 开始的 32KB 区域设为 Non-Cacheable
void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    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_NOT_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);
}

2.2 方案二:Write-Through 模式

将区域配置为 Write-Through(透写),CPU 写操作会同时更新 Cache 和 SRAM。这样 DMA 读时能拿到最新数据,但 DMA 写时 CPU Cache 仍可能有过时数据,需要配合 Invalidate 操作。

MPU_InitStruct.IsCacheable  = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // Write-Through

三、Clean 与 Invalidate 的精确边界

当无法使用 Non-Cacheable 区域时,必须在 DMA 传输前后手动维护 Cache。关键是要理解 CleanInvalidate 的语义:

  • Clean:将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 场景。
  • Invalidate:丢弃 Cache 中的数据,强制下次读取从 SRAM 加载。用于 DMA 写 → CPU 读 场景。

3.1 发送数据(CPU → DMA)

// 准备发送数据
memcpy(tx_buffer, source, length);

// 关键:Clean 操作,确保数据写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, length);

// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, length);

3.2 接收数据(DMA → CPU)

// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, length);

// 在 DMA 完成中断中:
void DMA_Complete_Callback(void)
{
    // 关键:Invalidate 操作,丢弃 Cache 中的旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, length);

    // 现在 CPU 可以安全读取新数据
    process_data(rx_buffer, length);
}

3.3 地址对齐要求

Cortex-M7 的 Cache 操作要求地址按 32 字节对齐。如果缓冲区未对齐,SCB_CleanDCache_by_Addr 等函数可能无法正确工作。建议使用 __attribute__((aligned(32))) 声明缓冲区:

__attribute__((aligned(32))) uint8_t tx_buffer[256];
__attribute__((aligned(32))) uint8_t rx_buffer[256];

四、完整实战:UART DMA 收发

以下代码展示了在 STM32H7 上使用 UART DMA 收发时,如何正确维护 Cache 一致性。

#include "stm32h7xx_hal.h"

#define BUFFER_SIZE 128

__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

// 发送函数
void UART_Send_Data(uint8_t *data, uint16_t len)
{
    if (len > BUFFER_SIZE) return;

    memcpy(tx_buffer, data, len);

    // Clean D-Cache,确保数据写入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len);

    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // Invalidate D-Cache,丢弃旧数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);

        // 处理接收到的数据
        Process_Received_Data(rx_buffer, BUFFER_SIZE);

        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
    }
}

// 初始化 MPU(将 DMA 缓冲区所在区域设为 Non-Cacheable 或 Write-Through)
void System_MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 假设缓冲区位于 0x30000000 (SRAM1)
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    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; // Non-Cacheable
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_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);
}

五、注意事项与避坑指南

  • 地址对齐:所有 Cache 维护函数的地址必须 32 字节对齐,长度建议为 32 的整数倍。
  • 避免过度 Invalidate:Invalidate 会丢弃 Cache 中所有未写回的数据,如果该区域包含 CPU 刚写入但未 Clean 的数据,会导致数据丢失。
  • DMA 描述符对齐:如果使用链表式 DMA,描述符本身也需注意对齐和 Cache 问题。
  • 中断中调用SCB_CleanDCache_by_Addr 等函数执行时间较长,在高速中断中频繁调用可能影响实时性,建议合理规划缓冲区大小。
  • 多缓冲区策略:使用双缓冲(Ping-Pong)时,确保每个缓冲区都正确维护 Cache。
  • 调试技巧:如果数据异常,可先关闭 D-Cache 验证是否为一致性问题,再逐步开启并添加维护操作。

六、总结

STM32H7 的 D-Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。核心解决思路是:要么让 DMA 缓冲区不可缓存(MPU 配置),要么在正确的时间点执行 Clean/Invalidate。理解 Clean 和 Invalidate 的语义边界,结合 32 字节对齐要求,就能在享受 D-Cache 高性能的同时,确保 DMA 数据传输的绝对可靠。