一、问题现象:DMA 数据错位
在 STM32H7 项目中,使用 DMA 搬运 ADC 采样、串口收发或 SPI 数据时,经常出现以下现象:
- DMA 发送的数据与内存中实际数据不一致,接收端收到错位或随机字节。
- DMA 接收完成后,CPU 读取缓冲区发现数据部分正确、部分为旧值。
- 单步调试正常,全速运行出错;关闭 Cache 后问题消失。
这些现象几乎都指向同一个根因:Cortex-M7 的 D-Cache 与 DMA 直接访问物理内存之间缺乏一致性维护。
二、原理:Cache 一致性为何成为问题
STM32H7 基于 Cortex-M7 内核,带有 L1 D-Cache 和 I-Cache。CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 控制器直接访问 AXI SRAM 或 DTCM 等物理内存,不经过 Cache。
- DMA 写内存,CPU 读:DMA 将新数据写入物理内存,但 CPU 可能仍从 D-Cache 读到旧数据。
- CPU 写内存,DMA 读:CPU 写入的数据可能还在 D-Cache 中未回写到物理内存,DMA 读到旧数据。
解决方式有两种:
- 将 DMA 缓冲区放在 MPU 配置为 Non-Cacheable 的内存区域,一劳永逸。
- 在 DMA 传输前后手动执行 Cache 清理(Clean)和无效化(Invalidate),但需注意时序和地址对齐。
推荐做法:用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,避免手动维护的复杂性和风险。
三、MPU 配置步骤
MPU(Memory Protection Unit)可将内存区域属性设置为 Normal、Device 或 Strongly-ordered,并控制 Cache 策略。
3.1 确定 DMA 缓冲区地址
假设使用 AXI SRAM 的 0x24000000 起始的 32KB 区域作为 DMA 缓冲区。
3.2 配置 MPU Region
以 HAL 库为例,在 main() 初始化早期调用:
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
/* Region 0: 0x24000000, 32KB, Non-Cacheable, 可共享 */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x24000000;
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_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:
-
IsCacheable = NOT_CACHEABLE和IsBufferable = NOT_BUFFERABLE使该区域绕过 D-Cache。 -
IsShareable = SHAREABLE保证多主机(CPU 与 DMA)之间一致性。 - 区域大小必须与基地址对齐,例如 32KB 区域基地址低 15 位为 0。
3.3 将 DMA 缓冲区定义到该区域
#define DMA_BUF_ADDR 0x24000000
#define DMA_BUF_SIZE 1024
static uint8_t *dma_rx_buf = (uint8_t *)DMA_BUF_ADDR;
或使用链接脚本将 .dma_buffer 段定位到该地址。
四、完整代码示例:UART DMA 接收
以下示例使用 UART4 + DMA1 Stream0 接收不定长数据,缓冲区位于 Non-Cacheable 区域。
#include "stm32h7xx_hal.h"
UART_HandleTypeDef huart4;
DMA_HandleTypeDef hdma_uart4_rx;
#define DMA_BUF_ADDR 0x24000000
#define DMA_BUF_SIZE 256
static uint8_t *rx_buf = (uint8_t *)DMA_BUF_ADDR;
void UART4_Init(void)
{
huart4.Instance = UART4;
huart4.Init.BaudRate = 115200;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart4);
}
void DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_uart4_rx.Instance = DMA1_Stream0;
hdma_uart4_rx.Init.Request = DMA_REQUEST_UART4_RX;
hdma_uart4_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_uart4_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_uart4_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_uart4_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_uart4_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_uart4_rx.Init.Mode = DMA_NORMAL;
hdma_uart4_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_uart4_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_uart4_rx);
__HAL_LINKDMA(&huart4, hdmarx, hdma_uart4_rx);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); /* 必须先配置 MPU */
UART4_Init();
DMA_Init();
HAL_UART_Receive_DMA(&huart4, rx_buf, DMA_BUF_SIZE);
while (1)
{
/* 处理 rx_buf 中的数据,无需手动 Cache 维护 */
}
}
五、排查方法与注意事项
5.1 快速定位
- 关闭 D-Cache 测试:在
SCB_EnableDCache()前加条件编译,若问题消失,则确认是 Cache 一致性导致。 - 检查 MPU 配置是否生效:读取
MPU->RBAR和MPU->RASR确认区域属性。 - 检查 DMA 缓冲区地址是否落在配置的 Non-Cacheable 区域内。
5.2 注意事项
- MPU 配置必须在使能 Cache 之前完成,否则已缓存的数据可能残留。
- DMA 缓冲区大小不能超过 MPU Region 大小,否则超出部分仍走 Cache。
-
不要对 Non-Cacheable 区域调用
SCB_CleanDCache或SCB_InvalidateDCache,虽然无害但无意义,且可能引入额外开销。 -
若必须使用 Cacheable 区域,则需在 DMA 发送前
SCB_CleanDCache_by_Addr,接收后SCB_InvalidateDCache_by_Addr,并确保地址 32 字节对齐。 - DTCM 不支持 DMA,不要把 DMA 缓冲区放在 DTCM(0x20000000)。
- 多缓冲区切换时,每个缓冲区都应位于 Non-Cacheable 区域,或分别维护 Cache。
六、总结
STM32H7 的 Cache 与 DMA 冲突是嵌入式开发中的经典问题。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 和 Shareable,可以从根本上避免数据错位。本文给出的配置步骤和代码可直接用于 UART、SPI、ADC 等 DMA 场景。掌握 MPU 与 Cache 一致性机制,是发挥 H7 高性能的前提。