一、为什么 D-Cache 会和 DMA 打架?
STM32H7 的 Cortex-M7 内核最高运行在 480MHz,为了匹配内核与 SRAM 之间的速度差,芯片集成了 16KB 的 D-Cache(数据缓存)。CPU 访问数据时,如果命中 Cache,就直接从 Cache 读写,不会立即同步到 SRAM。
而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,完全看不到 Cache 的存在。这就导致两个经典问题:
- CPU 写、DMA 读:CPU 把数据写入 Cache,但尚未回写到 SRAM,DMA 从 SRAM 读到的还是旧数据。
- DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本,看不到新数据。
解决思路只有两个:Clean(清理) 和 Invalidate(无效化)。
- Clean:把 Cache 中“脏”数据(已修改但未回写)强制写回 SRAM。
- Invalidate:把 Cache 中的对应行标记为无效,下次 CPU 读取时会重新从 SRAM 加载。
二、精确调用时机(核心)
2.1 CPU 写 → DMA 读(发送方向)
顺序必须是:CPU 写数据 → Clean D-Cache → 启动 DMA。
// 假设 buffer 是 32 字节对齐的发送缓冲区
memcpy(tx_buffer, source, LEN);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, LEN);
HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->DR, LEN);
注意:Clean 必须在启动 DMA 之前,否则 DMA 可能读到旧数据。
2.2 DMA 写 → CPU 读(接收方向)
顺序必须是:启动 DMA → 等待 DMA 完成 → Invalidate D-Cache → CPU 读数据。
HAL_DMA_Start(&hdma, (uint32_t)&UART->DR, (uint32_t)rx_buffer, LEN);
// 等待 DMA 传输完成(中断或轮询)
while (!dma_done);
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, LEN);
process_data(rx_buffer);
关键:Invalidate 必须在 DMA 完成之后、CPU 读取之前。如果在 DMA 传输过程中 Invalidate,可能把尚未写入 SRAM 的脏数据丢弃,造成数据损坏。
2.3 双向传输(如 SPI 全双工)
先 Clean 发送缓冲区,再启动 DMA,完成后 Invalidate 接收缓冲区。
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, LEN);
HAL_SPI_TransmitReceive_DMA(&hspi, tx_buf, rx_buf, LEN);
// 等待完成
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, LEN);
三、地址对齐与长度陷阱
SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 要求地址按 32 字节对齐,长度最好是 32 的整数倍。如果缓冲区未对齐,函数内部会处理,但可能误伤相邻数据。
最佳实践:
- 使用
__attribute__((aligned(32)))定义 DMA 缓冲区。 - 长度向上取整到 32 的倍数。
__attribute__((aligned(32))) uint8_t rx_buffer[256];
#define ALIGN_UP_32(x) (((x) + 31U) & ~31U)
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, ALIGN_UP_32(LEN));
四、实测数据对比
测试平台:STM32H743 @ 400MHz,SRAM 为 DTCM 和 AXI SRAM,DMA 搬运 4KB 数据。
| 场景 | 耗时 (μs) | 说明 | |------|-----------|------| | 无 D-Cache,CPU 直接访问 | 约 120 | 每次读写都到 SRAM | | 有 D-Cache,无维护 | 约 35 | 但数据可能不一致 | | 有 D-Cache + Clean/Invalidate | 约 42 | 增加约 7μs 维护开销 |
结论:Cache 维护带来的开销很小(约 20%),但换来了数据一致性,完全值得。对于高频小数据量 DMA,建议将缓冲区放在 DTCM(不经过 Cache),可彻底避免一致性问题。
五、完整代码示例(UART DMA 接收)
#include "stm32h7xx_hal.h"
#define RX_LEN 128
__attribute__((aligned(32))) uint8_t rx_buffer[RX_LEN];
volatile uint8_t dma_done = 0;
void UART_DMA_Init(void) {
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_LEN);
}
// DMA 完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 1. 无效化 D-Cache,确保 CPU 读到 SRAM 中的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, RX_LEN);
// 2. 处理数据
process_data(rx_buffer, RX_LEN);
// 3. 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_LEN);
}
六、注意事项与避坑指南
- 不要频繁 Invalidate:每次 Invalidate 都会导致后续访问 Cache Miss,降低性能。只在 DMA 完成后做一次。
-
中断中调用要小心:
SCB_InvalidateDCache_by_Addr本身不是线程安全的,但通常中断中调用没问题,注意不要与主循环同时操作同一缓冲区。 - DTCM 是避风港:DTCM 不经过 D-Cache,DMA 可以直接访问,无需维护。但 DTCM 容量有限(通常 128KB),适合小缓冲区。
- MPU 配置:可以将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题。
- 调试时关闭 Cache:如果怀疑 Cache 问题,可临时关闭 D-Cache 验证,但正式产品不建议关闭。
七、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。记住两个核心顺序:
- 发送:写数据 → Clean → 启动 DMA
- 接收:启动 DMA → 等待完成 → Invalidate → 读数据
配合 32 字节对齐和合理的 MPU 配置,就能在享受 Cache 高性能的同时,保证数据一致性。实测表明,维护开销仅增加约 20%,远小于数据错误带来的风险。