STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 误用导致的偶发数据错乱定位方法
一、为什么 H7 上 DMA 会“偶发”出错
STM32H7 的 Cortex-M7 内核带有一级 D-Cache(通常 16KB)。CPU 访问 SRAM 时,数据可能只落在 Cache 中,并未写回物理内存;同理,DMA 直接改写物理内存后,CPU 读到的却可能是 Cache 里的旧值。
- CPU 写 → DMA 读:CPU 写入的数据还在 Cache 里,DMA 从 SRAM 读到旧数据。
- DMA 写 → CPU 读:DMA 已更新 SRAM,但 CPU 命中了 Cache 中的旧数据。
这类问题往往表现为“偶发”“概率性”“重启后正常”,极难定位。根因通常是 Clean/Invalidate 的时机或范围用错。
二、Clean 与 Invalidate 的正确语义
- Clean(清理):把 Cache 中“脏”数据写回物理内存。用于 CPU 写、DMA 读 之前。
- Invalidate(无效化):丢弃 Cache 中的旧数据,强制下次从内存重新加载。用于 DMA 写、CPU 读 之后。
- Clean+Invalidate:既写回又丢弃,用于缓冲区被双向使用或地址范围可能重叠时。
关键原则:操作方向决定用哪个。写反了,问题只会更隐蔽。
三、典型误用场景
- 发送前只 Invalidate 不 Clean:CPU 刚填好的发送缓冲区被 Invalidate 丢弃,DMA 发出旧数据。
- 接收后只 Clean 不 Invalidate:CPU 仍读到 Cache 旧值,DMA 新数据被覆盖。
- 按字节地址而非 32 字节对齐操作:H7 的 Cache 行是 32 字节,非对齐范围会波及相邻变量,造成“无辜”数据被破坏。
- DMA 传输过程中 CPU 访问缓冲区:传输未完成就 Clean/Invalidate,导致数据撕裂。
四、系统化定位方法
- 第一步:确认缓冲区是否在 Cacheable 区域。H7 的 AXI SRAM(0x24000000)默认 Cacheable,DTCM(0x20000000)不经过 Cache。
- 第二步:用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,作为对照实验。若问题消失,基本锁定 Cache 一致性。
- 第三步:检查 Clean/Invalidate 的调用时机与方向,对照第二节原则。
-
第四步:检查地址与长度是否 32 字节对齐,必要时用
SCB_InvalidateDCache_by_Addr并向上取整。 -
第五步:在 DMA 完成中断里加内存屏障
__DSB(),确保操作顺序。
五、完整代码示例
以下以 UART DMA 接收为例,展示正确用法。
#include "stm32h7xx_hal.h"
#define RX_BUF_SIZE 256
/* 必须 32 字节对齐,且长度按 32 字节向上取整 */
__attribute__((aligned(32))) uint8_t rxBuf[RX_BUF_SIZE];
/* 启动 DMA 接收前:Invalidate,丢弃旧 Cache,让 DMA 写入内存 */
void UART_DMA_StartReceive(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuf, RX_BUF_SIZE);
__DSB(); /* 确保 Invalidate 完成后再启动 DMA */
HAL_UART_Receive_DMA(huart, rxBuf, RX_BUF_SIZE);
}
/* DMA 接收完成回调:Invalidate,让 CPU 读到 DMA 写入的新数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuf, RX_BUF_SIZE);
__DSB();
/* 此时 CPU 读取 rxBuf 才是 DMA 写入的真实数据 */
ProcessData(rxBuf, RX_BUF_SIZE);
}
/* DMA 发送:CPU 填好数据后 Clean,确保 DMA 读到最新数据 */
__attribute__((aligned(32))) uint8_t txBuf[RX_BUF_SIZE];
void UART_DMA_Send(UART_HandleTypeDef *huart)
{
FillTxData(txBuf, RX_BUF_SIZE);
SCB_CleanDCache_by_Addr((uint32_t *)txBuf, RX_BUF_SIZE);
__DSB();
HAL_UART_Transmit_DMA(huart, txBuf, RX_BUF_SIZE);
}
若缓冲区被双向使用,使用 SCB_CleanInvalidateDCache_by_Addr()。
六、注意事项
- 长度必须 32 字节对齐:H7 Cache 行 32 字节,非对齐会误伤相邻数据。
-
地址必须 32 字节对齐:用
__attribute__((aligned(32)))或ALIGN_32BYTES。 - DMA 传输期间禁止 CPU 访问缓冲区,否则 Clean/Invalidate 会破坏进行中的传输。
- DTCM 不经过 Cache,把 DMA 缓冲区放 DTCM 可彻底规避,但 DTCM 容量有限且部分外设无法访问。
- MPU 配置 Non-Cacheable 是更稳妥的方案,代价是 CPU 访问变慢。
- 调试时先关 D-Cache 验证,确认问题是否由 Cache 引起,再逐步开启。
七、小结
STM32H7 的 D-Cache 与 DMA 一致性问题的核心是:谁写谁读,决定 Clean 还是 Invalidate。牢记“CPU 写→Clean,DMA 写→Invalidate”,并保证 32 字节对齐与正确的内存屏障,就能把这类“偶发数据错乱”从玄学变成可定位、可复现的工程问题。