一、现象:DMA 数据为何“时好时坏”?
在 STM32H7 上使用 DMA 搬运 ADC 采样、串口数据或外设缓冲区时,常遇到以下诡异现象:
- 发送 DMA 传输完成,但目标内存中的数据部分正确、部分为旧值。
- 接收 DMA 完成后,CPU 读到的数据与示波器/逻辑分析仪看到的不一致。
- 关闭 D-Cache 后一切正常,开启后随机出错。
- 调试时单步执行正常,全速运行出错。
这些现象高度指向同一个根因:D-Cache 与 DMA 的一致性冲突。
二、原理:为什么 Cache 会“骗”了 DMA?
STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存)。CPU 访问内存时,数据可能被缓存在 Cache 中,而不是立即写回物理内存(SRAM)。
- 写操作:CPU 写数据到 Cache,若未配置写穿透(Write-Through),数据可能停留在 Cache 中,物理内存仍是旧值。此时 DMA 从物理内存读取,得到的是旧数据。
- 读操作:DMA 将新数据写入物理内存,但 CPU 之前已缓存了该地址的旧数据,CPU 再次读取时命中 Cache,读到旧值。
DMA 是“外设”,它直接访问物理内存,不经过 Cache。因此,CPU 与 DMA 对同一块内存的访问必须保证一致性。
三、排查流程:从现象到定位
步骤 1:确认是否由 Cache 引起
临时关闭 D-Cache,观察问题是否消失:
// 在 main 初始化早期调用
SCB_DisableDCache();
若问题消失,则基本确定是 Cache 一致性问题。注意:关闭 D-Cache 会降低性能,仅用于验证。
步骤 2:检查 DMA 缓冲区地址与属性
- 确认 DMA 缓冲区是否位于可 Cache 的内存区域(如 AXI SRAM、DTCM)。
- 注意:DTCM 默认不经过 Cache,但 DMA 通常无法访问 DTCM(部分 DMA 控制器不支持)。因此常用 AXI SRAM 作为 DMA 缓冲区,而 AXI SRAM 默认是可 Cache 的。
步骤 3:使用 MPU 配置内存属性
最优雅的方案是用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through。这样 CPU 访问该区域时绕过 Cache,DMA 与 CPU 看到的内存始终一致。
四、MPU 配置实战
以下代码将 AXI SRAM 的 0x24000000 起始的 32KB 区域配置为 Non-Cacheable,供 DMA 使用。
#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 = 0x24000000;
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);
// 区域 1:其余内存保持默认 Cache 属性(可选)
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
在 main() 中,必须在使能 Cache 之前调用 MPU_Config():
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 先配置 MPU
SCB_EnableICache();
SCB_EnableDCache(); // 再使能 Cache
// ...
}
五、替代方案:手动维护 Cache 一致性
若不想划分 MPU 区域,可在 DMA 传输前后手动操作 Cache:
-
DMA 发送前(CPU 写数据后):调用
SCB_CleanDCache_by_Addr()将 Cache 数据写回内存。 -
DMA 接收后(CPU 读数据前):调用
SCB_InvalidateDCache_by_Addr()使 Cache 失效,强制从内存重新加载。
// 发送前清理
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->TDR, len);
// 接收后无效化
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 100);
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
注意:SCB_InvalidateDCache_by_Addr 的地址和长度必须按 Cache Line(32 字节)对齐,否则可能误伤相邻数据。
六、注意事项与避坑指南
- MPU 配置必须在使能 Cache 前完成,否则不生效。
- DMA 缓冲区地址要 32 字节对齐,避免 Cache Line 跨界问题。
- 不要将 DTCM 用作 DMA 缓冲区,除非确认 DMA 控制器支持访问 DTCM(多数不支持)。
-
使用
SCB_InvalidateDCache_by_Addr时,确保没有其他变量与缓冲区共享同一 Cache Line,否则会丢失未写回的数据。 - 若使用 Ethernet、USB 等自带 DMA 的外设,同样需要处理 Cache 一致性,方法相同。
- 调试时若发现“单步正常、全速出错”,优先怀疑 Cache 与 DMA 冲突。
七、总结
STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。排查时先关闭 Cache 验证,再通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,或手动维护 Cache 一致性。推荐优先使用 MPU 方案,一劳永逸且不影响其他内存区域的性能。掌握这一流程,可显著提升 H7 系列开发的稳定性与效率。