STM32H7 的 DCache 与 DMA 数据一致性排查:从 Cache 维护到 MPU 配置的完整路径
1. 问题现象与根因
在 STM32H7 上启用 DCache 后,DMA 传输经常出现以下诡异现象:
- DMA 发送缓冲区数据部分正确、部分为旧值
- DMA 接收完成后,CPU 读到的数据仍是传输前的旧内容
- 偶发 HardFault,地址看似合法
根因在于 Cortex-M7 的 DCache 与 DMA 控制器访问内存的路径不同:
- CPU 读写可能命中 Cache,不直接落到 SRAM
- DMA 直接访问 SRAM,绕过 Cache
因此当 CPU 写数据到 Cache 但未回写 SRAM 时,DMA 读到的是旧数据;当 DMA 写入新数据到 SRAM,而 CPU 读的是 Cache 中的旧副本时,CPU 读到旧数据。
一句话:Cache 与 DMA 看到的内存不是同一份。
2. 三种解决思路对比
| 方案 | 原理 | 适用场景 | 性能影响 | |------|------|----------|----------| | 手动维护 | 传输前后 Clean/Invalidate | 临时缓冲区、小数据量 | 每次传输有开销 | | MPU Write-Through | 写操作同时更新 SRAM | DMA 发送缓冲区 | 写性能略降 | | MPU Non-Cacheable | 该区域完全绕过 Cache | DMA 收发缓冲区 | 读写均变慢 |
推荐做法:为 DMA 缓冲区单独划分 MPU Non-Cacheable 区域,一劳永逸。
3. 方案一:手动 Cache 维护
3.1 关键 API
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
3.2 使用规则
-
DMA 发送前:CPU 写完数据 →
Clean,把 Cache 刷到 SRAM -
DMA 接收后:
Invalidate,丢弃 Cache 旧副本,强制从 SRAM 重读 -
DMA 接收前:若缓冲区可能被 Cache 缓存,先
Invalidate
3.3 完整示例(UART DMA 发送)
#define TX_BUF_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[TX_BUF_SIZE];
void uart_dma_send(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
/* 关键:发送前 Clean,确保 SRAM 数据最新 */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
3.4 注意事项
- 地址必须 32 字节对齐,长度建议按 32 字节向上取整
- 不要对同一缓冲区同时 Clean 和 Invalidate 造成竞争
- 传输中不要修改缓冲区,否则 Cache 与 DMA 再次不一致
4. 方案二:MPU 配置 Non-Cacheable 区域
4.1 原理
通过 MPU 将某段 SRAM 标记为 Non-Cacheable,CPU 访问该区域时直接读写 SRAM,与 DMA 完全一致,无需手动维护。
4.2 配置步骤
- 在链接脚本中预留专用段,例如
0x30000000起 32KB - 使能 MPU 并配置 Region
- 将该段变量放入指定 section
4.3 完整代码
#include "stm32h7xx_hal.h"
/* 定义 DMA 专用段,放在 SRAM 的 0x30000000 */
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[1024];
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.BaseAddress = 0x30000000;
MPU_Init.Size = MPU_REGION_SIZE_32KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键 */
MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
链接脚本片段(GCC):
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
4.4 注意事项
- MPU 配置必须在
main()早期、任何 DMA 使用前完成 - 区域大小与基地址必须对齐(32KB 区域基地址需 32KB 对齐)
- 若使用 D2 SRAM,注意时钟使能
__HAL_RCC_D2SRAM1_CLK_ENABLE()
5. 方案三:Write-Through 折中
若 DMA 缓冲区读写频繁,可配置为 Write-Through:
MPU_Init.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0; /* Write-Through */
写操作同时更新 Cache 和 SRAM,DMA 发送安全;但 DMA 接收仍需 Invalidate。
6. 排查清单
遇到 DMA 数据异常时,按顺序检查:
- [ ] DCache 是否已使能(
SCB_EnableDCache()) - [ ] DMA 缓冲区是否 32 字节对齐
- [ ] 发送前是否 Clean,接收后是否 Invalidate
- [ ] MPU 区域是否覆盖缓冲区地址
- [ ] 链接脚本段是否真正映射到目标 SRAM
- [ ] 是否在 DMA 传输中修改了缓冲区
7. 总结
STM32H7 的 DCache 是性能利器,但必须与 DMA 协同。小数据量用手动 Clean/Invalidate,大数据流用 MPU Non-Cacheable 区域,是工程上最稳妥的组合。理解“Cache 与 DMA 看到的内存不是同一份”这一本质,所有现象都能迎刃而解。