一、为什么 STM32H7 的 Cache 会坑到 DMA?
STM32H7 的 Cortex-M7 内核带有 L1 D-Cache 和 I-Cache,默认开启。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 控制器直接访问物理内存,它看不到 Cache 里的“脏数据”。
典型翻车场景:
- CPU 写入缓冲区后启动 DMA 发送,DMA 读到的却是旧数据。
- DMA 接收数据到内存,CPU 读到的却是 Cache 中的旧内容。
- 使用
SCB_InvalidateDCache后,未回写的脏数据被直接丢弃,导致数据损坏。
根本原因:Cache 与 DMA 的可见性不一致。解决手段有两种:MPU 配置为 Write-Through/Non-Cacheable,或手动执行 Clean/Invalidate。
二、MPU 配置:从源头规避一致性问题
对于 DMA 缓冲区,推荐用 MPU 将其配置为 Non-Cacheable 或 Write-Through,这样 CPU 和 DMA 看到的内存始终一致。
2.1 配置步骤
- 使能 MPU:
SCB_EnableMPU()前先SCB_EnableICache()和SCB_EnableDCache()。 - 定义 MPU 区域,设置基地址、大小、属性。
- 调用
HAL_MPU_Config()并使能。
2.2 完整代码示例
#include "stm32h7xx_hal.h"
// 假设 DMA 缓冲区位于 D2 SRAM,地址 0x30000000,大小 32KB
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x8000
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域为 Non-Cacheable, Non-Shared
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 区域大小必须是 2 的幂,且基地址对齐到该大小。
三、手动 Clean/Invalidate:灵活但易错
若无法使用 MPU(如缓冲区分散),必须手动维护 Cache。
3.1 核心 API
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 脏数据写回内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重读。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效。
3.2 正确使用时机
-
CPU 写 → DMA 读:先
Clean,再启动 DMA。 -
DMA 写 → CPU 读:DMA 完成后
Invalidate,再读数据。 -
DMA 写 → CPU 改 → DMA 读:先
Invalidate,改完再Clean。
3.3 代码示例:UART DMA 接收
#define RX_BUF_SIZE 128
alignas(32) uint8_t rx_buf[RX_BUF_SIZE]; // 32 字节对齐
void Start_DMA_Receive(void)
{
// 接收前无效化,避免 Cache 旧数据干扰
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 接收完成后再次无效化,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
// 处理数据...
}
四、避坑清单与注意事项
- 地址对齐:Clean/Invalidate 的地址和大小必须 32 字节对齐,否则行为未定义。
- 不要过度无效化:Invalidate 会丢弃未回写的脏数据,若 CPU 刚写过缓冲区,必须先 Clean。
- DMA 传输中禁止操作 Cache:否则可能破坏正在传输的数据。
- MPU 配置后仍需注意:若区域配置为 Write-Through,CPU 写操作会立即写内存,但读仍可能缓存,必要时配合 Invalidate。
- 多缓冲区场景:每个缓冲区单独维护,避免交叉污染。
-
使用
__DSB()和__ISB():在 Cache 操作后插入屏障指令,确保操作完成。
五、总结
STM32H7 的 Cache 与 DMA 一致性没有银弹。首选 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cache,则严格遵循“写后 Clean,读前 Invalidate”的原则,并注意对齐与屏障。掌握这些,你就能避开 90% 的 H7 数据错乱坑。