STM32H7 的 DCache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 时序与踩坑案例

一、为什么 DCache 会让 DMA 出错?

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存),默认写回(Write-Back)、写分配(Write-Allocate)。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM;而 DMA 直接访问物理内存,两者看到的数据可能不一致。

典型场景:

  • CPU 写 → DMA 读:CPU 写的数据还在 Cache 里,DMA 读到的却是旧数据。
  • DMA 写 → CPU 读:DMA 已更新内存,但 CPU 读到的仍是 Cache 中的旧值。

解决思路只有两个:Clean(将 Cache 写回内存)和 Invalidate(丢弃 Cache,强制从内存重读)。但使用时序和地址对齐要求非常严格。

二、地址对齐:32 字节是硬性要求

Cortex-M7 的 Cache Line 为 32 字节。Clean/Invalidate 操作以 Cache Line 为单位,因此 DMA 缓冲区地址和长度必须 32 字节对齐,否则会误伤相邻数据。

// 推荐:使用编译器属性强制对齐
__attribute__((aligned(32))) uint8_t dma_buffer[1024];

// 或使用 ALIGN_32BYTES 宏(HAL 库提供)
ALIGN_32BYTES(uint8_t dma_buffer[1024]);

若缓冲区长度不是 32 的整数倍,Invalidate 时可能把相邻变量所在的 Cache Line 一并丢弃,导致其他数据丢失。建议长度也补齐到 32 字节的整数倍。

三、Clean / Invalidate 的正确时序

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

顺序:先 Clean,再启动 DMA。

// 1. CPU 填充数据到 dma_buffer
memcpy(dma_buffer, src, len);

// 2. Clean:将 Cache 写回内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_buffer, len);

// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_buffer, len);

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

顺序:先启动 DMA,DMA 完成后再 Invalidate。

// 1. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_buffer, len);

// 2. 在 DMA 完成回调中 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buffer, len);
    // 3. 此时 CPU 才能安全读取新数据
    process_data(dma_buffer, len);
}

关键点:Invalidate 必须在 DMA 完成之后、CPU 读取之前执行。若在 DMA 传输过程中 Invalidate,可能丢弃 DMA 刚写入但尚未完成的数据。

四、完整代码示例:UART DMA 收发

#include "stm32h7xx_hal.h"

#define BUF_SIZE 256
ALIGN_32BYTES(uint8_t tx_buf[BUF_SIZE]);
ALIGN_32BYTES(uint8_t rx_buf[BUF_SIZE]);

// 发送函数
void uart_send_dma(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    // 长度补齐到 32 字节
    uint32_t aligned_len = (len + 31) & ~31U;
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, aligned_len);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        uint32_t aligned_len = (BUF_SIZE + 31) & ~31U;
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, aligned_len);
        // 安全读取 rx_buf
        process_rx_data(rx_buf, BUF_SIZE);
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
    }
}

五、踩坑案例

案例 1:缓冲区未对齐导致 HardFault

某项目定义 uint8_t buf[100] 未加对齐属性,调用 SCB_InvalidateDCache_by_Addr 时地址非 32 字节对齐,触发 HardFault。解决:使用 ALIGN_32BYTES 并确保长度对齐。

案例 2:Invalidate 时机错误导致数据丢失

在 DMA 传输 进行中 调用 Invalidate,结果 DMA 后续写入的数据被 Cache 丢弃,接收数据随机错乱。解决:严格在 DMA 完成回调中执行 Invalidate。

案例 3:忘记 Clean 导致发送旧数据

CPU 更新 tx_buf 后直接启动 DMA,未 Clean,DMA 发送的是内存中的旧内容。解决:发送前必须 Clean。

六、注意事项

  • 对齐是底线:DMA 缓冲区地址和长度尽量按 32 字节对齐。
  • 时序不能乱:发送先 Clean 后启动;接收先启动后 Invalidate。
  • 避免频繁操作:Clean/Invalidate 本身有开销,小数据可考虑用非 Cache 区域(如 DTCM)。
  • MPU 配置:可将 DMA 缓冲区所在 SRAM 配置为 Write-Through 或 Non-Cacheable,从根源避免一致性问题。
  • 调试技巧:若数据异常,先检查 Cache 操作是否遗漏或顺序错误,再排查 DMA 配置。

掌握以上要点,STM32H7 的 DCache 与 DMA 就能和谐共存,既享受高性能,又保证数据可靠。