一、为什么 D-Cache 和 DMA 会打架?
STM32H7 主频高达 480MHz,为了匹配 CPU 速度,Cortex-M7 内核配备了 L1 D-Cache(数据缓存)。CPU 访问内存时,数据会先被加载到 Cache 中,后续读写直接命中 Cache,从而大幅提升性能。
但 DMA(直接存储器访问)是独立于 CPU 的外设,它直接读写物理内存(如 SRAM、SDRAM),不经过 D-Cache。这就导致:
- CPU 写数据到内存:数据可能还停留在 Cache 中(写回模式),DMA 读到的是旧数据。
- DMA 写数据到内存:新数据已写入物理内存,但 CPU 读到的却是 Cache 中的旧数据。
这就是经典的 Cache 一致性(Coherency)问题。
二、Cache 工作模式与操作原语
Cortex-M7 的 D-Cache 支持写回(Write-Back)和写通(Write-Through),通常使用写回模式。操作原语由 CMSIS 提供:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回物理内存(Clean)。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 中的内容,强制下次读取从物理内存加载(Invalidate)。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。
关键点:操作地址必须 32 字节对齐,长度建议向上取整到 32 字节边界。
三、精确操作时机(核心)
根据 DMA 传输方向,操作时机截然不同:
3.1 CPU 发送数据 → DMA 读取(如 UART TX、SPI TX)
- CPU 准备好数据缓冲区。
-
在启动 DMA 之前,对缓冲区执行
SCB_CleanDCache_by_Addr,确保数据写入物理内存。 - 启动 DMA。
- DMA 传输完成中断中,无需 Invalidate(因为 CPU 不再读该缓冲区)。
3.2 DMA 接收数据 → CPU 读取(如 UART RX、ADC)
- 启动 DMA 之前,对接收缓冲区执行
SCB_InvalidateDCache_by_Addr,丢弃可能存在的旧 Cache 行。 - 启动 DMA。
- DMA 传输完成中断中,再次执行
SCB_InvalidateDCache_by_Addr,确保 CPU 读到的是 DMA 写入的新数据。
3.3 双向传输(如 SPI 全双工)
- 发送缓冲区:启动前 Clean。
- 接收缓冲区:启动前 Invalidate,完成后再次 Invalidate。
四、完整代码示例
以下以 UART DMA 接收为例,展示正确操作。
#include "stm32h7xx.h"
#define RX_BUF_SIZE 128
// 必须 32 字节对齐
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void uart_dma_start(void)
{
// 1. 启动前 Invalidate,丢弃旧 Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 传输完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 3. 完成后再次 Invalidate,确保 CPU 读到新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
// 4. 安全处理数据
process_data(rx_buf, RX_BUF_SIZE);
}
发送示例:
__attribute__((aligned(32))) uint8_t tx_buf[64];
void uart_dma_send(void)
{
// 填充数据
fill_tx_data(tx_buf, 64);
// Clean:将 Cache 数据写回物理内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 64);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, 64);
}
五、踩坑记录
-
坑 1:缓冲区未对齐。使用
SCB_InvalidateDCache_by_Addr时地址非 32 字节对齐,导致 HardFault 或误清除相邻数据。务必用__attribute__((aligned(32)))。 - 坑 2:Invalidate 时机错误。在 DMA 传输过程中 Invalidate,可能丢弃 DMA 已写入但尚未被 CPU 读取的数据,导致数据丢失。
- 坑 3:忘记 Clean 导致发送乱码。CPU 写数据后未 Clean,DMA 读到旧值,串口输出乱码。
-
坑 4:使用
SCB_InvalidateDCache()全局操作。全局 Invalidate 会清掉所有 Cache,严重影响性能,且可能破坏其他变量。务必使用按地址操作版本。 - 坑 5:DMA 描述符或链表未对齐。若使用 MDMA 或链表模式,描述符本身也需对齐并 Clean。
六、最佳实践与注意事项
- 优先使用 MPU 配置:将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,可从根本上避免一致性问题,但会牺牲部分性能。
- 缓冲区对齐:所有 DMA 缓冲区必须 32 字节对齐,长度向上取整到 32 字节。
- 操作范围:Clean/Invalidate 的范围应覆盖整个缓冲区,不要多也不要少。
- 中断优先级:DMA 完成中断中执行 Cache 操作,注意不要被更高优先级中断打断导致时序混乱。
- 调试技巧:若怀疑 Cache 问题,可临时关闭 D-Cache 验证,但正式产品不建议关闭。
七、总结
STM32H7 的 D-Cache 与 DMA 共存是一把双刃剑。掌握 Clean 和 Invalidate 的精确时机——发送前 Clean,接收前后 Invalidate——就能避开绝大多数数据一致性问题。结合 32 字节对齐和 MPU 配置,你的嵌入式系统将既高效又稳定。