STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时序与踩坑复盘

STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 D-Cache。但 DMA 直接访问物理内存,不经过 Cache,若两者同时操作同一块内存,数据一致性就成了大问题。本文带你从原理到实践,彻底解决这个痛点。

一、为什么会有数据一致性问题?

  • CPU 写数据:先写入 D-Cache,标记为 dirty,稍后才写回 SRAM。
  • DMA 读数据:直接读 SRAM,此时 SRAM 里还是旧数据,导致 DMA 读到错误内容。
  • DMA 写数据:直接写 SRAM,但 CPU 可能仍从 Cache 读旧值,导致 CPU 看不到新数据。

因此,在 DMA 传输前后,必须手动维护 Cache 与内存的一致性。

二、地址对齐:必须遵守的硬性规则

Cortex-M7 的 Cache 行大小为 32 字节。任何 Clean/Invalidate 操作都以 32 字节为单位。

  • DMA 缓冲区必须 32 字节对齐,且大小最好是 32 的整数倍。
  • 若缓冲区跨越两个 Cache 行,Invalidate 时可能误伤相邻数据,导致其他变量被丢弃。

推荐使用 __attribute__((aligned(32))) 定义缓冲区:

__attribute__((aligned(32))) uint8_t dma_buffer[256];

三、Clean 与 Invalidate 的时序

1. CPU 写 → DMA 读(发送数据)

  • Clean(写回)D-Cache,确保 SRAM 数据最新。
  • 再启动 DMA 发送。
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
HAL_UART_Transmit_DMA(&huart1, dma_buffer, sizeof(dma_buffer));

2. DMA 写 → CPU 读(接收数据)

  • DMA 接收完成前,不要提前 Invalidate。
  • 在 DMA 完成中断中,先 Invalidate(丢弃)D-Cache,再读取数据。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
    // 现在可以安全读取 dma_buffer
}

注意:Invalidate 会丢弃未写回的数据,若缓冲区同时被 CPU 写过,必须先 Clean 再 Invalidate。

四、完整代码示例(UART DMA 接收)

#include "stm32h7xx_hal.h"

__attribute__((aligned(32))) uint8_t rx_buffer[128];

void uart_dma_init(void) {
    // 配置 UART 和 DMA,使能 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 1. 无效化 D-Cache,确保读到 DMA 写入的最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
        
        // 2. 处理数据
        process_data(rx_buffer, sizeof(rx_buffer));
        
        // 3. 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
    }
}

// 发送数据示例
void uart_send_data(uint8_t *data, uint16_t len) {
    // 确保数据在 32 字节对齐的缓冲区中
    __attribute__((aligned(32))) static uint8_t tx_buffer[128];
    memcpy(tx_buffer, data, len);
    
    // Clean D-Cache,将数据写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, len);
    
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

五、踩坑复盘与注意事项

  • 坑1:缓冲区未对齐。导致 Invalidate 时误伤相邻变量,程序跑飞。务必使用 aligned(32)
  • 坑2:Clean/Invalidate 顺序错误。发送前忘 Clean,接收后忘 Invalidate,数据错乱。
  • 坑3:在 DMA 传输过程中操作 Cache。例如 DMA 还在写,CPU 就 Invalidate,可能读到半新半旧数据。务必在 DMA 完成中断中处理。
  • 坑4:使用 SCB_InvalidateDCache() 全局无效化。这会丢弃所有 Cache 数据,极大降低性能,应使用按地址操作。
  • 坑5:MPU 配置不当。可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,但会牺牲性能。推荐仍用 Cache + 手动维护。

额外提醒:STM32H7 的 Cache 维护函数在 core_cm7.h 中,需包含 #include "stm32h7xx.h"。若使用 RTOS,注意临界区保护。

六、总结

D-Cache 与 DMA 的一致性并不复杂,核心就是:对齐、Clean 前、Invalidate 后。理解 32 字节 Cache 行的操作粒度,严格遵循时序,就能避开绝大多数坑。希望本文能帮你少走弯路,充分发挥 STM32H7 的性能。