STM32H7 D-Cache与DMA冲突实战:数据错乱排查与MPU配置详解
一、问题现象:DMA传输数据错乱
在 STM32H7 项目中使用 DMA 搬运 ADC 采样数据或串口收发时,常遇到以下现象:
- DMA 目标缓冲区数据部分正确、部分为旧值或随机值
- 串口发送的数据与预期不符,接收数据丢失
- 关闭 D-Cache 后问题消失,开启后复现
根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器访问内存时缺乏一致性。
二、原理剖析:为什么会产生冲突
2.1 D-Cache 的工作机制
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 写数据时先写 Cache,标记为脏(Dirty),稍后才写回主存;CPU 读数据时若 Cache 未命中,则从主存加载到 Cache。
2.2 DMA 与 Cache 的不一致
DMA 控制器直接访问主存(SRAM),不经过 Cache。因此:
- CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中未写回,DMA 读到的是旧数据。
- DMA 写、CPU 读:DMA 将新数据写入主存,但 CPU 读到的可能是 Cache 中的旧副本。
2.3 典型场景
// 错误示例:DMA 发送,CPU 填充数据
uint8_t tx_buf[256];
void send_data(void) {
for (int i = 0; i < 256; i++) tx_buf[i] = i;
HAL_UART_Transmit_DMA(&huart1, tx_buf, 256); // DMA 可能读到旧数据
}
三、排查步骤
-
确认问题是否与 Cache 相关:在
main()开头调用SCB_DisableDCache(),若问题消失,则确认为 Cache 一致性导致。 - 定位 DMA 缓冲区地址:检查缓冲区是否位于可 Cache 区域(如 DTCM 默认不经过 Cache,AXI SRAM 经过 Cache)。
- 检查 MPU 配置:确认是否对 DMA 缓冲区所在区域配置了正确的内存属性。
- 使用调试器观察:在 DMA 传输前后查看内存与 Cache 内容,验证数据是否同步。
四、解决方案:MPU 配置与 Cache 维护
4.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable
通过 MPU 将 DMA 使用的内存区域设置为 Non-Cacheable,一劳永逸。
步骤:
- 在链接脚本中为 DMA 缓冲区分配独立段,例如
.dma_buffer。 - 配置 MPU 区域,属性设为
Normal, Non-Cacheable。 - 使能 MPU 和 D-Cache。
链接脚本示例(STM32H7 的 AXI SRAM 区域):
.dma_buffer (NOLOAD) : {
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D1
C 代码定义缓冲区:
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_tx_buf[256];
MPU 配置函数:
#include "stm32h7xx_hal.h"
void MPU_Config(void) {
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 DMA 缓冲区区域为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24000000; // AXI SRAM 起始地址
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
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);
}
4.2 方案二:使用 Cache 维护操作
若缓冲区仍需 Cache 加速,可在 DMA 传输前后手动维护 Cache。
发送前(CPU 写,DMA 读):
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, sizeof(dma_tx_buf));
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, sizeof(dma_tx_buf));
接收后(DMA 写,CPU 读):
// DMA 接收完成中断中
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf));
// 之后 CPU 才能安全读取 dma_rx_buf
注意: SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度也建议按 32 字节对齐,否则可能误无效相邻数据。
4.3 方案三:使用 DTCM 内存
STM32H7 的 DTCM(Data Tightly-Coupled Memory)默认不经过 Cache,可直接用于 DMA 缓冲区。但 DTCM 容量有限(通常 128KB),且 DMA 无法访问所有外设(如某些 DMA 不能访问 DTCM)。
五、完整示例:ADC + DMA + D-Cache 正确配置
#include "stm32h7xx_hal.h"
#define ADC_BUF_SIZE 128
__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_dma_buf[ADC_BUF_SIZE];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void System_Init(void) {
HAL_Init();
MPU_Config(); // 先配置 MPU
SCB_EnableICache(); // 使能 I-Cache
SCB_EnableDCache(); // 使能 D-Cache
// ... 时钟、外设初始化
}
void ADC_DMA_Start(void) {
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_BUF_SIZE);
}
// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// 若缓冲区配置为 Non-Cacheable,无需维护 Cache
// 若为 Cacheable,需调用:
// SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, sizeof(adc_dma_buf));
// 安全读取数据
uint32_t sum = 0;
for (int i = 0; i < ADC_BUF_SIZE; i++) {
sum += adc_dma_buf[i];
}
}
六、注意事项与最佳实践
- MPU 配置必须在使能 D-Cache 之前完成,否则可能触发异常。
- DMA 缓冲区地址和长度尽量 32 字节对齐,避免 Cache 维护操作影响相邻数据。
- 避免在 DMA 传输过程中 CPU 访问同一缓冲区,否则仍可能产生竞争。
-
使用
SCB_CleanDCache_by_Addr而非SCB_CleanInvalidateDCache,后者会无效全部 Cache,影响性能。 - 对于频繁小数据 DMA,优先使用 Non-Cacheable 区域,减少维护开销。
-
调试时可用
__DSB()和__ISB()确保内存操作顺序。
七、总结
STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。理解 Cache 一致性原理,通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,或合理使用 Cache 维护函数,即可彻底解决数据错乱问题。推荐在项目初期就规划好内存布局,将 DMA 缓冲区统一放置于 Non-Cacheable 区域,既保证性能又避免隐患。