一、为什么 D-Cache 会让 DMA 数据“错乱”?

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

典型场景:

  • CPU 写、DMA 读:CPU 写入缓冲区后立即启动 DMA 发送,但数据还在 Cache 中,DMA 读到的是旧值。
  • DMA 写、CPU 读:DMA 将外设数据写入 RAM,但 CPU 读取时命中 Cache 中的旧数据,看不到新内容。

根本矛盾:Cache 与 DMA 对内存的视图不一致。

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

2.1 MPU 配置:将 DMA 缓冲区设为 Non-Cacheable

最彻底的方法是利用 MPU 将 DMA 使用的内存区域配置为 Non-Cacheable(非缓存)。这样 CPU 和 DMA 都直接访问物理内存,无需任何维护操作。

优点:一劳永逸,代码简单。
缺点:该区域访问速度下降,适合对性能不敏感但要求稳定的场景。

配置步骤(以 STM32H7 HAL 库为例):

  1. 使能 MPU 时钟(在 HAL_MPU_Enable 前调用 __HAL_RCC_MPU_CLK_ENABLE())。
  2. 定义 MPU 区域,设置 BaseAddress、Size、Number。
  3. 属性设为 MPU_ACCESS_NOT_CACHEABLE、MPU_ACCESS_NOT_BUFFERABLE、MPU_ACCESS_SHAREABLE。
  4. 调用 HAL_MPU_ConfigRegion 并 HAL_MPU_Enable。
void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 假设 DMA 缓冲区起始地址 0x30000000,大小 32KB
    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_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);
}

2.2 动态维护:Clean 与 Invalidate

如果缓冲区必须使用 Cache(例如频繁 CPU 读写),则需在 DMA 传输前后手动维护 Cache:

  • Clean(清理):将 Cache 中的脏数据写回 RAM。用于 CPU 写 → DMA 读 之前。
  • Invalidate(无效化):将 Cache 中的对应行标记为无效,强制下次读取从 RAM 加载。用于 DMA 写 → CPU 读 之后。

关键原则:

  • 传输前:若 CPU 写过数据,先 Clean。
  • 传输后:若 DMA 写过数据,先 Invalidate 再读。
  • 注意:Invalidate 会丢弃 Cache 中未写回的数据,因此必须先 Clean 再 Invalidate 如果同一区域既有 CPU 写又有 DMA 写。

三、完整代码示例:UART DMA 发送与接收

以 UART 空闲中断 + DMA 接收为例,展示 Cache 维护的正确位置。

#include "stm32h7xx_hal.h"

#define RX_BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t txBuffer[RX_BUFFER_SIZE];

// 发送前:Clean D-Cache
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
    memcpy(txBuffer, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)txBuffer, len);
    HAL_UART_Transmit_DMA(&huart1, txBuffer, len);
}

// 接收完成回调(DMA 写入了 rxBuffer)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 先 Invalidate,确保 CPU 读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuffer, RX_BUFFER_SIZE);
        // 处理数据...
        process_data(rxBuffer, RX_BUFFER_SIZE);
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
    }
}

注意:SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的整数倍。使用 __attribute__((aligned(32))) 确保缓冲区对齐。

四、避坑要点与最佳实践

  • 对齐!对齐! Cache 操作以 32 字节为单位。若缓冲区未对齐,维护操作可能影响相邻数据,导致难以调试的随机错误。
  • 避免在中断中频繁 Clean/Invalidate:这些操作耗时,可能影响实时性。尽量用 MPU 配置非缓存区。
  • DMA 传输期间不要访问缓冲区:否则可能引发 Cache 与 DMA 的竞争。
  • 使用 SCB_InvalidateDCache_by_Addr 前先 Clean:如果该区域有 CPU 未写回的数据,Invalidate 会直接丢弃,造成数据丢失。
  • MPU 配置与链接脚本配合:将 DMA 缓冲区放在特定段(如 .dma_buffer),再对该段配置 MPU,避免手动计算地址。
  • 调试时关闭 D-Cache:若问题诡异,可临时关闭 D-Cache 验证是否为一致性问题。
  • STM32H7 的 AXI SRAM 与 DTCM:DTCM 不支持 DMA,且默认非缓存,适合放 DMA 缓冲区,但容量有限(128KB)。

五、总结

D-Cache 与 DMA 的一致性问题是 STM32H7 开发中的经典陷阱。核心解决方案有二:MPU 配置非缓存区(推荐用于固定缓冲区)和手动 Clean/Invalidate(适合动态场景)。无论哪种,都要牢记对齐、时序和竞争条件。掌握这些,你的 H7 项目将既快又稳。