一、问题现象:DMA 数据偶发错位
在 STM32H7 项目中,使用 DMA 进行串口/UART 或 SPI 收发时,经常遇到以下现象:
- 接收到的数据偶尔出现前几个字节错位或丢失。
- 发送的数据偶尔被篡改,或 DMA 传输完成但内存中数据未更新。
- 问题在开启 D-Cache 后出现,关闭 Cache 则正常。
根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器访问同一块内存时,数据一致性被破坏。
二、原理:Cache 与 DMA 为何冲突
STM32H7 内核为 Cortex-M7,带有 L1 I-Cache 和 D-Cache。D-Cache 会缓存 CPU 对内存的读写:
- CPU 写数据:先写入 Cache,标记为 dirty,稍后才写回内存。若此时启动 DMA 发送,DMA 从内存读取的是旧数据。
- DMA 写数据:直接写入内存,但 CPU 可能仍从 Cache 读取旧值,导致读到过期数据。
- DMA 读数据:若 CPU 之前写过该区域,数据可能还在 Cache 中未回写,DMA 读到旧内容。
因此,必须通过 MPU 配置内存属性 或 手动维护 Cache 一致性 来解决。
三、MPU 配置步骤
MPU(Memory Protection Unit)可将特定内存区域配置为:
- Non-cacheable:完全绕过 Cache,适合 DMA 缓冲区。
- Write-through:写操作同时更新 Cache 和内存,适合只读或写少读多场景。
- Write-back:默认,需手动 clean/invalidate。
3.1 配置流程
- 使能 MPU:
SCB->CTRL |= SCB_CTRL_MPU_Msk; - 选择区域号(0~15),设置基地址和大小。
- 设置属性:TEX、C、B、S 位。
- 使能该区域并更新。
3.2 关键寄存器
-
MPU->RBAR:基地址 + 区域号 + 有效位。 -
MPU->RASR:大小、属性、使能。 -
MPU->CTRL:使能 MPU。
四、完整代码示例
以下代码将 0x30000000 开始的 32KB 区域配置为 Non-cacheable,用于 DMA 缓冲区。
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000UL
#define DMA_BUFFER_SIZE 0x8000UL // 32KB
void MPU_Config(void)
{
// 禁用 MPU
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置区域 0
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// DMA 发送前:若缓冲区可缓存,需 clean D-Cache
void DMA_SendData(uint8_t *pData, uint16_t len)
{
// 若 MPU 已配置为 Non-cacheable,则无需 clean
// 否则调用:SCB_CleanDCache_by_Addr((uint32_t*)pData, len);
HAL_UART_Transmit_DMA(&huart1, pData, len);
}
// DMA 接收后:若缓冲区可缓存,需 invalidate D-Cache
void DMA_ReceiveData(uint8_t *pData, uint16_t len)
{
// 等待 DMA 完成
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
// 若 MPU 已配置为 Non-cacheable,则无需 invalidate
// 否则调用:SCB_InvalidateDCache_by_Addr((uint32_t*)pData, len);
// 处理数据...
}
五、排查与解决思路
- 确认问题:关闭 D-Cache 后问题消失,基本可判定为 Cache 一致性。
- 定位缓冲区:找到 DMA 使用的内存地址,检查其 MPU 属性。
- 配置 MPU:将 DMA 缓冲区设为 Non-cacheable 或 Shareable。
-
手动维护:若不能改 MPU,则在 DMA 传输前后调用
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr。 - 注意对齐:Cache 操作要求地址 32 字节对齐,长度也需对齐。
六、注意事项
- MPU 区域大小:必须是 2 的幂,且基地址对齐到区域大小。
- 多区域重叠:高优先级区域会覆盖低优先级,注意编号顺序。
- DMA 与 CPU 竞争:即使 Non-cacheable,也要避免 CPU 和 DMA 同时写同一区域。
- 性能权衡:Non-cacheable 会降低 CPU 访问速度,建议仅对 DMA 缓冲区使用。
-
Cache 维护函数:
SCB_CleanDCache_by_Addr写回,SCB_InvalidateDCache_by_Addr无效化,注意不要对未对齐地址使用。 - 中断与 DMA:在 DMA 完成中断中处理数据前,确保已 invalidate 对应 Cache 行。
七、总结
STM32H7 的 Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。通过合理配置 MPU,将 DMA 缓冲区设为 Non-cacheable,或手动维护 Cache 一致性,可彻底解决数据错位问题。建议在项目初期就规划好内存布局,避免后期调试困难。掌握本文方法后,你也能轻松驾驭 STM32H7 的高性能与数据一致性。