STM32H7 的 L1 Cache 与 DMA 数据一致性:Cache 维护操作踩坑与实测对比
1. 为什么 Cache 会成为 DMA 的“猪队友”?
STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 L1 Cache(指令 Cache 与数据 Cache,各 16KB)。默认情况下,数据 Cache 是开启的。
CPU 访问内存时,数据会被缓存到 L1 Cache 中。当 CPU 写数据时,可能只更新了 Cache 而未写回物理内存(Write-Back 模式);当 CPU 读数据时,可能直接从 Cache 中拿到旧数据,而物理内存已被 DMA 更新。
DMA 控制器则直接访问物理内存(SRAM、外设 FIFO),它“看不见”Cache。因此,当 CPU 与 DMA 共享同一块内存区域时,就会出现数据不一致:
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 中,DMA 从物理内存读到的却是旧值。
- DMA 写,CPU 读:DMA 将新数据写入物理内存,但 CPU 读到的却是 Cache 中的旧数据。
解决这一问题的核心就是 Cache 维护操作:Clean(写回)和 Invalidate(无效化)。
2. Cache 维护操作原理
Cortex-M7 提供了以下关键维护指令,CMSIS 中封装为函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中指定地址范围的数据写回物理内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):将 Cache 中指定地址范围的数据标记为无效,下次读取时从物理内存重新加载。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效化。
正确使用原则:
- CPU 写 → DMA 读:在启动 DMA 传输前,对内存区域执行 Clean 操作,确保数据已写入物理内存。
- DMA 写 → CPU 读:在 DMA 传输完成后,对内存区域执行 Invalidate 操作,丢弃 Cache 中的旧数据。
- 双向传输:使用 CleanInvalidate。
3. 配置步骤与完整代码示例
3.1 内存对齐与 MPU 配置
Cache 维护操作要求地址和大小按 Cache Line(32 字节)对齐。建议将 DMA 缓冲区放在非缓存区域或使用 MPU 配置为 Write-Through 模式,但最通用的方法还是手动维护。
// 定义 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];
3.2 发送数据(CPU 写,DMA 读)
// 填充数据
for (int i = 0; i < 256; i++) {
dma_buffer[i] = i;
}
// 关键:Clean 操作,将 Cache 数据写回物理内存
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, 256);
// 启动 DMA 传输
HAL_UART_Transmit_DMA(&huart1, dma_buffer, 256);
3.3 接收数据(DMA 写,CPU 读)
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_buffer, 256);
// 等待传输完成(中断或轮询)
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
// 关键:Invalidate 操作,丢弃 Cache 中的旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, 256);
// 此时读取 dma_buffer 才能得到 DMA 写入的新数据
process_data(dma_buffer, 256);
3.4 双向传输
SCB_CleanInvalidateDCache_by_Addr((uint32_t*)dma_buffer, 256);
4. 实测对比:踩坑与验证
4.1 错误场景:忘记 Clean
// 错误示范:未 Clean 直接启动 DMA
for (int i = 0; i < 256; i++) dma_buffer[i] = i;
HAL_UART_Transmit_DMA(&huart1, dma_buffer, 256);
实测结果:串口助手收到的数据部分正确、部分为随机值。原因是部分数据仍在 Cache 中未写回,DMA 读到的是旧数据。
4.2 错误场景:Invalidate 时机错误
// 错误示范:在 DMA 传输过程中 Invalidate
HAL_UART_Receive_DMA(&huart1, dma_buffer, 256);
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, 256); // 过早
实测结果:数据丢失或错乱。Invalidate 会丢弃 Cache 中可能尚未写回的数据,且 DMA 仍在写入,导致竞争。
4.3 正确场景:按序维护
// 发送:先 Clean 再启动 DMA
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, 256);
HAL_UART_Transmit_DMA(&huart1, dma_buffer, 256);
// 接收:先启动 DMA,传输完成后再 Invalidate
HAL_UART_Receive_DMA(&huart1, dma_buffer, 256);
// ... 等待完成 ...
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, 256);
实测结果:数据传输稳定,误码率为 0。
5. 注意事项与最佳实践
- 地址对齐:缓冲区必须按 32 字节对齐,大小建议为 32 的整数倍,否则维护操作可能覆盖相邻数据。
- 避免频繁维护:Cache 维护操作耗时,尽量批量处理,减少调用次数。
- MPU 配置:可将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,从根本上避免一致性问题,但会牺牲部分性能。
- 中断与 DMA 竞争:在 DMA 传输完成中断中执行 Invalidate 是安全的,但需确保此时 CPU 未访问该缓冲区。
- 多缓冲区策略:使用双缓冲区(Ping-Pong)可减少维护操作对性能的影响。
- 调试技巧:若数据异常,可先关闭 D-Cache 验证是否为一致性问题。
6. 总结
STM32H7 的 L1 Cache 是性能利器,但也是 DMA 应用的“暗礁”。理解 Clean 与 Invalidate 的语义,严格遵循“写前 Clean,读后 Invalidate”的原则,并注意对齐与时机,才能确保数据一致性。实测表明,正确的 Cache 维护操作可完全消除 DMA 传输错误,让 H7 的性能与可靠性兼得。