STM32H7 的 Cache 与 DMA 一致性维护:Clean/Invalidate 操作顺序与典型踩坑
一、为什么 Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核带有 L1 数据缓存(D-Cache)和指令缓存(I-Cache)。当 CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 控制器直接访问物理内存(SRAM/SDRAM),不经过 Cache。这就导致:
- CPU 写数据到内存:数据可能还在 Cache 中,未写回物理内存,DMA 读到旧数据。
- DMA 写数据到内存:新数据写入物理内存,但 CPU 读到的却是 Cache 中的旧数据。
因此,必须在 DMA 传输前后正确维护 Cache 一致性。
二、核心操作:Clean 与 Invalidate
- Clean(清理):将 Cache 中已修改的数据写回物理内存。
- Invalidate(无效化):将 Cache 中的对应行标记为无效,下次 CPU 读取时强制从物理内存重新加载。
注意:Invalidate 会丢弃 Cache 中未写回的数据!如果 CPU 修改了数据但未 Clean,直接 Invalidate 会导致修改丢失。
三、操作顺序:发送与接收场景
1. CPU 发送数据给 DMA(外设)
- CPU 填充缓冲区数据。
- Clean 缓冲区对应的 Cache 行,确保数据写回物理内存。
- 启动 DMA 传输。
2. DMA 接收数据到内存(CPU 读取)
- 启动 DMA 传输前,Invalidate 缓冲区对应的 Cache 行,防止 Cache 中的旧数据干扰。
- 等待 DMA 传输完成。
- 再次 Invalidate 缓冲区,确保 CPU 读取到 DMA 写入的新数据。
关键:接收场景中,启动 DMA 前的 Invalidate 是为了避免 Cache 中残留的旧数据在 DMA 写入后仍被 CPU 误读;传输完成后的 Invalidate 才是真正让 CPU 看到新数据。
四、完整代码示例(基于 HAL 库)
假设使用 SCB_InvalidateDCache_by_Addr 和 SCB_CleanDCache_by_Addr,地址需 32 字节对齐。
#include "stm32h7xx_hal.h"
#define BUF_SIZE 128
#define BUF_ALIGN 32
// 使用对齐属性确保缓冲区32字节对齐
__attribute__((aligned(BUF_ALIGN))) uint8_t dma_rx_buf[BUF_SIZE];
__attribute__((aligned(BUF_ALIGN))) uint8_t dma_tx_buf[BUF_SIZE];
// DMA 接收初始化
void DMA_Receive_Init(void)
{
// 1. 启动 DMA 前,无效化 Cache,丢弃可能存在的旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, BUF_SIZE);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
}
// DMA 接收完成回调(在中断中调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 3. 传输完成后,再次无效化 Cache,确保 CPU 读取到新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, BUF_SIZE);
// 4. 现在可以安全读取 dma_rx_buf 中的数据
process_data(dma_rx_buf, BUF_SIZE);
}
// DMA 发送函数
void DMA_Send_Data(uint8_t *data, uint16_t len)
{
// 1. 将数据拷贝到发送缓冲区(如果直接使用原缓冲区,需确保对齐)
memcpy(dma_tx_buf, data, len);
// 2. Clean Cache,将数据写回物理内存
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}
五、典型踩坑与注意事项
-
地址未对齐:
SCB_*DCache_by_Addr要求地址 32 字节对齐,长度也建议为 32 的倍数。否则可能触发 HardFault 或操作无效。 - 缓冲区跨 Cache 行:如果缓冲区跨越多个 Cache 行,Invalidate 可能误伤相邻数据。务必使用对齐的独立缓冲区。
- 忘记 Clean 导致发送旧数据:CPU 写完后未 Clean,DMA 读到的是物理内存中的旧值。
- 忘记 Invalidate 导致接收旧数据:DMA 写入新数据后,CPU 仍从 Cache 读旧值。
- 在中断中调用 Cache 维护函数:这些函数执行时间较长,可能影响实时性,建议在 DMA 完成回调中仅做必要操作。
- 多缓冲区与 Cache 行共享:如果两个缓冲区共享同一 Cache 行,Invalidate 一个可能影响另一个。建议每个缓冲区独立对齐。
- 使用 MPU 配置内存属性:可将 DMA 缓冲区所在内存配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会牺牲性能。
六、总结
STM32H7 的 Cache 与 DMA 一致性维护并不复杂,关键在于理解 Clean 和 Invalidate 的语义,并严格遵守操作顺序:发送前 Clean,接收前 Invalidate,接收后再次 Invalidate。同时注意地址对齐和缓冲区隔离,就能避开大多数陷阱。对于性能要求不高的场景,也可以考虑将 DMA 缓冲区配置为 Non-Cacheable,一劳永逸。