STM32H7 的 D-Cache 与 DMA 一致性:用 MPU 配置 Non-Cacheable 区域的正确姿势
STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,当开发者启用 D-Cache 后,DMA 传输的数据却常常“不翼而飞”或“面目全非”。这背后是 Cache 与 DMA 的一致性问题。本文将带你从原理到实践,彻底解决这一痛点。
一、问题根源:Cache 与 DMA 的“视角差异”
CPU 通过 D-Cache 访问内存,而 DMA 直接访问物理内存。当 CPU 写数据到 Cache 后,若未及时回写到物理内存,DMA 读取的仍是旧数据;反之,DMA 写入新数据到物理内存,CPU 却可能从 Cache 中读到旧值。
- 写穿透(Write-Through):CPU 写 Cache 时同步写内存,但 STM32H7 默认是写回(Write-Back),无法避免不一致。
- 读分配(Read-Allocate):CPU 读内存时先加载到 Cache,DMA 更新内存后 Cache 不会自动失效。
传统解决方案是手动调用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr(),但频繁调用影响性能且易遗漏。更优雅的方式是利用 MPU 将 DMA 缓冲区配置为 Non-Cacheable。
二、MPU 配置 Non-Cacheable 区域原理
MPU(Memory Protection Unit)允许将特定内存区域属性设置为 Non-Cacheable。当 CPU 访问该区域时,直接读写物理内存,不经过 D-Cache,从而天然保证与 DMA 的一致性。
STM32H7 的 MPU 支持 16 个区域,每个区域可独立配置基地址、大小、权限和内存属性。关键属性位:
- TEX=0, C=0, B=0:Non-Cacheable,Normal memory。
- S=1:共享属性,确保多主机(CPU 与 DMA)看到一致数据。
注意:MPU 区域必须按 32 字节对齐,且大小是 2 的幂(最小 32 字节)。
三、配置步骤
-
确定 DMA 缓冲区地址与大小:例如
uint8_t dma_buffer[256] __attribute__((aligned(32))); -
禁用 MPU 并配置区域:调用
HAL_MPU_Disable(),然后填充MPU_Region_InitTypeDef结构体。 -
使能 MPU:调用
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT)。 -
确保缓冲区地址在配置区域内:通常放在
0x24000000(AXI SRAM)或0x30000000(SRAM1-3)。
四、完整代码示例
以下代码将 AXI SRAM 的 0x24000000 起始的 64KB 区域配置为 Non-Cacheable,供 DMA 使用。
#include "stm32h7xx_hal.h"
// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[256];
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置区域 0:0x24000000 ~ 0x2400FFFF (64KB) 为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x24000000;
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_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
// 将 dma_buffer 放置在 Non-Cacheable 区域
// 方法:通过链接脚本或直接指定地址
// 此处假设 dma_buffer 已被链接到 0x24000000 区域
// 启动 DMA 传输
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dma_buffer, 256);
while (1) {}
}
关键点:dma_buffer 必须实际位于 0x24000000 区域。可通过修改链接脚本(.ld 文件)将 .dma_buffer 段定位到该地址,或使用 __attribute__((section(".dma_buffer"))) 并配置链接脚本。
五、注意事项
- MPU 区域不能重叠:若多个区域覆盖同一地址,编号高的优先,但建议避免重叠。
-
Cache 维护操作:即使配置为 Non-Cacheable,若之前该区域被缓存过,仍需调用
SCB_InvalidateDCache_by_Addr()清理旧数据。 - 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区设为 Non-Cacheable,其他代码/数据仍走 Cache。
- 对齐要求:缓冲区地址和大小必须严格对齐,否则 MPU 配置无效。
- 多缓冲区管理:若使用多个 DMA 缓冲区,可合并到一个 MPU 区域,或分别配置。
-
调试技巧:若数据仍异常,检查 MPU 配置是否生效(读取
MPU->RBAR等寄存器),并确认链接脚本地址。
通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是从根源上解决 Cache 一致性问题的优雅方案。掌握这一技能,你的 STM32H7 应用将既拥有 Cache 的高性能,又具备 DMA 的可靠性。