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

STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,启用 D-Cache 后,DMA 传输的数据一致性成为许多开发者的噩梦。本文将系统讲解 D-Cache 与 DMA 的冲突原理、地址对齐要求、Clean/Invalidate 的正确顺序,并给出完整代码与实测避坑指南。

1. 为什么 D-Cache 会与 DMA 冲突?

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据可能只存在于 Cache 中,并未写回 SRAM。而 DMA 直接访问物理内存,若此时 Cache 中有脏数据(Dirty),DMA 读到的是旧数据;反之,DMA 更新了内存,但 Cache 中仍是旧副本,CPU 读到的也是旧数据。

  • 写回:CPU 写数据只更新 Cache,不立即写内存。
  • 写分配:CPU 写未命中时,先加载整个 Cache Line 到 Cache,再修改。
  • Cache Line:STM32H7 的 D-Cache 行大小为 32 字节。

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

2. 地址对齐:32 字节的硬性要求

由于 Cache Line 为 32 字节,所有 DMA 缓冲区必须按 32 字节对齐,且大小最好是 32 的整数倍。否则,Clean/Invalidate 操作可能误伤相邻数据。

  • 对齐声明:使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。
  • 大小对齐:缓冲区长度建议向上取整到 32 的倍数。
  • 避免栈上变量:局部变量地址不可控,务必使用全局或静态数组。
// 正确示例:32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];

3. Clean 与 Invalidate 的顺序

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

3.1 CPU 发送数据(Memory -> Peripheral)

  1. CPU 填充缓冲区。
  2. Clean D-Cache:将 Cache 中的脏数据写回内存。
  3. 启动 DMA 发送。

注意:Clean 后不要 Invalidate,否则可能丢失尚未写回的数据。

3.2 CPU 接收数据(Peripheral -> Memory)

  1. Invalidate D-Cache:丢弃 Cache 中的旧数据,强制 CPU 后续从内存读取。
  2. 启动 DMA 接收。
  3. DMA 完成后,CPU 读取数据(此时 Cache 未命中,从内存加载新数据)。

注意:Invalidate 必须在 DMA 启动前执行,且要确保没有脏数据未写回。

3.3 双向传输

先 Clean 再 Invalidate 是危险的,因为 Invalidate 会丢弃 Clean 后可能残留的脏数据。正确做法:先 Clean,再 Invalidate,但需确保 Clean 后没有 CPU 写操作。通常建议分开处理。

4. 完整代码示例

以下代码基于 STM32H7 HAL 库,演示 UART DMA 发送与接收的 Cache 维护。

#include "stm32h7xx_hal.h"

#define DMA_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t tx_buffer[DMA_BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[DMA_BUFFER_SIZE];

// 清理 D-Cache(写回)
void clean_dcache(void *addr, uint32_t size) {
    uint32_t start = (uint32_t)addr & ~0x1F; // 向下对齐到 32 字节
    uint32_t end = ((uint32_t)addr + size + 0x1F) & ~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 & ~0x1F;
    uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
    SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start);
}

// UART DMA 发送
void uart_send_dma(uint8_t *data, uint16_t len) {
    memcpy(tx_buffer, data, len);
    clean_dcache(tx_buffer, len);          // 写回 Cache
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

// UART DMA 接收(启动前无效化)
void uart_receive_dma_start(void) {
    invalidate_dcache(rx_buffer, DMA_BUFFER_SIZE); // 丢弃旧数据
    HAL_UART_Receive_DMA(&huart1, rx_buffer, DMA_BUFFER_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 此时 rx_buffer 中已是 DMA 写入的新数据,但 Cache 中可能还有旧副本
    // 需要再次无效化,确保 CPU 读取到最新数据
    invalidate_dcache(rx_buffer, DMA_BUFFER_SIZE);
    // 处理数据...
}

5. 实测踩坑与注意事项

  • 坑1:忘记对齐导致数据错乱。缓冲区未对齐时,Clean/Invalidate 会波及相邻变量,引发随机错误。务必使用 aligned(32)。
  • 坑2:Invalidate 前未 Clean。如果 Cache 中有脏数据,Invalidate 会直接丢弃,导致数据丢失。接收场景下,若 CPU 曾写过该缓冲区,必须先 Clean。
  • 坑3:DMA 传输中 CPU 访问缓冲区。DMA 进行时,CPU 若读写同一缓冲区,会破坏一致性。应使用双缓冲或等待传输完成。
  • 坑4:MPU 配置不当。可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,但会牺牲性能。推荐使用手动维护方式。
  • 坑5:中断中调用 Cache 维护函数。SCB_CleanDCache_by_Addr 等函数执行时间较长,在高速中断中可能影响实时性,建议在任务级处理。

6. 总结

STM32H7 的 D-Cache 与 DMA 一致性并非无解,关键在于:32 字节对齐、正确的 Clean/Invalidate 顺序、以及传输前后的及时维护。掌握这些要点后,你就能在享受 D-Cache 高性能的同时,确保 DMA 数据传输的绝对可靠。建议在项目初期就建立统一的 Cache 维护接口,避免后期调试的痛苦。