STM32H7 的 Cache 与 DMA 一致性实战:MPU 配置与 Clean/Invalidate 时机踩坑记录
STM32H7 系列凭借 480MHz 主频和 L1 Cache 成为高性能嵌入式首选,但 Cache 的引入也让 DMA 传输变得“暗藏杀机”。如果你曾遇到 DMA 发送的数据错乱、接收的数据被覆盖,或者明明内存里数据正确却传输错误,那么本文正是为你准备的。
一、为什么 Cache 会导致 DMA 数据不一致?
Cortex-M7 的 L1 Cache 分为 D-Cache 和 I-Cache。D-Cache 采用**写回(Write-Back)**策略:CPU 写数据时只更新 Cache,不立即写回主存;读数据时若 Cache 未命中,则从主存加载并缓存。
DMA 控制器直接访问主存(AXI SRAM、DTCM 等),不经过 Cache。因此:
- CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,主存仍是旧值,DMA 读到旧数据。
- DMA 写 → CPU 读:DMA 将新数据写入主存,但 CPU 可能从 Cache 中读到旧数据。
解决思路:在 DMA 传输前后,手动维护 Cache 一致性。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
最优雅的方案是利用 MPU 将 DMA 缓冲区配置为非缓存(Non-Cacheable)或写通(Write-Through),从而避免手动 Clean/Invalidate。
2.1 MPU 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;并调用ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk); - 定义区域基址和属性。
- 调用
ARM_MPU_SetRegion()配置。
2.2 示例:将 0x30000000 开始的 64KB 设为 Non-Cacheable
#include "cmsis_compiler.h"
void MPU_Config(void)
{
ARM_MPU_Disable();
// 区域 0:DMA 缓冲区,Non-Cacheable, Non-Shareable
ARM_MPU_SetRegion(
0, // 区域编号
0x30000000, // 基址
ARM_MPU_RASR( // 属性
0, // 禁用指令访问
ARM_MPU_AP_FULL, // 全权限
1, // 可共享?0=Non-shareable
0, // 可缓存?0=Non-cacheable
0, // 可缓冲?0=Non-bufferable
0, // 子区域禁用
ARM_MPU_REGION_SIZE_64KB
)
);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
}
注意:
ARM_MPU_RASR宏参数顺序因 CMSIS 版本略有差异,请以实际头文件为准。
配置后,该区域所有访问都直接读写主存,DMA 与 CPU 看到的数据始终一致。
三、手动 Clean/Invalidate 的正确时机
若无法使用 MPU(如缓冲区动态分配),则必须手动维护 Cache。关键函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回主存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从主存读取。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效。
3.1 发送(CPU → DMA)
CPU 填充缓冲区后,必须 Clean,确保数据写入主存,再启动 DMA。
uint8_t tx_buf[256];
// ... 填充数据 ...
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_DMA_Start(&hdma_memtomem, (uint32_t)tx_buf, (uint32_t)dest, sizeof(tx_buf));
3.2 接收(DMA → CPU)
DMA 传输完成后,必须 Invalidate,丢弃 Cache 中的旧数据,让 CPU 从主存读取新数据。
uint8_t rx_buf[256];
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)rx_buf, sizeof(rx_buf));
// 等待传输完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 现在可以安全读取 rx_buf
致命陷阱:Invalidate 操作会丢弃 Cache 中尚未写回的数据。如果缓冲区在 Invalidate 前被 CPU 写过,这些修改将永久丢失!因此,接收缓冲区在 DMA 传输期间不应被 CPU 写入。
四、完整实战代码:UART DMA 收发
以 UART 空闲中断 + DMA 接收为例,展示完整流程。
#define RX_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE]; // 32 字节对齐
void UART_DMA_Init(void)
{
// ... 初始化 UART 和 DMA ...
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// 空闲中断回调
void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1) {
// 停止 DMA 以获取已接收长度
HAL_UART_DMAStop(huart);
uint16_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 关键:Invalidate Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
// 处理数据
ProcessData(rx_buf, len);
// 重启接收
HAL_UART_Receive_DMA(huart, rx_buf, RX_BUF_SIZE);
}
}
五、踩坑记录与注意事项
-
坑 1:地址未对齐。Clean/Invalidate 要求地址 32 字节对齐,长度也建议对齐。使用
__attribute__((aligned(32)))修饰缓冲区。 - 坑 2:Invalidate 范围过大。若缓冲区与其它变量共享 Cache 行,Invalidate 可能丢弃相邻变量的修改。务必按 Cache 行(32 字节)对齐并精确操作。
- 坑 3:DMA 传输中 CPU 访问缓冲区。传输期间 CPU 读写缓冲区会导致数据竞争,应使用双缓冲或信号量隔离。
-
坑 4:MPU 配置后忘记使能。
ARM_MPU_Enable必须在配置后调用,且需设置PRIVDEFENA使能默认背景区域。 - 坑 5:DTCM 与 Cache。DTCM 不经过 Cache,无需维护一致性,但 DMA 通常无法访问 DTCM(除非通过 AHBS 总线),建议将 DMA 缓冲区放在 AXI SRAM。
六、总结
STM32H7 的 Cache 与 DMA 一致性并非洪水猛兽,核心原则:要么让 DMA 缓冲区绕过 Cache(MPU),要么在正确时机手动 Clean/Invalidate。理解数据流向,严格对齐地址,避免传输期间 CPU 访问,就能写出稳定可靠的代码。希望本文的实战经验能帮你少走弯路。