一、为什么 Cache 会让 DMA 数据“出错”?
STM32H7 的 Cortex-M7 内核带有 16KB 指令 Cache 和 16KB 数据 Cache(L1 Cache)。当 CPU 访问内存时,数据会被缓存到 L1 中;后续访问若命中 Cache,则直接读写 Cache,不再访问实际内存。
而 DMA 控制器是独立于 CPU 的总线主设备,它直接读写物理内存(SRAM、SDRAM 等),不经过 Cache。这就导致两个典型问题:
- CPU 写,DMA 读:CPU 将数据写入 Cache 但尚未回写到内存,DMA 从内存读到旧数据。
- DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读的是 Cache 中的旧数据。
例如,用 DMA 接收串口数据到缓冲区,CPU 去读缓冲区时可能读到全 0 或旧内容,这就是数据一致性问题。
二、三种解决方案对比
| 方案 | 原理 | 优点 | 缺点 | |------|------|------|------| | 禁用 Cache | 关闭 D-Cache | 简单 | 性能损失巨大,H7 失去意义 | | 手动维护 | 调用 SCB_CleanDCache/InvalidateDCache | 灵活 | 易漏、易错,中断中调用有风险 | | MPU 配置 | 将 DMA 缓冲区设为 Write-Through/Non-Cacheable | 一劳永逸,零维护 | 需正确配置 MPU 区域 |
推荐方案:MPU 配置。将 DMA 使用的内存区域配置为 Non-Cacheable 或 Write-Through,CPU 和 DMA 看到的数据始终一致,无需手动 Clean/Invalidate。
三、MPU 配置实战
3.1 确定 DMA 缓冲区地址
假设我们使用 0x30000000 开始的 64KB 区域(例如 SDRAM 或 AXI SRAM)作为 DMA 缓冲区。
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE 0x10000 // 64KB
3.2 配置 MPU 区域
STM32H7 的 MPU 有 16 个区域,每个区域可独立设置属性。我们配置 Region 0 覆盖 DMA 缓冲区。
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable(); // 配置前必须关闭 MPU
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:DMA 缓冲区,Non-Cacheable, Non-Shareable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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);
// 可选:配置其他区域,如 Flash、普通 SRAM 为 Cacheable
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:IsCacheable = MPU_ACCESS_NOT_CACHEABLE 使该区域绕过 D-Cache,CPU 读写直接访问物理内存,与 DMA 天然一致。
3.3 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 前配置
SCB_EnableICache();
SCB_EnableDCache();
// 后续初始化 DMA、外设...
}
四、完整示例:DMA 串口接收 + MPU 保护
#define RX_BUFFER_SIZE 256
__attribute__((section(".dma_buffer"))) // 链接脚本中定位到 0x30000000
uint8_t rx_buffer[RX_BUFFER_SIZE];
void UART_DMA_Init(void)
{
// 假设 huart1 已初始化
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
void process_data(void)
{
// 直接读取 rx_buffer,无需 Clean/Invalidate
for (int i = 0; i < RX_BUFFER_SIZE; i++) {
if (rx_buffer[i] != 0) {
// 处理数据
}
}
}
链接脚本片段(GCC):
MEMORY
{
DMA_RAM (xrw) : ORIGIN = 0x30000000, LENGTH = 64K
}
SECTIONS
{
.dma_buffer :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > DMA_RAM
}
五、注意事项与避坑指南
- MPU 配置必须在使能 Cache 之前,否则可能触发异常或配置不生效。
- 区域大小必须与对齐匹配:基地址需按区域大小对齐(如 64KB 区域,基地址低 16 位为 0)。
- 不要将整个 SRAM 设为 Non-Cacheable,否则性能严重下降。只保护 DMA 缓冲区。
-
若使用 Write-Through 模式:
IsCacheable = MPU_ACCESS_CACHEABLE+IsBufferable = MPU_ACCESS_NOT_BUFFERABLE,可兼顾性能与一致性,但 CPU 写操作会立即写穿到内存,适合读多写少的 DMA 缓冲区。 - 中断中避免手动 Clean/Invalidate:若采用手动维护方案,在中断中操作 Cache 可能因流水线导致数据损坏,MPU 方案无此问题。
- 多缓冲区场景:每个 DMA 缓冲区都应放入 MPU 保护的同一区域,或分别配置多个 MPU 区域。
-
调试时确认 MPU 生效:可通过读取
MPU->RBAR和MPU->RASR验证配置。
六、总结
STM32H7 的 L1 Cache 与 DMA 冲突是高性能嵌入式开发的常见陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从硬件层面根除一致性问题,代码简洁且无需在数据传输前后插入维护指令。建议在项目初期就规划好内存布局,将 DMA 缓冲区集中放置,并正确配置 MPU 区域,从而充分发挥 H7 的性能优势,同时保证数据可靠性。