引言

在 STM32F4 系列(如 STM32F407、STM32F429)中,Cortex-M4 内核集成了可选的 D-Cache(数据缓存)和 I-Cache(指令缓存)。启用 D-Cache 后,CPU 对内存的读写会先经过缓存,而 DMA 外设则直接访问物理内存(SRAM)。这种架构差异导致了一个经典问题:CPU 和 DMA 看到的数据可能不一致。例如,CPU 写入数据后,数据可能仍停留在 Cache 中,尚未写回 SRAM,此时 DMA 读取 SRAM 就会得到旧数据;反之,DMA 更新了 SRAM,CPU 读取时却可能命中过期的 Cache 行。

本文将针对 STM32F4 系列,提供三种实用解法,并附上基于 HAL 库的代码示例。

问题根源:Cache 与 DMA 的“视角”差异

Cortex-M4 的 D-Cache 采用写回(Write-back)策略,即 CPU 写操作只更新 Cache,并在特定时机(如 Cache 行被替换或显式清理)才写回 SRAM。DMA 控制器不经过 Cache,直接读写 SRAM。因此,当 CPU 和 DMA 共享同一块内存区域时,就会发生数据不一致。

典型场景

  • 使用 DMA 发送内存中的数据(如 UART、SPI、以太网)。
  • 使用 DMA 接收数据到内存,再由 CPU 处理。

解法一:软件维护 Cache(清理与失效)

这是最直接的方法,通过操作协处理器 CP15 指令或 CMSIS 提供的函数,在关键操作前后手动同步 Cache 和 SRAM。

原理

  • 清理(Clean):将 Cache 中脏数据写回 SRAM。
  • 失效(Invalidate):丢弃 Cache 中的数据,下次访问时重新从 SRAM 加载。

配置步骤

  1. 启用 D-Cache(通常在系统初始化时)。
  2. 在 DMA 发送前,调用 SCB_CleanDCache_by_Addr 清理缓冲区。
  3. 在 DMA 接收完成后,调用 SCB_InvalidateDCache_by_Addr 使缓冲区失效。

代码示例

// 缓冲区需按 32 字节对齐(Cache 行大小)
__ALIGN_BEGIN static uint8_t tx_buffer[256] __ALIGN_END;
__ALIGN_BEGIN static uint8_t rx_buffer[256] __ALIGN_END;

// 启用 D-Cache
SCB_EnableDCache();

// DMA 发送前:确保 CPU 写入的数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

// DMA 接收完成后(在回调中):使 Cache 失效,强制从 SRAM 重新读取
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
        // 现在可以安全处理 rx_buffer
    }
}

注意事项

  • 地址和大小必须按 32 字节对齐,否则可能无效或引发异常。
  • 频繁清理/失效会降低性能,适合数据量小或低频场景。

解法二:MPU 配置为“不可缓存”区域

通过内存保护单元(MPU)将 DMA 使用的内存区域配置为“不可缓存”(或写-through),从而绕过 Cache。

原理

MPU 允许将内存区域划分为不同属性,包括 Cache 策略。将共享缓冲区设为“不可缓存”后,CPU 访问该区域时直接读写 SRAM,与 DMA 保持一致。

配置步骤

  1. 在系统初始化时,配置 MPU 区域。
  2. 设置区域基地址、大小、属性(禁止缓存)。
  3. 使能 MPU。

代码示例

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    __HAL_RCC_MPU_CLK_ENABLE();
    
    HAL_MPU_Disable();
    
    // 配置区域 0:0x20000000 开始,大小 32KB(根据实际调整)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;  // 不使用 TEX
    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_CONTROL_PRIVILEGED_DEFAULT);
}

// 在 main 中调用 MPU_Config() 后再启用 D-Cache

注意事项

  • MPU 区域大小必须是 2 的幂,且起始地址对齐。
  • 不可缓存区域会降低 CPU 访问速度,但保证一致性。
  • 适用于缓冲区固定且大小已知的场景。

解法三:使用无缓存 DMA 缓冲区(通过链接脚本或属性)

将 DMA 缓冲区放置在特定的内存段中,该段被配置为无缓存属性。在 STM32F4 上,通常利用 GCC 的 __attribute__((section)) 或 IAR 的 #pragma location 将变量放入自定义段,并在链接脚本中设置该段的属性。

原理

通过链接脚本将缓冲区分配到 SRAM 的某个区域,并利用 MPU 或硬件特性(如 TCM 内存)使其绕过 Cache。STM32F4 没有 TCM,但可以使用 MPU 配合链接脚本实现。

配置步骤

  1. 在链接脚本中定义一个新的内存区域(如 RAM_DMA)。
  2. 将缓冲区变量放入该区域。
  3. 使用 MPU 将该区域配置为不可缓存(类似解法二)。

代码示例(GCC 环境)

// 定义在自定义段中
__attribute__((section(".dma_buffer"))) uint8_t dma_rx_buf[128];

// 链接脚本(.ld)中添加:
// .dma_buffer (NOLOAD) :
// {
//   . = ALIGN(32);
//   *(.dma_buffer)
//   . = ALIGN(32);
// } >RAM_DMA

// 在内存区域定义中:
// RAM_DMA (xrw) : ORIGIN = 0x20000000, LENGTH = 32K  // 示例,需根据实际 SRAM 划分

// 然后使用 MPU 将 0x20000000 区域配置为不可缓存(如解法二)

注意事项

  • 需要手动调整链接脚本,确保区域不重叠。
  • 这种方法将缓冲区隔离,便于管理,但增加了链接配置复杂度。
  • 适合大型项目,可集中管理所有 DMA 缓冲区。

总结与选型建议

| 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件维护 Cache | 简单、无需额外配置 | 性能损失,需注意对齐 | 低频、小数据量 | | MPU 不可缓存区域 | 性能较好,配置一次 | 占用 MPU 区域,灵活性低 | 固定缓冲区、中等数据量 | | 无缓存 DMA 缓冲区 | 隔离清晰,性能最优 | 链接脚本复杂,移植性差 | 大型项目、高频传输 |

核心建议

  • 如果项目简单,优先使用软件维护 Cache,快速解决问题。
  • 如果缓冲区固定且性能要求高,使用 MPU 配置。
  • 如果项目复杂,推荐结合链接脚本和 MPU,实现无缓存专用缓冲区。

无论选择哪种方法,务必确保缓冲区地址按 32 字节对齐,并在 DMA 操作前后正确处理 Cache。希望本文能帮助你在 STM32F4 上稳定运行 DMA 外设,避免数据不一致的坑。