STM32H7 的 D-Cache 与 DMA 一致性排查:从 Cache Line 对齐到 MPU 配置的完整避坑指南
一、问题现象与根源
在 STM32H7 上使用 DMA 搬运数据时,常遇到以下现象:
- DMA 发送的数据与内存中实际数据不符;
- DMA 接收的数据看似正确,但 CPU 读取时部分字节为旧值;
- 数据偶尔正确,偶尔错误,与编译器优化等级、变量地址相关。
根源在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问同一块物理内存时,缺乏硬件一致性机制。DMA 直接读写物理内存,而 CPU 可能读写的是 Cache 中的副本。若 Cache 中的数据未及时写回(Clean)或失效(Invalidate),就会产生数据不一致。
二、Cache Line 对齐:一切的基础
Cortex-M7 的 D-Cache 行大小为 32 字节。DMA 传输缓冲区必须按 32 字节对齐,且长度最好是 32 的整数倍。否则,Clean/Invalidate 操作会波及相邻数据,导致意外丢失。
-
对齐声明:使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。 - 长度处理:若长度非 32 整数倍,需手动补齐或使用 MPU 将缓冲区配置为 Write-Through 模式。
// 定义 32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
三、MPU 配置:划定非缓存区域
最稳妥的方案是将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,避免 Cache 介入。STM32H7 的 MPU 支持 16 个区域,可针对特定地址范围设置属性。
配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 禁用 MPU:
MPU->CTRL = 0; - 配置区域基址与属性:
- 基址:
MPU->RBAR = (uint32_t)buf & MPU_RBAR_ADDR_Msk; - 属性:
MPU->RASR = (0x3 << MPU_RASR_AP_Pos) | (1 << MPU_RASR_XN_Pos) | (0x1 << MPU_RASR_TEX_Pos) | ...
- 基址:
- 使能 MPU 与 D-Cache:
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
void MPU_Config(void) {
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buf;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
四、手动 Clean/Invalidate 操作
若无法使用 MPU,则必须在 DMA 传输前后手动维护 Cache:
- 发送前:对发送缓冲区执行 Clean,将 Cache 数据写回内存。
- 接收后:对接收缓冲区执行 Invalidate,丢弃 Cache 中的旧数据。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf, (uint32_t)&periph, DMA_BUF_SIZE);
// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
注意:Invalidate 操作会丢弃未写回的数据,务必确保 CPU 不再需要这些数据。
五、完整代码示例:DMA 串口接收
#include "stm32h7xx_hal.h"
#define RX_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void) {
// 1. MPU 配置(将 rx_buf 区域设为 Non-Cacheable)
MPU_Config();
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 若未使用 MPU,需在此处 Invalidate
// SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
// 处理数据...
}
六、注意事项与避坑总结
- 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,长度最好为 32 的倍数。
- MPU 优先:能配置 MPU 就配置,一劳永逸,避免手动维护 Cache。
- Clean 与 Invalidate 顺序:发送前 Clean,接收后 Invalidate,不可颠倒。
- 避免跨区域:MPU 区域不要重叠,否则行为未定义。
- 调试技巧:若数据错乱,先检查缓冲区地址是否对齐,再确认 MPU 配置是否生效。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,仅对 DMA 缓冲区使用。
通过以上步骤,可彻底解决 STM32H7 上 D-Cache 与 DMA 的一致性问题,让高性能与数据正确性兼得。