一、现象:DMA 数据为何“时对时错”?
在 STM32H7 上使用 DMA 搬运 ADC 采样、串口数据或外设缓冲区时,常遇到以下现象:
- 单步调试时数据完全正确,全速运行却随机出错;
- 关闭 D-Cache 后一切正常,开启后 DMA 目标缓冲区数据错乱;
- 发送 DMA 数据时,源缓冲区内容被“旧值”覆盖,或接收数据部分丢失;
- 使用
SCB_InvalidateDCache后偶尔出现更严重的错误。
这些现象的根本原因在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问内存时缺乏一致性。CPU 通过 Cache 读写,DMA 直接访问物理内存,两者看到的数据可能不同步。
二、原理:为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate) 策略。当 CPU 写数据时,数据先写入 Cache,并不立即写回主存;当 CPU 读数据时,若 Cache 未命中,则从主存加载到 Cache。
DMA 则绕过 Cache,直接读写主存。因此:
- CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到的是旧的主存数据。
- DMA 写 → CPU 读:DMA 写入新数据到主存,但 CPU 可能从 Cache 中读到旧数据。
-
Cache 行(Cache Line):M7 的 Cache 行大小为 32 字节。若 DMA 缓冲区未按 32 字节对齐,
Invalidate操作可能丢弃相邻变量,导致数据损坏。
三、排查流程:从现象到 MPU 配置
3.1 确认问题是否由 Cache 引起
- 临时关闭 D-Cache(
SCB_DisableDCache()),若问题消失,则基本确定是 Cache 一致性问题。 - 注意:关闭 Cache 会降低性能,仅用于验证。
3.2 检查 DMA 缓冲区属性
- 缓冲区是否定义在可 Cache 区域(如 DTCM、AXI SRAM)?
- 是否按 32 字节对齐?使用
__attribute__((aligned(32)))。 - 是否使用了
volatile?对 DMA 缓冲区,volatile只能防止编译器优化,不能解决 Cache 一致性。
3.3 使用 Cache 维护操作(临时方案)
-
DMA 发送前:调用
SCB_CleanDCache_by_Addr()将 CPU 写入的数据写回主存。 -
DMA 接收后:调用
SCB_InvalidateDCache_by_Addr()丢弃 Cache 中的旧数据,强制从主存重新加载。 - 注意:
Invalidate操作必须按 32 字节对齐,且不能作用于包含其他变量的 Cache 行。
// 示例:DMA 发送前清理 Cache
#define BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];
void DMA_Send(void) {
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, BUFFER_SIZE);
HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->DR, BUFFER_SIZE);
}
// 示例:DMA 接收后无效化 Cache
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];
void DMA_ReceiveCompleteCallback(void) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);
// 此时 CPU 读取 rx_buffer 才能获得 DMA 写入的新数据
}
3.4 终极方案:MPU 配置为 Non-Cacheable
频繁调用 Cache 维护函数会增加 CPU 开销,且容易遗漏。更优雅的方式是使用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through。
以 STM32H7 为例,将 AXI SRAM 的某一段(如 0x24000000 ~ 0x2400FFFF)配置为 Non-Cacheable:
#include "stm32h7xx_hal.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.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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);
}
配置后,该区域所有访问都绕过 D-Cache,DMA 与 CPU 看到的内存完全一致,无需再手动维护 Cache。
四、完整示例:ADC + DMA + D-Cache 安全采集
#define ADC_BUFFER_SIZE 256
__attribute__((aligned(32))) uint16_t adc_buffer[ADC_BUFFER_SIZE];
void ADC_DMA_Init(void) {
// 假设已配置好 ADC 和 DMA,DMA 目标为 adc_buffer
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
}
// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
// 若未使用 MPU Non-Cacheable,必须无效化 Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, ADC_BUFFER_SIZE * 2);
// 此时读取 adc_buffer 才是 DMA 写入的最新值
uint16_t value = adc_buffer[0];
// ... 处理数据
}
五、注意事项与避坑指南
-
对齐:所有 DMA 缓冲区必须 32 字节对齐,否则
Invalidate会破坏相邻数据。 -
Cache 行共享:若多个变量位于同一 Cache 行,
Invalidate会丢弃其他变量的修改,建议将 DMA 缓冲区单独放在一个区域。 -
MPU 配置时机:在
main()初始化阶段、使能 Cache 之前配置 MPU。 - 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,适合 DMA 缓冲区,不适合频繁 CPU 读写的数据。
- 多核场景:STM32H7 双核(CM4 + CM7)共享内存时,还需考虑核间通信的 Cache 一致性。
-
调试技巧:使用
SCB_InvalidateDCache()全无效化可快速验证,但会严重影响性能,仅用于调试。
六、总结
STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典问题。排查时先关闭 Cache 确认现象,再检查缓冲区对齐与属性,临时可用 Cache 维护函数,最终推荐使用 MPU 将 DMA 区域配置为 Non-Cacheable。理解 Cache 行、写回策略与 MPU 机制,才能写出既高效又稳定的代码。