一、问题现象与背景

某产品基于 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 库)

  1. 在 CubeMX 中启用 MPU:System Core > MPU,勾选 Enable。
  2. 添加一个 Region,设置:
    • Region Base Address:例如 0x20000000(SRAM1 起始地址)
    • Region Size:根据需要,如 16KB(覆盖缓冲区)
    • TEX=1, C=0, B=0(非缓存)或 TEX=0, C=1, B=1(写透,但推荐非缓存)
    • Enable Region 勾选。
  3. 生成代码后,在 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 的开发者,建议在项目初期就规划好内存区域属性,避免后期排查的困扰。