STM32H7 的 DCache 与 DMA 数据一致性:Clean/Invalidate 时机与踩坑复盘
STM32H7 凭借 480MHz 的 Cortex-M7 和 16KB D-Cache,性能远超 F4/F7。但很多开发者第一次用 DMA 就遇到“数据对不上”“偶发硬件错误”,根源往往在 D-Cache 与 DMA 的数据一致性。本文从原理到代码,帮你彻底理清。
一、为什么 DMA 和 D-Cache 会打架?
Cortex-M7 的 D-Cache 位于 CPU 和总线矩阵之间。CPU 读写内存时,若命中 Cache,实际只操作 Cache 行,不会立刻写回 SRAM。而 DMA 是总线上的独立主设备,它直接访问 SRAM,完全绕过 Cache。
于是出现两种典型问题:
- CPU 写、DMA 读:CPU 把数据写进 Cache 但未回写 SRAM,DMA 从 SRAM 读到旧数据。
- DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本。
解决思路只有两条:Clean(把 Cache 脏行写回 SRAM)和 Invalidate(丢弃 Cache 行,强制下次从 SRAM 重读)。
二、Clean 与 Invalidate 的正确时机
记住一个口诀:发送前 Clean,接收后 Invalidate。
1. CPU 发送数据给 DMA(内存 → 外设)
DMA 要读 SRAM,必须保证 SRAM 里是最新数据。所以启动 DMA 前,对发送缓冲区执行 Clean:
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, tx_len);
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, tx_len);
2. DMA 接收数据给 CPU(外设 → 内存)
DMA 写 SRAM 后,CPU 的 Cache 里可能还是旧数据。所以 DMA 传输完成中断里,对接收缓冲区执行 Invalidate:
void DMA_RxCompleteCallback(void) {
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);
// 现在可以安全读取 rx_buf
}
3. 双向传输(如 SPI 全双工)
发送前 Clean TX,接收后 Invalidate RX,两者独立操作。
三、完整代码示例:UART DMA 收发
以下代码基于 STM32H743,使用 HAL 库,演示安全的数据收发。
#include "stm32h7xx_hal.h"
#define BUF_SIZE 64
// 注意:缓冲区必须 32 字节对齐,且大小为 32 字节整数倍
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
// 发送函数:先 Clean,再启动 DMA
void UART_Send_DMA(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
// 关键:Clean 发送缓冲区,确保 SRAM 数据最新
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
// DMA 接收完成回调:先 Invalidate,再处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 关键:Invalidate 接收缓冲区,丢弃 Cache 旧副本
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 此时读取 rx_buf 才是 DMA 写入的新数据
Process_Data(rx_buf, BUF_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
}
// 初始化时确保 MPU 配置正确(见注意事项)
void Cache_Init(void) {
// 使能 D-Cache 和 I-Cache
SCB_EnableICache();
SCB_EnableDCache();
}
四、踩坑复盘:那些年我们掉过的坑
坑 1:缓冲区未对齐,Clean/Invalidate 越界
SCB_CleanDCache_by_Addr 按 32 字节 Cache 行操作。若缓冲区地址未 32 字节对齐,或长度不是 32 的倍数,会误伤相邻数据,导致其他变量被清掉或回写。
对策:所有 DMA 缓冲区用 __attribute__((aligned(32))),长度向上取整到 32 的倍数。
坑 2:在 DMA 传输过程中 Invalidate
有人图省事,在 DMA 还在写 SRAM 时就 Invalidate,结果 Cache 行被丢弃,但 DMA 后续写入的数据又可能被 CPU 读走旧值,甚至触发总线错误。
对策:Invalidate 必须在 DMA 完成中断里执行,确保传输已结束。
坑 3:忘记配置 MPU,Cache 策略错误
STM32H7 默认 Cache 策略是 Write-Back, Write-Allocate。对于 DMA 缓冲区,最好通过 MPU 配置为 Write-Through 或 Non-Cacheable,减少手动维护。但注意:Non-Cacheable 会降低 CPU 访问性能。
对策:对频繁 DMA 的大缓冲区,用 MPU 设为 Non-Cacheable;小缓冲区用 Clean/Invalidate 手动维护。
坑 4:只 Clean 不 Invalidate,或反之
发送时只 Clean 是对的,但接收时若只 Clean 不 Invalidate,CPU 仍读 Cache 旧值。必须严格按方向操作。
坑 5:中断里调用 Cache 维护函数耗时过长
SCB_InvalidateDCache_by_Addr 会遍历 Cache 行,若缓冲区很大(如几十 KB),在中断里执行可能影响实时性。
对策:大缓冲区建议用 MPU 配置为 Non-Cacheable,或把 Cache 维护放到主循环中处理。
五、总结
STM32H7 的 D-Cache 与 DMA 一致性并不复杂,核心就是:发送前 Clean,接收后 Invalidate,缓冲区对齐,MPU 辅助。理解 Cache 行和总线行为,就能写出既高效又稳定的代码。下次遇到 DMA 数据错乱,先检查 Cache 维护时机,多半能秒杀问题。