一、为什么 H7 上 DMA 会“读到旧数据”?

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存),默认写回(Write-Back)、写分配(Write-Allocate)。CPU 写内存时,数据可能只停留在 Cache 里,并没有真正落到 SRAM;而 DMA 是直接访问 SRAM 的“物理内存”,它看不到 Cache。于是出现两种经典错误:

  • CPU 写、DMA 读:CPU 刚写入的缓冲区还在 Cache 中,DMA 启动后从 SRAM 读到旧值。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本。

解决思路只有两条:发送前 Clean(把 Cache 写回内存),接收后 Invalidate(让 Cache 失效,强制从内存重读)。

二、关键 API 与 Cache Line 概念

CMSIS 提供两个核心函数(需包含 core_cm7.h):

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
  • Clean:将指定地址范围的 Cache 行写回 SRAM。
  • Invalidate:将指定地址范围的 Cache 行标记为无效,下次读取从 SRAM 加载。

注意 Cache Line = 32 字节。操作地址必须 32 字节对齐,长度建议向上取整到 32 的倍数,否则可能误伤相邻数据。

三、配置步骤(以 ADC + DMA 为例)

  1. 在 SRAM 中定义 DMA 缓冲区,并做 32 字节对齐。
  2. 启动 DMA 前,若 CPU 写过该缓冲区,调用 SCB_CleanDCache_by_Addr。
  3. DMA 传输完成中断中,调用 SCB_InvalidateDCache_by_Addr 再读数据。
  4. 若使用 __attribute__((section)) 放到 DTCM,则无需 Cache 操作(DTCM 不经过 Cache)。

四、完整代码示例

#include "stm32h7xx_hal.h"
#include "core_cm7.h"

#define BUF_LEN   256
#define CACHE_LINE 32

/* 32 字节对齐的 DMA 缓冲区 */
aligned_32 uint16_t adc_buf[BUF_LEN];
volatile uint8_t dma_done = 0;

/* 向上取整到 Cache Line 的辅助宏 */
#define ALIGN_UP(x)  (((x) + CACHE_LINE - 1) & ~(CACHE_LINE - 1))

void adc_dma_start(void)
{
    /* 若 CPU 预填充过缓冲区,先 Clean 保证 SRAM 数据最新 */
    SCB_CleanDCache_by_Addr((uint32_t *)adc_buf, ALIGN_UP(sizeof(adc_buf)));

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, BUF_LEN);
}

/* DMA 传输完成回调 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /* 先让 Cache 失效,确保 CPU 读到 DMA 写入的新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, ALIGN_UP(sizeof(adc_buf)));

    dma_done = 1;
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_DMA_Init();
    MX_ADC1_Init();

    adc_dma_start();

    while (1) {
        if (dma_done) {
            dma_done = 0;
            uint32_t sum = 0;
            for (int i = 0; i < BUF_LEN; i++) {
                sum += adc_buf[i];   /* 此时数据已一致 */
            }
            /* 处理 sum ... */
        }
    }
}

五、踩坑记录与注意事项

  • 地址未对齐:SCB_CleanDCache_by_Addr 内部按 32 字节行操作,传入非对齐地址会触发断言或误清相邻变量。务必使用 aligned_32 或手动对齐。
  • 长度未取整:长度不是 32 的倍数时,最后一个 Cache Line 可能未被处理,导致部分数据不一致。用 ALIGN_UP 宏统一处理。
  • Clean 与 Invalidate 顺序:发送方向先 Clean 再启动 DMA;接收方向先等 DMA 完成再 Invalidate。顺序颠倒会丢数据。
  • Invalidate 前必须确保 DMA 已停止:若 DMA 仍在写,Invalidate 可能丢弃刚写入的数据。建议在传输完成中断中操作。
  • 多缓冲区/双缓冲:乒乓缓冲时,每个缓冲区都要独立做 Cache 维护,不能只处理首地址。
  • MPU 配置:可将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,从根源避免一致性问题,但会牺牲部分性能。
  • DTCM 陷阱:DTCM 不经过 D-Cache,放这里无需 Clean/Invalidate,但 DMA 能否访问取决于总线矩阵,需查参考手册。
  • 性能开销:频繁 Clean/Invalidate 会拖慢系统,建议缓冲区尽量大、操作次数尽量少。

六、小结

STM32H7 的 D-Cache 是性能利器,也是 DMA 数据一致性的“隐形杀手”。牢记 “CPU 写后 Clean,DMA 写后 Invalidate”,并严格保证地址与长度按 32 字节对齐,就能避开绝大多数数据错乱问题。若项目对实时性要求极高,可结合 MPU 将 DMA 区域设为 Non-Cacheable,用空间换稳定。