STM32H7 MPU配置实战:外部SDRAM划成可缓存/设备内存后,DMA描述符还会不会踩坑?
1. 为什么STM32H7的MPU和DMA容易踩坑?
STM32H7 内核是 Cortex-M7,带有 L1 Cache(I-Cache 和 D-Cache)。D-Cache 能大幅提升 CPU 访问外部 SDRAM 的性能,但也会引入数据一致性问题:CPU 写数据可能停留在 Cache 中,DMA 直接访问物理内存时读到旧数据;反之 DMA 写入新数据,CPU 可能从 Cache 读到旧值。
MPU(Memory Protection Unit)允许我们将内存区域配置为不同的属性:
- Normal, Write-Back, Write-Allocate:可缓存,适合 CPU 频繁读写的数据。
- Normal, Non-Cacheable:非缓存,适合 DMA 缓冲区。
- Device 或 Strongly Ordered:设备内存,禁止缓存和乱序访问,适合外设寄存器。
但仅仅把 SDRAM 划成可缓存/设备内存还不够,DMA 描述符的存放位置同样关键。如果描述符放在可缓存区域,CPU 更新描述符后未清理 Cache,DMA 可能读到旧描述符,导致传输错误。
2. MPU配置步骤(以STM32H743为例)
假设外部 SDRAM 映射在 0xC0000000,大小 32MB。我们计划:
- 前 16MB 作为可缓存区域(存放代码、数据)。
- 后 16MB 作为非缓存区域(存放 DMA 缓冲区、描述符)。
2.1 在CubeMX中配置MPU
- 打开
System Core -> MPU,点击Add添加区域。 - 区域0:Base Address =
0xC0000000,Size = 16MB,TPU = Level 1,Cacheable = Write-Back, Write-Allocate,Shareable = No。 - 区域1:Base Address =
0xC1000000,Size = 16MB,TPU = Level 1,Cacheable = Non-Cacheable,Shareable = Yes(若多主设备访问)。 - 使能 MPU 和 D-Cache。
2.2 手动初始化代码(若不用CubeMX)
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域0:可缓存 SDRAM
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 区域1:非缓存 SDRAM(DMA缓冲区)
MPU_InitStruct.BaseAddress = 0xC1000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3. DMA描述符存放位置的影响
以以太网 DMA 或 MDMA 为例,描述符通常包含控制字、状态字、缓冲区地址等。若描述符放在可缓存区域,CPU 修改描述符后必须执行 SCB_CleanDCache_by_Addr() 将数据写回物理内存,否则 DMA 读到旧内容。同样,DMA 更新描述符状态后,CPU 读取前需 SCB_InvalidateDCache_by_Addr() 无效化 Cache。
最佳实践:将 DMA 描述符放在非缓存区域(如 0xC1000000 起始的 16MB)。这样 CPU 和 DMA 看到的内存完全一致,无需手动维护 Cache,简化代码并避免隐蔽错误。
3.1 示例:以太网DMA描述符定义
// 将描述符放在非缓存区域
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];
在链接脚本中定义 .dma_desc 段到 0xC1000000:
MEMORY
{
RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
SDRAM_NC (xrw) : ORIGIN = 0xC1000000, LENGTH = 16M
}
SECTIONS
{
.dma_desc :
{
. = ALIGN(32);
*(.dma_desc)
} >SDRAM_NC
}
4. 完整代码示例:SDRAM初始化 + MPU + DMA发送
// sdram.c
void SDRAM_Init(void)
{
FMC_SDRAM_CommandTypeDef cmd;
// ... 初始化FMC时序、GPIO等
// 发送命令序列:时钟配置使能、预充电、刷新、模式寄存器
}
// main.c
uint8_t dma_buffer[1024] __attribute__((section(".dma_buffer")));
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
SDRAM_Init();
SCB_EnableICache();
SCB_EnableDCache();
// 填充数据
for (int i = 0; i < 1024; i++) dma_buffer[i] = i;
// 启动DMA传输(假设使用DMA1_Stream0)
HAL_DMA_Start(&hdma_memtomem_dma1_stream0, (uint32_t)dma_buffer, (uint32_t)0xC0000000, 1024);
while (1) {}
}
5. 注意事项与避坑指南
-
Cache维护函数:若描述符或缓冲区在可缓存区域,必须使用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr(),注意地址 32 字节对齐。 - MPU区域重叠:后配置的区域优先级更高,确保非缓存区域覆盖描述符地址。
- DMA突发传输:非缓存区域可能降低性能,但换来了数据一致性。对性能敏感的场景可保留可缓存区域,并严格维护 Cache。
- 多主设备访问:若 SDRAM 被 CPU 和 DMA 同时访问,建议将共享区域设为 Shareable,避免缓存一致性问题。
-
链接脚本对齐:DMA 描述符通常要求 32 字节对齐,链接脚本中需
ALIGN(32)。 -
调试技巧:若 DMA 传输异常,先检查描述符地址是否落在非缓存区域,再用
SCB_InvalidateDCache()后读取状态。
通过合理配置 MPU 并将 DMA 描述符置于非缓存区域,可以彻底避免 Cache 一致性导致的隐蔽 Bug,让 STM32H7 的性能与稳定性兼得。