一、问题现象与背景
某产品基于 STM32F407,使用 DMA 接收串口数据,并启用 D-Cache 提高 CPU 性能。调试时发现:接收数据偶尔出现乱码或丢失,且仅在优化等级较高时出现,逻辑分析仪显示数据正确,但 CPU 读取的缓冲区内容错误。
根因分析
- STM32F4 的 Cortex-M4 内核具有 4KB D-Cache,默认在复位后关闭,但若通过
SCB_EnableDCache()启用,CPU 访问 SRAM 时会先经过 Cache。 - DMA 控制器(如 DMA2)不经过 Cache,直接访问物理 SRAM。
- 当 CPU 读取 DMA 刚写入的数据时,若 Cache 中已有旧数据(脏行),CPU 会读到过期数据;反之,若 CPU 先写入数据再由 DMA 读取(如 DMA 发送),Cache 中的新数据可能未回写,DMA 读到旧数据。
二、解决方案:MPU 配置非缓存区域
MPU(内存保护单元)可以设置内存区域的属性,包括 Cache 策略。将 DMA 缓冲区所在的 SRAM 区域配置为“非缓存(Non-cacheable)”或“写透(Write-through)”,即可保证 CPU 与 DMA 访问的一致性。
配置步骤(基于 CubeMX + HAL 库)
- 在 CubeMX 中启用 MPU:
System Core > MPU,勾选 Enable。 - 添加一个 Region,设置:
- Region Base Address:例如
0x20000000(SRAM1 起始地址) - Region Size:根据需要,如 16KB(覆盖缓冲区)
- TEX=1, C=0, B=0(非缓存)或 TEX=0, C=1, B=1(写透,但推荐非缓存)
- Enable Region 勾选。
- Region Base Address:例如
- 生成代码后,在
main()中调用MPU_Config()(CubeMX 自动生成),并确保在SCB_EnableDCache()之前调用。
代码示例
// 自定义 DMA 缓冲区(必须位于 MPU 配置的区域内)
#define BUF_SIZE 256
__attribute__((section(".ARM.__at_0x20000000"))) uint8_t dma_rx_buf[BUF_SIZE];
// 在 main 函数中初始化顺序:
void main(void) {
HAL_Init();
SystemClock_Config();
// 1. 配置 MPU(必须在使能 D-Cache 之前)
MPU_Config();
// 2. 使能 D-Cache
SCB_EnableDCache();
// 3. 初始化外设和 DMA
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart2, dma_rx_buf, BUF_SIZE);
while (1) {
// 主循环中处理数据
}
}
// CubeMX 生成的 MPU 配置函数(可手动调整)
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 SRAM1 区域为非缓存(地址 0x20000000,大小 16KB)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
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_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
三、注意事项
- MPU 配置必须在使能 D-Cache 之前,否则无效。
- 缓冲区地址必须落在 MPU 配置的区域内,且大小匹配。若使用多个缓冲区,可配置多个 Region 或扩大区域。
- 非缓存区域会降低访问速度,但仅对 DMA 缓冲区影响较小;若追求性能,可使用“写透”策略(TEX=0, C=1, B=1),但需注意写透仍可能产生读不命中,一致性不如非缓存。
- 若使用
__attribute__((section))指定地址,需确保链接脚本中该段未被其他变量占用。 - 对于 DMA 发送(CPU 写,DMA 读),即使配置了非缓存,也建议在启动 DMA 前调用
SCB_CleanDCache()确保数据回写(非缓存区域无需,但写透区域需要)。 - 调试时可用
SCB_InvalidateDCache_by_Addr()强制失效缓存行,但根治方案仍是 MPU 配置。
四、总结
通过 MPU 将 DMA 缓冲区配置为非缓存区域,从根本上避免了 D-Cache 与 DMA 的数据一致性问题。本案例中,配置后串口接收数据稳定,不再出现乱码。对于使用 STM32F4 且启用 D-Cache 的开发者,建议在项目初期就规划好内存区域属性,避免后期排查的困扰。