一、问题现象:DMA 数据错乱从何而来
在 STM32H7 上使用 DMA 传输 ADC 采样、串口数据或 SPI 帧时,常遇到以下现象:
- DMA 传输完成中断触发,但缓冲区数据部分为旧值或全 0
- CPU 写入缓冲区后启动 DMA,发送出去的数据却是修改前的
- 数据偶发错误,且与编译器优化等级、内存布局相关
根本原因在于 Cortex-M7 的 D-Cache 与 DMA 控制器访问内存的路径不同。
二、原理:为什么 Cache 会与 DMA 冲突
2.1 两条访问路径
- CPU 路径:CPU 读写内存时,若地址在 Cacheable 区域,数据会先进入 D-Cache,不一定立即写回 SRAM。
- DMA 路径:DMA 控制器直接访问 SRAM,不经过 D-Cache。
因此:
- CPU 写数据到缓冲区 → 数据可能还在 Cache 中,未写入 SRAM → DMA 读到旧数据。
- DMA 写数据到缓冲区 → 数据已写入 SRAM → CPU 读时若 Cache 中有旧副本,则读到旧数据。
2.2 常见但不够优雅的解决方案
- 使用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()手动维护一致性。 - 缺点:调用时机易错、地址对齐要求严格、性能开销大。
2.3 推荐方案:MPU 配置 Non-Cacheable 区域
将 DMA 缓冲区所在的内存区域通过 MPU 配置为 Non-Cacheable,CPU 和 DMA 都直接访问 SRAM,从根本上避免一致性问题。
三、实操步骤:MPU 配置 Non-Cacheable 区域
以 STM32H743 为例,将 0x30000000 开始的 32KB D2 SRAM 配置为 Non-Cacheable,用于 DMA 缓冲区。
3.1 确定内存区域
- D2 SRAM 地址范围:
0x30000000~0x30048000(不同型号略有差异) - 选择其中一段,例如
0x30000000起始的 32KB
3.2 配置 MPU
#include "stm32h7xx.h"
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
/* 配置 Region 0:D2 SRAM 32KB 为 Non-Cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
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);
/* 使能 MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.3 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 D-Cache 之前配置
SCB_EnableICache();
SCB_EnableDCache();
// 后续初始化 DMA 和外设
...
}
3.4 定义 DMA 缓冲区
#define DMA_BUF_SIZE 1024
/* 强制放在 0x30000000 区域,且 32 字节对齐 */
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[DMA_BUF_SIZE];
在链接脚本中添加段:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
四、完整示例:DMA 串口接收
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
void DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_usart1_rx.Instance = DMA1_Stream0;
hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_NORMAL;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}
void Start_Receive(void)
{
HAL_UART_Receive_DMA(&huart1, dma_buffer, DMA_BUF_SIZE);
}
五、注意事项
-
MPU 配置必须在
SCB_EnableDCache()之前,否则可能不生效。 - DMA 缓冲区必须 32 字节对齐,即使配置为 Non-Cacheable,对齐仍能避免其他问题。
- 若使用
SCB_InvalidateDCache_by_Addr,地址必须按 Cache Line(32 字节)对齐,长度也需对齐。 - 不要将整个 SRAM 都设为 Non-Cacheable,会严重降低性能。
- 不同 STM32H7 型号的 SRAM 地址范围不同,请查阅参考手册。
- 使用
__attribute__((section()))时,确保链接脚本正确分配。
六、验证方法
- 在 DMA 传输前后打印缓冲区内容,观察是否一致。
- 使用调试器查看内存窗口,确认数据正确写入 SRAM。
- 对比开启/关闭 D-Cache 的行为,确认问题消失。
通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,是 STM32H7 上解决 Cache 一致性问题最干净、最可靠的方法。掌握这一技能,能让你在高性能嵌入式开发中少走弯路。