STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 操作顺序与典型踩坑

STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备了 16KB 的 D-Cache 和 I-Cache。D-Cache 的引入大幅提升了 CPU 访问外部存储器的效率,但也给 DMA 传输带来了棘手的数据一致性问题。如果你在使用 DMA 时发现数据“偶尔”出错,很可能就是 D-Cache 在作祟。本文将深入剖析原理,给出正确的 Clean/Invalidate 操作顺序,并总结典型踩坑案例。

一、为什么 D-Cache 会导致 DMA 数据不一致?

Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 访问数据时,会先将数据读入 Cache 行(通常 32 字节)。当 CPU 写数据时,如果 Cache 命中,则只修改 Cache 中的内容,并标记为“脏”(Dirty),并不会立即写回主存。

而 DMA 控制器直接访问主存(SRAM 或外部 SDRAM),它看不到 Cache 中的最新数据。这就导致了两种典型的不一致场景:

  • CPU 写,DMA 读:CPU 写入的数据还在 Cache 中(脏行),DMA 从主存读到的却是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入主存,但 CPU 读到的却是 Cache 中缓存的旧数据。

解决这一问题的核心就是 Clean(清理)Invalidate(无效化) 操作:

  • Clean:将 Cache 中的脏行写回主存,使主存数据与 Cache 一致。
  • Invalidate:将 Cache 行标记为无效,下次 CPU 读取时会从主存重新加载。

二、正确操作顺序:Clean 与 Invalidate 的时机

操作顺序取决于数据传输方向。记住一个原则:谁写数据,谁负责 Clean;谁读数据,谁负责 Invalidate

2.1 CPU 写 → DMA 读(发送方向)

  1. CPU 准备好数据缓冲区。
  2. 执行 Clean 操作,将缓冲区对应的 Cache 行写回主存。
  3. 启动 DMA 发送。
  4. 等待 DMA 传输完成。

注意:Clean 操作必须在启动 DMA 之前执行。否则 DMA 可能读到旧数据。

2.2 DMA 写 → CPU 读(接收方向)

  1. 启动 DMA 接收。
  2. 等待 DMA 传输完成。
  3. 执行 Invalidate 操作,使缓冲区对应的 Cache 行无效。
  4. CPU 读取数据(此时会从主存重新加载最新数据)。

注意:Invalidate 必须在 DMA 完成之后执行。如果在 DMA 传输过程中执行 Invalidate,可能会丢弃 CPU 尚未写回的数据,导致数据损坏。

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

对于同时收发的情况,通常需要:

  1. 准备发送数据,执行 Clean。
  2. 启动 DMA 收发。
  3. 等待完成。
  4. 对接收缓冲区执行 Invalidate。

三、完整代码示例

以下代码基于 STM32H7 HAL 库,演示了 UART DMA 发送和接收的正确 Cache 维护操作。

#include "stm32h7xx_hal.h"

#define BUFFER_SIZE 64

/* 使用 32 字节对齐的缓冲区,确保 Cache 行对齐 */
aligned(32) uint8_t tx_buffer[BUFFER_SIZE];
aligned(32) uint8_t rx_buffer[BUFFER_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

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

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

/* DMA 发送示例 */
void uart_dma_send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buffer, data, len);
    /* 1. 清理 Cache,确保主存数据最新 */
    clean_dcache(tx_buffer, len);
    /* 2. 启动 DMA 发送 */
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
    /* 3. 等待完成(实际项目中可用中断或轮询标志) */
    while (huart1.gState != HAL_UART_STATE_READY) { }
}

/* DMA 接收示例 */
void uart_dma_receive(uint8_t *data, uint16_t len)
{
    /* 1. 启动 DMA 接收 */
    HAL_UART_Receive_DMA(&huart1, rx_buffer, len);
    /* 2. 等待完成 */
    while (huart1.RxState != HAL_UART_STATE_READY) { }
    /* 3. 无效化 Cache,丢弃旧缓存 */
    invalidate_dcache(rx_buffer, len);
    /* 4. 读取数据 */
    memcpy(data, rx_buffer, len);
}

四、典型踩坑与注意事项

  • 缓冲区未对齐:Cache 操作以 32 字节行为单位。如果缓冲区起始地址或大小未对齐,Clean/Invalidate 可能影响相邻数据,导致意外错误。务必使用 __attribute__((aligned(32)))aligned(32) 声明缓冲区。
  • Invalidate 时机错误:在 DMA 传输过程中执行 Invalidate,可能丢弃 CPU 刚写入但未 Clean 的数据。务必等待 DMA 完成后再操作。
  • 忘记 Clean 导致发送旧数据:CPU 写缓冲区后直接启动 DMA,未执行 Clean,DMA 读到的是主存中的旧内容。
  • 忘记 Invalidate 导致接收数据错乱:DMA 接收完成后,CPU 直接读缓冲区,读到的是 Cache 中的旧数据。
  • 多缓冲区或链表传输:如果使用多个缓冲区,需对每个缓冲区分别执行 Clean/Invalidate。
  • 中断与 Cache 操作冲突:在中断中操作 Cache 时,注意临界区保护,避免打断正在进行的 Cache 维护。
  • 使用 MPU 配置内存属性:对于频繁 DMA 访问的区域,可配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会牺牲部分性能。

五、总结

STM32H7 的 D-Cache 与 DMA 协同工作并不复杂,关键在于理解数据流向并正确使用 Clean 和 Invalidate。记住:发送前 Clean,接收后 Invalidate。同时注意缓冲区对齐和操作时机,就能避开绝大多数坑。希望本文能帮助你在 STM32H7 上写出稳定高效的 DMA 应用。