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

STM32H7 的 Cortex-M7 内核带 16KB D-Cache 和 16KB I-Cache。D-Cache 作为 CPU 与 SRAM 之间的高速缓冲,会缓存 CPU 读写过的数据。而 DMA 是独立于 CPU 的总线主设备,它直接访问 SRAM,不经过 Cache。

于是出现两种典型问题:

  • CPU 写、DMA 读:CPU 把数据写入 Cache,尚未回写到 SRAM,DMA 启动后从 SRAM 读到的是旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧副本。

解决思路就是按需执行 Clean(清理,把 Cache 写回 SRAM) 和 Invalidate(无效化,丢弃 Cache 内容,强制从 SRAM 重读)。

二、Clean 与 Invalidate 的时机

记住一条原则:谁写谁负责同步。

  • CPU 写缓冲区 → DMA 读(如 UART 发送、DAC 输出):DMA 启动前对缓冲区做 Clean。
  • DMA 写缓冲区 → CPU 读(如 ADC 采集、以太网接收):DMA 完成后对缓冲区做 Invalidate。
  • 双向传输(如以太网描述符):先 Clean 再 Invalidate,或使用 SCB_CleanInvalidateDCache_by_Addr()。

注意:Invalidate 会丢弃 Cache 中未回写的数据,若 CPU 曾写过该区域且未 Clean,数据会丢失。因此 Invalidate 前必须确保没有脏数据。

三、配置步骤

  1. 使能 D-Cache:在 main() 最开始调用 SCB_EnableDCache(),且必须在使能前初始化 MPU(若使用)。
  2. 配置 MPU(可选但推荐):将 DMA 缓冲区所在 SRAM 区域设为 Non-Cacheable,可彻底避免一致性问题,但会牺牲性能。
  3. 地址与长度对齐:Cache 操作以 32 字节 Cache Line 为单位,地址必须 32 字节对齐,长度建议向上取整到 32 字节倍数。
  4. 在 DMA 前后插入维护函数:发送前 Clean,接收后 Invalidate。

四、完整代码示例

以 ADC + DMA 采集为例,缓冲区定义与维护函数如下:

#include "stm32h7xx.h"

#define ADC_BUF_LEN  256
/* 必须 32 字节对齐,否则 Cache 维护会误伤相邻数据 */
__attribute__((aligned(32))) uint16_t adc_buf[ADC_BUF_LEN];

/* CPU 写、DMA 读:发送前 Clean */
void dma_tx_prepare(void *buf, uint32_t len)
{
    uint32_t addr = (uint32_t)buf;
    uint32_t size = (len + 31U) & ~31U;   /* 向上取整到 32 字节 */
    SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
}

/* DMA 写、CPU 读:接收后 Invalidate */
void dma_rx_complete(void *buf, uint32_t len)
{
    uint32_t addr = (uint32_t)buf;
    uint32_t size = (len + 31U) & ~31U;
    SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
}

int main(void)
{
    SCB_EnableICache();
    SCB_EnableDCache();          /* 使能 D-Cache */

    adc_dma_init(adc_buf, ADC_BUF_LEN);

    while (1)
    {
        /* 等待 DMA 传输完成标志 */
        if (dma_done_flag)
        {
            dma_done_flag = 0;
            /* 先 Invalidate,再读取缓冲区 */
            dma_rx_complete(adc_buf, sizeof(adc_buf));
            process_adc_data(adc_buf, ADC_BUF_LEN);
        }
    }
}

若使用以太网发送,则发送前对描述符和数据缓冲区执行 Clean:

void eth_send_frame(uint8_t *frame, uint32_t len)
{
    memcpy(tx_buf, frame, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, (len + 31U) & ~31U);
    ETH_TransmitFrame(len);
}

五、踩坑复盘

  • 坑 1:缓冲区未对齐。uint8_t buf[100] 默认 4 字节对齐,Clean 时按 32 字节操作会波及相邻变量,导致莫名数据被改写。务必用 __attribute__((aligned(32)))。
  • 坑 2:Invalidate 前未 Clean。CPU 刚写过接收缓冲区又立即 Invalidate,未回写的脏数据被丢弃。正确做法是先 Clean 再 Invalidate,或避免 CPU 提前写。
  • 坑 3:DMA 传输中操作 Cache。DMA 正在读写时执行 Clean/Invalidate 会破坏传输。必须在 DMA 完成中断或传输前后操作。
  • 坑 4:忘记使能 D-Cache。代码里写了维护函数,但 SCB_EnableDCache() 没调用,问题不出现,一旦使能就暴露,调试时容易误判。
  • 坑 5:多缓冲区共享 Cache Line。两个变量落在同一 32 字节行内,Invalidate 一个会连带丢弃另一个的修改。用对齐隔离或 MPU 分区解决。

六、总结

D-Cache 与 DMA 的一致性没有银弹,核心是理解 Clean 用于“CPU 写后让 DMA 看到”,Invalidate 用于“DMA 写后让 CPU 看到”。配合 32 字节对齐、传输前后操作、必要时用 MPU 设为 Non-Cacheable,就能在享受 H7 高性能的同时,保证数据可靠。建议把维护函数封装成统一接口,在驱动层调用,避免遗漏。