一、现象:DMA 数据为何“随机”出错?
在 STM32H7 上开发时,你可能遇到这样的场景:ADC 用 DMA 搬运数据到数组,CPU 读取时数值偶尔跳变;或 UART 发送 DMA 传输字符串,接收端出现乱码。调试时发现,关闭 DCache 后一切正常,开启后问题复现。
根本原因:Cortex-M7 的 DCache 与 DMA 控制器访问内存的路径不同。DMA 直接读写物理内存(SRAM),而 CPU 可能从 Cache 中读取旧数据,或写入的数据还停留在 Cache 未同步到 SRAM。这就是Cache 一致性(Coherency)问题。
二、原理:DCache 如何“捣乱”
- 读操作:CPU 读取某地址时,若 Cache 未命中,会从 SRAM 加载数据到 Cache 行(通常 32 字节)。之后 DMA 更新了 SRAM 中该地址的数据,但 CPU 再次读取时仍命中旧 Cache,得到过时值。
- 写操作:CPU 写入数据到 Cache(写回策略),并未立即写入 SRAM。若此时启动 DMA 发送该缓冲区,DMA 读到的是 SRAM 中的旧内容。
- Cache 行共享:一个 Cache 行包含多个变量,DMA 只更新其中一部分,CPU 读取其他变量时可能触发整行回写,覆盖 DMA 刚写入的数据。
三、排查流程:从快速验证到精准配置
步骤 1:关闭 DCache 验证问题
在 main() 初始化后调用 SCB_DisableDCache(),若问题消失,则确认为 Cache 一致性冲突。
#include "core_cm7.h"
SCB_DisableDCache(); // 仅用于验证,正式产品不建议全局关闭
步骤 2:使用 SCB 维护函数(临时方案)
在 DMA 传输前后手动维护 Cache:
-
DMA 发送前(CPU 写数据):调用
SCB_CleanDCache_by_Addr()将 Cache 写回 SRAM。 -
DMA 接收后(CPU 读数据):调用
SCB_InvalidateDCache_by_Addr()丢弃 Cache,强制从 SRAM 重新加载。
#define BUF_SIZE 64
uint8_t dma_buf[BUF_SIZE] __attribute__((aligned(32))); // 32 字节对齐
// 发送前
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_buf, BUF_SIZE);
// 接收后(在 DMA 完成中断中)
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 此时 CPU 读取 dma_buf 才是最新数据
注意:地址必须 32 字节对齐,长度建议为 32 的整数倍,否则可能误伤相邻数据。
步骤 3:MPU 配置——终极解决方案
手动维护 Cache 易遗漏且影响性能。更优雅的方式是用 MPU(内存保护单元)将 DMA 缓冲区所在内存区域配置为 Write-Through, No Write Allocate 或 Non-Cacheable,让该区域数据不经过 DCache,从根本上避免一致性问题。
3.1 确定缓冲区地址与大小
假设使用 0x30000000 开始的 64KB SRAM(D2 域 SRAM1),定义缓冲区:
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x10000 // 64KB
3.2 配置 MPU 区域
#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 = DMA_BUF_ADDR;
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; // 关键:禁用 Cache
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);
}
调用 MPU_Config() 后,该区域所有访问(CPU 和 DMA)都直接操作 SRAM,无需手动维护 Cache。
3.3 将缓冲区定位到该区域
使用链接脚本或 __attribute__((section(".dma_buffer"))) 将数组放到指定地址。
uint8_t dma_buf[BUF_SIZE] __attribute__((section(".dma_buffer"), aligned(32)));
并在链接脚本中定义:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
四、完整示例:ADC + DMA + DCache 安全读取
#include "stm32h7xx_hal.h"
#define ADC_BUF_SIZE 128
uint16_t adc_buf[ADC_BUF_SIZE] __attribute__((section(".dma_buffer"), aligned(32)));
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void System_Init(void)
{
HAL_Init();
MPU_Config(); // 先配置 MPU
SCB_EnableDCache(); // 再开启 DCache
// ... 初始化时钟、ADC、DMA
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
}
// DMA 完成回调中直接读取,无需 Invalidate
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1) {
// adc_buf 位于 Non-Cacheable 区域,数据已是最新
uint32_t sum = 0;
for (int i = 0; i < ADC_BUF_SIZE; i++) {
sum += adc_buf[i];
}
// 处理 sum...
}
}
五、注意事项与避坑指南
- MPU 区域对齐:基地址必须按区域大小对齐(如 64KB 区域基地址需 64KB 对齐)。
-
Cache 维护函数地址对齐:
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,长度建议为 32 的倍数。 - 避免全局关闭 DCache:会严重降低性能,仅用于调试。
- 多缓冲区场景:若多个 DMA 缓冲区相邻,建议统一放在同一个 Non-Cacheable 区域,避免 Cache 行共享冲突。
- DMA 描述符:若使用链表式 DMA,描述符本身也需放在 Non-Cacheable 区域。
- 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区放入,其他代码和数据仍享受 Cache 加速。
通过以上流程,你可以从现象快速定位到根因,并用 MPU 配置一劳永逸地解决 STM32H7 的 DCache 与 DMA 冲突问题。