引言

在 STM32F4 系列(如 STM32F407)以 168MHz 主频运行时,CPU 和 DMA 都通过 AHB 总线访问内存,但 Cortex-M4 内核带有可选的 Cache(L1-Cache)。当 DMA 直接读写内存缓冲区时,CPU 可能因 Cache 命中而使用陈旧数据,导致串口收发数据错乱。尤其在双缓冲模式下,缓冲区切换频繁,问题更易暴露。本文提供三种经过验证的维护方案,帮助你彻底解决这一隐患。

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

  • CPU 视角:访问内存时,优先命中 Cache,读写操作在 Cache 中完成,之后才回写内存(write-back)。
  • DMA 视角:DMA 直接访问物理内存,不经过 Cache。
  • 冲突场景
    • 发送:CPU 写入数据到缓冲区,但数据仍留在 Cache 中,DMA 读取内存时拿到旧数据。
    • 接收:DMA 将新数据写入缓冲区,但 CPU 读取时命中 Cache 中的旧副本。

双缓冲模式(通常使用两个缓冲区交替收发)加剧了问题,因为切换时缓冲区内容必须实时同步。

方案一:软件清缓存(简单直接)

原理

在关键操作前后,使用 CMSIS 提供的函数强制 Cache 与内存同步。

  • 发送前:SCB_CleanDCache() 将 Cache 中脏数据写回内存。
  • 接收后:SCB_InvalidateDCache() 使 Cache 失效,强制 CPU 从内存重新读取。

配置步骤

  1. 启用 Cache(默认开启,但需确保时钟已配置)。
  2. 在 DMA 传输完成中断中,调用相应函数。
  3. 注意:双缓冲时,需对两个缓冲区分别处理。

代码示例

// 发送缓冲区
uint8_t tx_buf[2][256];
uint8_t tx_idx = 0;

void DMA_TX_Complete_IRQHandler(void) {
    // 发送完成,清缓存(可选,若下次要复用缓冲区)
    SCB_CleanDCache();
}

void send_data(uint8_t* data, uint32_t len) {
    // 拷贝数据到当前发送缓冲区
    memcpy(tx_buf[tx_idx], data, len);
    // 清缓存,确保 DMA 看到最新数据
    SCB_CleanDCache();
    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, tx_buf[tx_idx], len);
    tx_idx ^= 1; // 切换缓冲区
}

// 接收中断
void DMA_RX_Complete_IRQHandler(void) {
    // 使缓存失效,读取最新数据
    SCB_InvalidateDCache();
    process_data(rx_buf[rx_idx]);
    rx_idx ^= 1;
}

注意事项

  • 清缓存操作有性能开销,但 168MHz 下影响可忽略。
  • 必须确保 DMA 传输完成后才调用 Invalidate,否则可能丢失数据。
  • 适用于缓冲区较小、频率不高的场景。

方案二:MPU 配置非缓存区域(推荐)

原理

利用 MPU(Memory Protection Unit)将 DMA 缓冲区所在内存区域配置为“非缓存”(Normal memory, Non-cacheable)。这样 CPU 访问该区域时直接操作内存,与 DMA 保持一致。

配置步骤

  1. 定义缓冲区内存区域,建议使用独立数组或链接脚本指定段。
  2. 初始化 MPU,设置区域属性为 Non-cacheable。
  3. 在启动代码或主函数中使能 MPU。

代码示例

// 定义缓冲区,放在特定段(如 .noncache)
__attribute__((section(".noncache"))) uint8_t tx_buf[2][256];
__attribute__((section(".noncache"))) uint8_t rx_buf[2][256];

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    HAL_MPU_Disable();
    
    // 配置非缓存区域(假设缓冲区位于 0x20000000 起始的 4KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void) {
    HAL_Init();
    MPU_Config();
    // ... 其他初始化
}

注意事项

  • 缓冲区地址必须按区域大小对齐(如 4KB 区域需 4KB 对齐)。
  • 非缓存区域访问速度稍慢,但 DMA 场景下影响不大。
  • 确保所有 DMA 缓冲区都覆盖在配置区域内,否则仍会出问题。

方案三:硬件双缓冲对齐策略(进阶)

原理

利用 STM32F4 的 DMA 双缓冲模式(DMA_Init 中的 Mode 设置为 Circular 或 DoubleBuffer),并确保缓冲区地址与 Cache Line(通常 32 字节)对齐。通过合理设计,使 DMA 在切换缓冲区时自动同步,减少软件干预。

配置步骤

  1. 将缓冲区定义为 32 字节对齐(使用 __attribute__((aligned(32))))。
  2. 配置 DMA 为双缓冲模式,使能中断。
  3. 在中断中仅切换指针,不进行 Cache 操作(因为对齐后,DMA 写入不会跨越 Cache Line 边界,且 CPU 读取时不会命中旧数据)。

代码示例

__attribute__((aligned(32))) uint8_t rx_buf[2][256];
__attribute__((aligned(32))) uint8_t tx_buf[2][256];

void DMA_Config(void) {
    // 假设使用 DMA2_Stream0 用于接收
    hdma_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_rx.Init.Mode = DMA_CIRCULAR; // 双缓冲模式
    hdma_rx.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_rx);
    
    // 链接两个缓冲区
    HAL_DMAEx_MultiBufferStart(&hdma_rx, (uint32_t)&huart1.Instance->DR, (uint32_t)rx_buf[0], (uint32_t)rx_buf[1], 256);
}

// 中断处理
void DMA_RX_IRQHandler(void) {
    if (__HAL_DMA_GET_FLAG(&hdma_rx, DMA_FLAG_TCIF0_4)) {
        __HAL_DMA_CLEAR_FLAG(&hdma_rx, DMA_FLAG_TCIF0_4);
        // 当前使用的缓冲区由 DMA 自动切换,无需手动维护
        process_data(rx_buf[hdma_rx.State == HAL_DMA_STATE_READY ? 0 : 1]);
    }
}

注意事项

  • 对齐要求:缓冲区起始地址必须为 32 的倍数,且大小也建议为 32 的倍数。
  • 此方案依赖 DMA 硬件特性,需确认所用 DMA 支持双缓冲(F4 系列均支持)。
  • 若缓冲区大小不是 32 的倍数,仍可能出现跨行问题,此时需结合方案一。

总结与选型建议

| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件清缓存 | 简单,无需硬件配置 | 性能开销,需注意时机 | 低频、小数据量 | | MPU 非缓存 | 硬件保证,性能稳定 | 需配置 MPU,区域管理复杂 | 高频、大数据量,推荐 | | 硬件对齐 | 零软件开销,高效 | 依赖硬件特性,对齐要求严格 | 极高频,双缓冲模式 |

在实际项目中,建议优先使用 MPU 方案,它平衡了性能与可靠性。若追求极致性能,可结合硬件对齐策略。无论哪种方案,务必在开发初期就考虑 Cache 一致性,否则后期排查数据错乱会非常痛苦。

参考资料

  • STM32F4xx 参考手册(RM0090)
  • Cortex-M4 编程手册(PM0214)
  • CMSIS 文档(SCB 函数定义)