STM32H7 的 D-Cache 与 DMA 一致性:从 Cache 维护到 MPU 配置的完整避坑指南

STM32H7 系列凭借 480MHz 主频和 Cortex-M7 内核,成为高性能嵌入式应用的首选。但许多开发者从 F1/F4 迁移到 H7 后,常遇到 DMA 传输数据错乱、外设通信失败等诡异问题。根源往往在于 D-Cache 与 DMA 的数据一致性。本文将带你彻底理解并解决这一难题。

一、为什么 D-Cache 会与 DMA 冲突?

Cortex-M7 的 D-Cache 位于内核与总线矩阵之间。当 CPU 访问内存时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存,不经过 Cache

  • 写操作:CPU 写数据到 Cache 后,若未及时写回内存,DMA 读取到的仍是旧数据。
  • 读操作:DMA 将新数据写入内存后,CPU 若从 Cache 读取,可能得到旧缓存值。

这种不一致性在以下场景尤为突出:

  • ADC/DAC 连续采样
  • UART/SPI 高速收发
  • 摄像头 DCMI 图像传输
  • SDMMC 读写

二、三种解决方案对比

| 方案 | 原理 | 适用场景 | 性能影响 | |------|------|----------|----------| | 手动维护 | 在 DMA 传输前后调用 Clean/Invalidate | 偶发传输、大数据块 | 中等 | | MPU Write-Through | 配置内存区域为透写模式 | 频繁小数据 | 较小 | | MPU Non-Cacheable | 完全禁用该区域 Cache | 实时性要求高 | 较大 |

三、方案一:手动 Cache 维护(最常用)

3.1 核心函数

CMSIS 提供了两个关键函数:

// 将 Cache 内容写回内存(CPU 写 -> DMA 读)
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);

// 丢弃 Cache 内容,强制从内存重新加载(DMA 写 -> CPU 读)
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

3.2 完整示例:UART DMA 接收

#include "stm32h7xx.h"

#define RX_BUFFER_SIZE  256

// 必须 32 字节对齐!
__attribute__((aligned(32))) uint8_t rx_buffer[RX_BUFFER_SIZE];

void uart_dma_init(void)
{
    // ... UART 和 DMA 初始化代码 ...
    
    // 启动 DMA 接收前,Invalidate 缓存
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, RX_BUFFER_SIZE);
    
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 处理数据前再次 Invalidate,确保读到最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, RX_BUFFER_SIZE);
        
        // 此时 rx_buffer 中的数据是有效的
        process_data(rx_buffer, RX_BUFFER_SIZE);
    }
}

3.3 发送场景

__attribute__((aligned(32))) uint8_t tx_buffer[TX_BUFFER_SIZE];

void uart_send_dma(uint8_t *data, uint16_t len)
{
    memcpy(tx_buffer, data, len);
    
    // 发送前 Clean,确保 DMA 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, TX_BUFFER_SIZE);
    
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

四、方案二:MPU 配置 Write-Through 模式

对于频繁小数据量传输,手动维护开销较大。可将 DMA 缓冲区所在内存区域配置为 Write-Through 模式,CPU 写操作同时更新 Cache 和内存。

void MPU_Config(void)
{
    HAL_MPU_Disable();
    
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    
    // 假设 DMA 缓冲区位于 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_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_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);
}

注意:Write-Through 仍可能因乱序执行导致问题,最保险的是 Non-Cacheable。

五、方案三:MPU 配置 Non-Cacheable 区域(最稳妥)

将 DMA 缓冲区完全排除在 Cache 之外,彻底避免一致性问题。

void MPU_Config_NonCacheable(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_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_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

六、避坑要点总结

  • 地址对齐:Cache 维护函数要求地址 32 字节对齐,缓冲区必须使用 __attribute__((aligned(32)))
  • 长度对齐SCB_InvalidateDCache_by_Addr 的长度参数应为 32 的倍数,否则可能误伤相邻数据。
  • 顺序不能错:发送前 Clean,接收后 Invalidate,顺序颠倒会导致数据丢失。
  • 避免 Cache 行伪共享:不同 DMA 缓冲区不要放在同一 32 字节 Cache 行内。
  • 中断中慎用:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
  • DMA 描述符:若使用链表模式,描述符本身也需对齐并维护 Cache。
  • 调试技巧:可临时关闭 D-Cache 验证问题是否由一致性引起。

七、结语

D-Cache 与 DMA 的一致性是 STM32H7 开发中的经典难题。理解其原理后,根据实际场景选择合适方案:偶发大数据用手动维护,频繁小数据用 MPU Write-Through,实时性要求极高用 Non-Cacheable。掌握这些技巧,你的 H7 项目将更加稳定可靠。