一、为什么 D-Cache 和 DMA 会打架?

STM32H7 主频高达 480MHz,为了匹配 CPU 速度,Cortex-M7 内核配备了 L1 D-Cache(数据缓存)。CPU 访问内存时,数据会先被加载到 Cache 中,后续读写直接命中 Cache,从而大幅提升性能。

但 DMA(直接存储器访问)是独立于 CPU 的外设,它直接读写物理内存(如 SRAM、SDRAM),不经过 D-Cache。这就导致:

  • CPU 写数据到内存:数据可能还停留在 Cache 中(写回模式),DMA 读到的是旧数据。
  • DMA 写数据到内存:新数据已写入物理内存,但 CPU 读到的却是 Cache 中的旧数据。

这就是经典的 Cache 一致性(Coherency)问题。

二、Cache 工作模式与操作原语

Cortex-M7 的 D-Cache 支持写回(Write-Back)和写通(Write-Through),通常使用写回模式。操作原语由 CMSIS 提供:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回物理内存(Clean)。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 中的内容,强制下次读取从物理内存加载(Invalidate)。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。

关键点:操作地址必须 32 字节对齐,长度建议向上取整到 32 字节边界。

三、精确操作时机(核心)

根据 DMA 传输方向,操作时机截然不同:

3.1 CPU 发送数据 → DMA 读取(如 UART TX、SPI TX)

  1. CPU 准备好数据缓冲区。
  2. 在启动 DMA 之前,对缓冲区执行 SCB_CleanDCache_by_Addr,确保数据写入物理内存。
  3. 启动 DMA。
  4. DMA 传输完成中断中,无需 Invalidate(因为 CPU 不再读该缓冲区)。

3.2 DMA 接收数据 → CPU 读取(如 UART RX、ADC)

  1. 启动 DMA 之前,对接收缓冲区执行 SCB_InvalidateDCache_by_Addr,丢弃可能存在的旧 Cache 行。
  2. 启动 DMA。
  3. DMA 传输完成中断中,再次执行 SCB_InvalidateDCache_by_Addr,确保 CPU 读到的是 DMA 写入的新数据。

3.3 双向传输(如 SPI 全双工)

  • 发送缓冲区:启动前 Clean。
  • 接收缓冲区:启动前 Invalidate,完成后再次 Invalidate。

四、完整代码示例

以下以 UART DMA 接收为例,展示正确操作。

#include "stm32h7xx.h"

#define RX_BUF_SIZE  128
// 必须 32 字节对齐
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];

void uart_dma_start(void)
{
    // 1. 启动前 Invalidate,丢弃旧 Cache
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);

    // 2. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 传输完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 3. 完成后再次 Invalidate,确保 CPU 读到新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);

    // 4. 安全处理数据
    process_data(rx_buf, RX_BUF_SIZE);
}

发送示例:

__attribute__((aligned(32))) uint8_t tx_buf[64];

void uart_dma_send(void)
{
    // 填充数据
    fill_tx_data(tx_buf, 64);

    // Clean:将 Cache 数据写回物理内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 64);

    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, tx_buf, 64);
}

五、踩坑记录

  • 坑 1:缓冲区未对齐。使用 SCB_InvalidateDCache_by_Addr 时地址非 32 字节对齐,导致 HardFault 或误清除相邻数据。务必用 __attribute__((aligned(32)))。
  • 坑 2:Invalidate 时机错误。在 DMA 传输过程中 Invalidate,可能丢弃 DMA 已写入但尚未被 CPU 读取的数据,导致数据丢失。
  • 坑 3:忘记 Clean 导致发送乱码。CPU 写数据后未 Clean,DMA 读到旧值,串口输出乱码。
  • 坑 4:使用 SCB_InvalidateDCache() 全局操作。全局 Invalidate 会清掉所有 Cache,严重影响性能,且可能破坏其他变量。务必使用按地址操作版本。
  • 坑 5:DMA 描述符或链表未对齐。若使用 MDMA 或链表模式,描述符本身也需对齐并 Clean。

六、最佳实践与注意事项

  • 优先使用 MPU 配置:将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,可从根本上避免一致性问题,但会牺牲部分性能。
  • 缓冲区对齐:所有 DMA 缓冲区必须 32 字节对齐,长度向上取整到 32 字节。
  • 操作范围:Clean/Invalidate 的范围应覆盖整个缓冲区,不要多也不要少。
  • 中断优先级:DMA 完成中断中执行 Cache 操作,注意不要被更高优先级中断打断导致时序混乱。
  • 调试技巧:若怀疑 Cache 问题,可临时关闭 D-Cache 验证,但正式产品不建议关闭。

七、总结

STM32H7 的 D-Cache 与 DMA 共存是一把双刃剑。掌握 Clean 和 Invalidate 的精确时机——发送前 Clean,接收前后 Invalidate——就能避开绝大多数数据一致性问题。结合 32 字节对齐和 MPU 配置,你的嵌入式系统将既高效又稳定。