一、为什么 Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核包含 L1 D-Cache(数据缓存)和 I-Cache。CPU 读写内存时,数据可能暂存在 Cache 中,并未立即写回物理内存(SRAM/SDRAM)。而 DMA 控制器直接访问物理内存,不经过 Cache。
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 里,DMA 从物理内存读到旧值。
- DMA 写,CPU 读:DMA 更新了物理内存,但 CPU 读的是 Cache 中的旧数据。
这就是 Cache 一致性(Coherency) 问题。解决思路有两种:
- 软件维护:在 DMA 传输前后手动 Clean/Invalidate Cache。
- 硬件属性:通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,让 CPU 访问直接穿透到物理内存。
二、MPU 配置:给 DMA 缓冲区“开小灶”
MPU(Memory Protection Unit)可以按地址区域设置内存属性。推荐将 DMA 缓冲区放在 0x30000000(SRAM1/2/3) 或 0x24000000(AXI SRAM),并配置为:
- Non-Cacheable, Non-Shareable:最简单,CPU 和 DMA 都直接访问物理内存,无需 Clean/Invalidate。
- Write-Through, No Write Allocate:CPU 写操作同时更新 Cache 和物理内存,读操作可缓存。适合读多写少的 DMA 缓冲区。
以下为 MPU 配置示例(使用 HAL 库):
#include "stm32h7xx_hal.h"
// 假设 DMA 缓冲区位于 0x30000000,大小 32KB
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x8000
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 DMA 缓冲区区域:Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:非缓存
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 可选:将其他 SRAM 区域配置为 Write-Through
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 区域必须按 2 的幂次对齐,且大小与基地址匹配。修改 MPU 后需调用
SCB_EnableDCache()等函数重新使能 Cache。
三、Clean 与 Invalidate:软件维护的利器
如果不想用 MPU,或者缓冲区必须可缓存,则需在 DMA 传输前后调用 CMSIS 函数:
- Clean:将 Cache 中已修改的数据写回物理内存。用于 CPU 写 → DMA 读 之前。
- Invalidate:丢弃 Cache 中的旧数据,强制从物理内存重新加载。用于 DMA 写 → CPU 读 之前。
- Clean+Invalidate:两者都做,用于双向传输或不确定方向时。
#include "core_cm7.h"
// 发送前:确保 CPU 写入的数据到达物理内存
void DMA_SendPrepare(uint8_t *buf, uint32_t len)
{
SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
// 然后启动 DMA 发送
}
// 接收后:丢弃 Cache 旧数据,让 CPU 读到 DMA 写入的新数据
void DMA_ReceiveComplete(uint8_t *buf, uint32_t len)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
// 然后 CPU 处理数据
}
关键细节:
- 地址必须 32 字节对齐,长度建议按 32 字节向上取整。
- Invalidate 前必须确保 DMA 已停止,否则可能丢失数据。
- 若缓冲区跨越 Cache Line(32 字节),需处理相邻数据被误 Clean/Invalidate 的问题。
四、完整实战:DMA 串口接收 + Cache 维护
以 UART DMA 接收为例,展示完整流程:
#define RX_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 1. 配置 MPU 将 rx_buf 所在区域设为 Non-Cacheable(推荐)
// 或跳过 MPU,使用下面的 Clean/Invalidate 方式
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 若未配置 MPU,必须 Invalidate Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
// 此时 CPU 读取 rx_buf 才是 DMA 写入的最新数据
ProcessData(rx_buf, RX_BUF_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
五、注意事项与避坑指南
- 优先用 MPU:将 DMA 缓冲区设为 Non-Cacheable 最省心,避免频繁 Clean/Invalidate 带来的性能开销。
- 对齐:所有 Cache 操作地址必须 32 字节对齐,长度按 32 字节倍数处理。
- Invalidate 风险:若缓冲区部分数据在 Cache 中未写回,Invalidate 会丢弃它们。务必先 Clean 再 Invalidate。
- 多缓冲区:使用多个 DMA 缓冲区时,每个缓冲区独立维护 Cache。
- 调试:开启 Cache 后,调试器查看变量可能看到旧值,需手动 Clean 或关闭 Cache 调试。
- 性能权衡:Non-Cacheable 会降低 CPU 访问速度,对大数据量频繁访问的场景,可考虑 Write-Through 模式。
掌握 MPU 配置与 Clean/Invalidate 的配合,你就能在 STM32H7 上放心使用 DMA,不再被“灵异数据”困扰。