STM32H7 的 DCache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 顺序与典型踩坑

STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,并配备 L1 数据缓存(DCache)。DCache 能显著提升 CPU 访问外部存储器的效率,但同时也给 DMA 传输带来了数据一致性问题。许多开发者在调试以太网、SDMMC、SPI 等外设时,常遇到“数据错乱”、“偶发丢包”等现象,根源往往就是 DCache 与 DMA 的冲突。本文将深入剖析原理,给出正确配置步骤和完整代码,并总结典型踩坑点。

一、为什么 DCache 会导致 DMA 数据不一致?

Cortex-M7 的 DCache 位于 CPU 和总线矩阵之间。当 CPU 访问内存时,数据会被缓存到 DCache 中,后续访问直接命中缓存,不再访问实际内存。而 DMA 控制器直接访问物理内存,不经过 DCache。

  • CPU 写数据到内存:数据可能只停留在 DCache 中(写回模式),实际内存还是旧值。此时启动 DMA 发送,DMA 读到的是旧数据。
  • DMA 从外设接收数据到内存:DMA 更新了物理内存,但 DCache 中可能还保留着该地址的旧数据。CPU 读取时命中缓存,得到旧数据。

因此,必须在 DMA 传输前后正确维护 DCache,保证 CPU 和 DMA 看到的数据一致。

二、地址对齐与 Cache Line 的影响

Cortex-M7 的 DCache 以 Cache Line 为单位操作,通常为 32 字节。Clean 和 Invalidate 操作都是按行进行的。

  • 如果 DMA 缓冲区起始地址或长度未按 32 字节对齐,Clean/Invalidate 会影响到相邻数据,可能导致其他变量被意外清除或写回。
  • 例如,缓冲区大小为 100 字节,起始地址 0x20000000(32 字节对齐),但结束地址不是 32 字节对齐,Invalidate 时会多清除几个字节,若这些字节属于其他变量,就会丢失数据。

建议:DMA 缓冲区使用 __attribute__((aligned(32))) 强制 32 字节对齐,且长度尽量为 32 的整数倍。

三、Clean 与 Invalidate 的正确顺序

根据 DMA 传输方向,操作顺序不同:

1. CPU 发送数据(内存 -> 外设)

  • 步骤:CPU 填充缓冲区 -> Clean DCache(将缓存写回内存) -> 启动 DMA 发送。
  • 注意:Clean 后不要再次写缓冲区,否则新数据可能仍在缓存中。

2. CPU 接收数据(外设 -> 内存)

  • 步骤:启动 DMA 接收 -> 等待 DMA 完成 -> Invalidate DCache(丢弃缓存中的旧数据) -> CPU 读取缓冲区。
  • 注意:Invalidate 必须在 DMA 完成后进行,否则可能丢弃 DMA 尚未写入的数据。

3. 双向传输(如以太网描述符)

  • 需根据描述符所有权交替执行 Clean 和 Invalidate。

四、完整代码示例

以下代码基于 STM32H7 HAL 库,演示如何安全使用 DMA 进行 UART 发送和接收。

#include "stm32h7xx_hal.h"

#define DMA_BUF_SIZE  128

/* 32 字节对齐的 DMA 缓冲区 */
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

/* 清理 DCache:将指定地址范围的数据写回内存 */
void dcache_clean(void *addr, uint32_t size)
{
    uint32_t start = (uint32_t)addr;
    uint32_t end = start + size;
    /* 按 32 字节对齐调整 */
    start &= ~0x1F;
    end = (end + 0x1F) & ~0x1F;
    SCB_CleanDCache_by_Addr((uint32_t *)start, end - start);
}

/* 无效化 DCache:丢弃指定地址范围的缓存 */
void dcache_invalidate(void *addr, uint32_t size)
{
    uint32_t start = (uint32_t)addr;
    uint32_t end = start + size;
    start &= ~0x1F;
    end = (end + 0x1F) & ~0x1F;
    SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start);
}

/* 发送数据:先 Clean 再启动 DMA */
void uart_dma_send(uint8_t *data, uint16_t len)
{
    memcpy(dma_tx_buf, data, len);
    dcache_clean(dma_tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

/* 接收完成回调:Invalidate 后处理数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        dcache_invalidate(dma_rx_buf, DMA_BUF_SIZE);
        /* 现在可以安全读取 dma_rx_buf 中的数据 */
        process_data(dma_rx_buf, DMA_BUF_SIZE);
        /* 重新启动接收 */
        HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
    }
}

五、典型踩坑与注意事项

  • 坑1:忘记 Clean 导致发送旧数据。CPU 写缓冲区后直接启动 DMA,DMA 读到内存中的旧值。务必在启动发送前 Clean。
  • 坑2:Invalidate 时机错误。在 DMA 完成前 Invalidate,会丢弃 DMA 正在写入的数据,导致接收数据丢失。应在 DMA 完成中断中执行。
  • 坑3:缓冲区未对齐。非 32 字节对齐的缓冲区,Clean/Invalidate 会波及相邻变量,引发随机错误。务必使用 aligned(32)
  • 坑4:Cache 维护操作未考虑重叠。如果多个 DMA 缓冲区在同一个 Cache Line 内,一个缓冲区的 Invalidate 可能影响另一个。建议每个缓冲区独占 Cache Line。
  • 坑5:使用 SCB_InvalidateDCache 全局操作。全局 Invalidate 会清掉所有缓存,严重影响性能,且可能丢失未写回的数据。应使用按地址操作函数。
  • 坑6:忽略内存屏障。在 Clean/Invalidate 后,最好插入 __DSB() 确保操作完成。

六、总结

STM32H7 的 DCache 与 DMA 共存时,数据一致性必须由软件维护。核心原则:发送前 Clean,接收后 Invalidate,地址对齐 32 字节,操作范围精确。掌握这些要点,就能避免绝大多数 DMA 数据错乱问题,充分发挥 H7 的高性能优势。