一、为什么 Cache 会与 DMA 打架?

STM32H7 的 Cortex-M7 内核带有 L1 数据缓存(D-Cache)和指令缓存(I-Cache)。当 CPU 访问外部 SDRAM 或内部 SRAM 时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存,不经过 Cache。

这就导致两种典型问题:

  • CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 中未写回内存,DMA 读到的却是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读到的却是 Cache 中的旧数据。

解决思路:在 DMA 传输前后,对相关内存区域执行 Cache 维护操作(Clean / Invalidate)。

二、Cache 维护操作的正确顺序

2.1 核心 API

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);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
  • Clean:将 Cache 中已修改的数据写回内存。
  • Invalidate:将 Cache 中的对应行标记为无效,下次读取时从内存重新加载。
  • CleanInvalidate:先写回再无效。

2.2 发送方向(CPU → DMA)

CPU 准备好数据后,DMA 要发送出去。

正确顺序

  1. CPU 写入数据到内存缓冲区。
  2. 执行 SCB_CleanDCache_by_Addr(),确保数据写回物理内存。
  3. 启动 DMA 传输。

错误做法:先启动 DMA,再 Clean —— 此时 DMA 可能已经读取了旧数据。

2.3 接收方向(DMA → CPU)

DMA 接收数据到内存,CPU 随后读取。

正确顺序

  1. 启动 DMA 接收。
  2. 等待 DMA 传输完成。
  3. 执行 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧数据。
  4. CPU 读取缓冲区。

错误做法:先 Invalidate,再启动 DMA —— 若 CPU 在 DMA 传输期间访问了该缓冲区,Cache 会再次填充旧数据。

2.4 双向传输

若同一缓冲区既发送又接收,推荐使用 SCB_CleanInvalidateDCache_by_Addr(),但需注意操作时机。

三、失效场景复现:DMA 接收数据错乱

3.1 场景描述

使用 DMA 将 UART 接收的数据搬运到 SDRAM 缓冲区,CPU 轮询读取。未做 Cache 维护时,CPU 读到的数据可能始终是第一次的旧值。

3.2 复现代码(错误示范)

#define BUF_SIZE 32
__attribute__((section(".sdram"))) uint8_t rx_buf[BUF_SIZE];

void uart_dma_init(void) {
    // 配置 UART 和 DMA,目标地址为 rx_buf
    // ...
}

void process_data(void) {
    // 等待 DMA 完成
    while (!dma_transfer_complete);
    // 直接读取,未 Invalidate Cache
    for (int i = 0; i < BUF_SIZE; i++) {
        printf("%02X ", rx_buf[i]);
    }
}

现象:第一次接收后,后续每次打印都是相同的数据,即使 UART 发送了新内容。

3.3 修正代码

void process_data(void) {
    while (!dma_transfer_complete);
    // 关键:无效化 Cache,强制从内存重新加载
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    for (int i = 0; i < BUF_SIZE; i++) {
        printf("%02X ", rx_buf[i]);
    }
}

四、完整示例:UART DMA 接收 + Cache 维护

#include "stm32h7xx.h"

#define RX_BUF_SIZE 64
__attribute__((section(".sdram"))) __attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t dma_done = 0;

void uart_dma_config(void) {
    // 使能时钟、配置 GPIO、UART、DMA
    // 设置 DMA 目标地址为 rx_buf,传输长度 RX_BUF_SIZE
    // 使能 DMA 传输完成中断
}

void DMA1_Stream0_IRQHandler(void) {
    if (DMA1->LISR & DMA_LISR_TCIF0) {
        DMA1->LIFCR = DMA_LIFCR_CTCIF0;
        dma_done = 1;
    }
}

void read_uart_data(void) {
    if (dma_done) {
        // 1. 无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
        // 2. 处理数据
        for (int i = 0; i < RX_BUF_SIZE; i++) {
            // 使用 rx_buf[i]
        }
        dma_done = 0;
        // 3. 重新启动 DMA 接收
        // ...
    }
}

五、注意事项与避坑指南

  • 地址对齐:Cache 操作要求地址 32 字节对齐,长度也建议按 32 字节对齐,否则可能影响相邻数据。
  • 缓冲区大小:若缓冲区不是 Cache 行大小的整数倍,Invalidate 可能丢弃相邻变量,建议使用 __attribute__((aligned(32))) 并填充到 32 字节倍数。
  • 中断与并发:在 DMA 传输期间,避免 CPU 访问同一缓冲区,否则 Cache 可能被意外填充。
  • MPU 配置:可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,一劳永逸,但会牺牲性能。
  • 性能权衡:频繁的 Cache 维护会降低性能,合理划分 Cache 区域与 DMA 区域。
  • 调试技巧:若怀疑 Cache 问题,可暂时关闭 D-Cache 验证,但正式产品不建议关闭。

六、总结

STM32H7 的 L1 Cache 与 DMA 一致性是嵌入式开发中的经典难题。牢记两条黄金法则:

  • CPU 写、DMA 读 → 先 Clean,再启动 DMA
  • DMA 写、CPU 读 → 先等 DMA 完成,再 Invalidate

配合正确的对齐与 MPU 配置,即可在享受 Cache 高性能的同时,保证数据传输的可靠性。