STM32H7 的 D-Cache 与 DMA 一致性踩坑:用 MPU 配置 Non-Cacheable 区域的正确姿势
1. 问题现象:DMA 数据“不更新”
很多开发者从 F4/F7 迁移到 H7 后,会遇到这样的怪事:
- DMA 接收串口/ADC 数据到缓冲区,CPU 读到的却始终是旧值。
- DMA 发送缓冲区数据,实际发出的内容错乱或只发了一部分。
- 单步调试正常,全速运行就出错。
根本原因:Cortex-M7 的 D-Cache 与 DMA 访问同一块内存时,数据不一致。
2. 原理:为什么会有缓存一致性问题?
- D-Cache 工作方式:CPU 访问内存时,数据被缓存到 D-Cache。写操作可能只更新 Cache(Write-Back),读操作可能直接命中 Cache 而不读内存。
- DMA 绕过 Cache:DMA 控制器直接访问物理内存,不经过 D-Cache。
-
冲突场景:
- CPU 写数据到缓冲区 → 数据在 Cache 中,内存未更新 → DMA 从内存读旧数据发送。
- DMA 从外设搬数据到内存 → 内存已更新 → CPU 读时命中 Cache 旧数据。
解决思路有三种:
- 使用
SCB_CleanDCache_by_Addr()/SCB_InvalidateDCache_by_Addr()手动维护。 - 将 DMA 缓冲区放在 Non-Cacheable 内存区域(推荐)。
- 使用 Write-Through 模式(仍有风险,不推荐)。
本文重点讲解 方法 2:通过 MPU 配置 Non-Cacheable 区域,一劳永逸。
3. MPU 配置步骤
3.1 确定内存区域
STM32H7 的 RAM 分布复杂,常见可用区域:
-
0x24000000:AXI SRAM (512KB),DMA 可访问,推荐。 -
0x30000000:SRAM1/2/3 (288KB),DMA 可访问。 -
0x38000000:SRAM4 (64KB),DMA 可访问。
注意:DTCM (0x20000000) 和 ITCM (0x00000000) DMA 无法访问,不能用作 DMA 缓冲区。
3.2 配置 MPU Region
以 AXI SRAM 中划出 32KB 作为 Non-Cacheable 区域为例:
- 基地址:
0x24000000 - 大小:32KB(2^15)
- 属性:Normal, Non-Cacheable, Shareable
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x24000000UL
#define DMA_BUFFER_SIZE 0x8000UL // 32KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 Region 0 为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_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_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);
}
关键点:
-
IsCacheable = NOT_CACHEABLE是核心。 -
IsShareable = SHAREABLE保证多总线主控(CPU、DMA)看到一致数据。 -
DisableExec = INSTRUCTION_ACCESS_DISABLE防止误取指,提升安全性。
3.3 在链接脚本中保留该区域
确保编译器不会把普通变量分配到该区域,可在链接脚本中定义专用段:
MEMORY
{
AXI_SRAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
}
SECTIONS
{
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
} > AXI_SRAM
}
然后在代码中声明:
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[4096];
4. 完整示例:UART DMA 接收
#include "stm32h7xx_hal.h"
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t uart_rx_buf[256];
void UART_DMA_Init(void)
{
// 1. 使能 DMA 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
// 2. 配置 DMA
hdma_usart1_rx.Instance = DMA1_Stream0;
hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
// 3. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));
}
// 主循环中直接读取 uart_rx_buf,无需 Clean/Invalidate
void Process_UART_Data(void)
{
for (int i = 0; i < sizeof(uart_rx_buf); i++) {
if (uart_rx_buf[i] != 0) {
// 处理数据
}
}
}
5. 注意事项与常见坑
-
DTCM 不能用于 DMA:DTCM 地址
0x20000000虽快,但 DMA 无法访问,必须用 AXI SRAM 或 SRAM1/2/3/4。 - 对齐要求:DMA 缓冲区建议 32 字节对齐,避免 Cache Line 边界问题。
- MPU Region 数量有限:Cortex-M7 只有 16 个 Region,规划好地址范围。
- 不要混用:如果已配置 Non-Cacheable,就不要再手动 Clean/Invalidate,否则可能引入新问题。
- 调试时注意:某些 IDE 的实时变量查看会触发 Cache 操作,可能掩盖问题,建议用内存窗口查看物理地址。
- 多缓冲区场景:若多个 DMA 缓冲区分散,可合并到一个 Region,或使用多个 Region。
-
Cache 维护函数:若必须使用 Cacheable 区域,务必在 DMA 传输前后调用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr(),且地址按 32 字节对齐。
6. 总结
STM32H7 的 D-Cache 是把双刃剑,用好了性能飞升,用不好 bug 缠身。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是最简单、最可靠的方案。记住三句话:
- DMA 缓冲区放 AXI SRAM,别放 DTCM。
- MPU 配 Non-Cacheable + Shareable。
- 配好了就别再手动维护 Cache。
掌握这些,你就能在 H7 上愉快地使用 DMA 了。