STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时序与实测踩坑清单

一、为什么 D-Cache 会让 DMA 数据错乱?

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存),默认写回(Write-Back)策略。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 SRAM。而 DMA 控制器直接访问物理内存,不经过 Cache。

  • CPU 写 → DMA 读:CPU 写入的数据还在 Cache 里,DMA 读到的却是旧数据。
  • DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧内容。

解决思路:在 DMA 传输前后,手动维护 Cache 一致性。

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

Cortex-M7 的 Cache 行大小为 32 字节。Clean 和 Invalidate 操作以 Cache 行为单位,若缓冲区地址或长度未按 32 字节对齐,会误伤相邻数据。

  • 起始地址:必须 32 字节对齐,推荐使用 __attribute__((aligned(32)))
  • 长度:必须为 32 的整数倍,不足时向上取整。
  • 推荐做法:使用 SCB_InvalidateDCache_by_AddrSCB_CleanDCache_by_Addr,它们内部会处理对齐,但传入的地址仍需对齐,否则可能触发 HardFault。
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

三、Clean 与 Invalidate 的时序

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

  1. CPU 填充缓冲区。
  2. Clean D-Cache:将 Cache 中的数据写回 SRAM。
  3. 启动 DMA 发送。
  4. 等待 DMA 完成。
memcpy(dma_tx_buf, src, len);
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);

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

  1. 启动 DMA 接收。
  2. 等待 DMA 完成。
  3. Invalidate D-Cache:丢弃 Cache 中的旧数据,强制 CPU 从 SRAM 重新读取。
  4. CPU 读取缓冲区。
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, len);
// 等待传输完成回调
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, len);
process_data(dma_rx_buf, len);

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

先 Clean 发送缓冲区,再启动传输,完成后 Invalidate 接收缓冲区。

SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buf, rx_buf, len);
// 传输完成回调中
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);

四、完整代码示例:UART DMA 回环测试

#include "stm32h7xx_hal.h"

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
volatile uint8_t dma_done = 0;

void uart_dma_init(void) {
    // 填充发送数据
    for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;

    // 启动接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);

    // 发送:先 Clean
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 接收完成:Invalidate 后再读
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    dma_done = 1;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_DMA_Init();
    MX_USART1_UART_Init();
    uart_dma_init();

    while (1) {
        if (dma_done) {
            // 校验数据
            for (int i = 0; i < BUF_SIZE; i++) {
                if (rx_buf[i] != i) {
                    Error_Handler();
                }
            }
            dma_done = 0;
            HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
        }
    }
}

五、实测踩坑清单

  • 忘记 Clean 导致发送数据错乱:CPU 写完后直接启动 DMA,DMA 读到旧数据。
  • 忘记 Invalidate 导致接收数据错乱:DMA 写完后 CPU 读 Cache 旧值。
  • 缓冲区未对齐SCB_InvalidateDCache_by_Addr 传入非 32 字节对齐地址,触发 HardFault。
  • 长度非 32 倍数:Invalidate 会误伤相邻变量,导致其他数据被意外丢弃。
  • 在中断中调用 Cache 维护函数:这些函数执行时间较长,可能影响实时性,建议在 DMA 完成回调中处理。
  • 使用 SCB_InvalidateDCache() 全局失效:会清掉所有 Cache,性能骤降,且可能影响其他任务。
  • DMA 描述符与数据缓冲区共享 Cache 行:若描述符与数据在同一 32 字节行内,Clean/Invalidate 会互相干扰,务必分开对齐。
  • MPU 配置不当:可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,但会牺牲性能,需权衡。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性核心就是:发送前 Clean,接收后 Invalidate,地址长度 32 字节对齐。掌握时序并严格遵守对齐规则,就能在享受 Cache 高性能的同时,保证 DMA 数据传输稳定可靠。建议将 Cache 维护封装成宏或函数,减少遗漏。