一、问题现象与根源
在STM32H7上使用DMA搬运数据时,常遇到:
- DMA发送的数据不是最新写入的;
- DMA接收的数据CPU读不到,或读到旧值;
- 偶发数据错位,调试时正常,全速运行出错。
根源在于D-Cache与DMA访问同一块内存时,CPU看到的是Cache中的副本,而DMA直接访问物理内存。当CPU写数据时,若命中Cache且未回写,DMA读到的是旧内存;当DMA写数据时,CPU若命中Cache,读到的仍是旧缓存。
解决思路有三:
- 使用
SCB_CleanDCache_by_Addr/SCB_InvalidateDCache_by_Addr手动维护; - 将DMA缓冲区放到Non-Cacheable区域(推荐,一劳永逸);
- 使用DTCM/ITCM(但DMA无法访问DTCM,需注意)。
本文重点讲解MPU配置Non-Cacheable区域的实操。
二、MPU配置原理
Cortex-M7的MPU(Memory Protection Unit)可将物理地址划分为多个区域,每个区域独立设置属性。关键属性:
- TEX、C、B:决定Cache策略。C=0、B=0、TEX=0 即为Non-Cacheable。
- AP:访问权限,通常设为全访问。
- XN:禁止取指,数据区可设1。
STM32H7的MPU有16个区域,区域0~3可重叠,优先级由区域号决定(编号越大优先级越高)。我们通常将DMA缓冲区所在SRAM区域配置为Non-Cacheable。
三、实操步骤
步骤1:确定DMA缓冲区地址
假设使用D2域SRAM1,起始地址0x30000000,大小32KB。
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x8000 // 32KB
步骤2:配置MPU区域
在系统初始化时(如main开头)调用:
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域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; // C=0
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // B=0
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 可选:配置其他区域,如Flash为Cacheable
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
步骤3:将DMA缓冲区定位到该区域
在链接脚本或代码中指定:
__attribute__((section(".dma_buffer"))) uint8_t dma_buffer[DMA_BUF_SIZE];
并在链接脚本中把.dma_buffer放到0x30000000:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
步骤4:DMA配置与使用
// DMA发送示例
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_buffer, (uint32_t)&UART->TDR, DMA_BUF_SIZE);
此时CPU写dma_buffer会直接写内存,DMA读到的就是最新数据。
四、完整代码示例
#include "stm32h7xx_hal.h"
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x8000
__attribute__((section(".dma_buffer"))) uint8_t dma_buffer[DMA_BUF_SIZE];
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
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);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
// 初始化DMA、UART等
// ...
for (int i = 0; i < DMA_BUF_SIZE; i++) {
dma_buffer[i] = i & 0xFF;
}
// 启动DMA发送
HAL_DMA_Start(&hdma_uart_tx, (uint32_t)dma_buffer, (uint32_t)&huart1.Instance->TDR, DMA_BUF_SIZE);
while (1) {}
}
五、注意事项
- MPU区域对齐:基地址必须按区域大小对齐,如32KB区域基地址需32KB对齐。
-
Cache维护:若无法使用Non-Cacheable,务必在DMA传输前后调用
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr。 -
DTCM限制:DTCM地址
0x20000000无法被DMA访问,不要将DMA缓冲区放在此处。 - 多区域重叠:若同一地址被多个MPU区域覆盖,高编号区域生效,注意优先级。
- 性能权衡:Non-Cacheable会降低CPU访问速度,仅对DMA缓冲区使用,其他内存保持Cacheable。
-
调试技巧:若仍异常,可临时关闭D-Cache验证,或使用
__DSB()确保写操作完成。
通过MPU将DMA缓冲区设为Non-Cacheable,可从根本上避免Cache一致性问题,是STM32H7开发中的推荐做法。