STM32F4 168MHz 下 DMA+双缓冲串口收发:Cache 一致性维护的三种实用策略

1. 问题根源:Cache 与 DMA 的“各自为政”

STM32F4 系列(如 STM32F407)在 168MHz 主频下,CPU 运行速度远高于外部 SRAM 或 SDRAM。为提升性能,Cortex-M4 内核集成了 Cache(通常为 4KB 或 8KB,分为 I-Cache 和 D-Cache)。当 CPU 访问内存时,数据会先被复制到 Cache 中,后续访问直接命中 Cache,避免重复访问慢速内存。

然而,DMA 控制器直接访问物理内存,不经过 CPU 的 Cache。这就导致了一个经典问题:

  • 发送场景:CPU 将数据写入内存缓冲区(数据可能被缓存,尚未写回物理内存),然后启动 DMA 传输。DMA 从物理内存读取数据,可能读到旧数据或未初始化的数据。
  • 接收场景:DMA 将接收到的数据写入内存缓冲区(直接写入物理内存),然后 CPU 读取该缓冲区。CPU 可能命中 Cache 中的旧数据,而看不到 DMA 写入的新数据。

在双缓冲串口收发中,这种不一致性会导致数据包错乱、丢包,甚至系统崩溃。因此,必须采取策略维护 Cache 一致性。

2. 策略一:软件清 Cache(简单直接)

原理

通过调用 CMSIS 提供的函数,在关键操作前后强制将 Cache 中的数据写回内存(Clean)或使 Cache 失效(Invalidate)。

  • 发送前:Clean 数据缓冲区,确保 DMA 能从物理内存读到最新数据。
  • 接收后:Invalidate 数据缓冲区,使 CPU 重新从物理内存读取 DMA 写入的数据。

配置步骤

  1. 开启 D-Cache(默认可能开启,需确认)。
  2. 在 DMA 传输前/后调用相应函数。

代码示例

#include "stm32f4xx.h"

// 定义双缓冲结构
#define BUF_SIZE 256
uint8_t rx_buf[2][BUF_SIZE] __attribute__((aligned(32)));  // 对齐到 Cache 行(32字节)
uint8_t tx_buf[2][BUF_SIZE] __attribute__((aligned(32)));

// 发送函数
void UART_DMA_Send(uint8_t *data, uint16_t len) {
    // 1. 将数据拷贝到 tx_buf(假设已在 tx_buf 中)
    // 2. 清 Cache:确保数据写回物理内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
    
    // 3. 启动 DMA 发送
    DMA_SetConfig(DMA2_Stream7, data, (uint32_t)&USART1->DR, len);
    DMA_Cmd(DMA2_Stream7, ENABLE);
}

// DMA 接收完成回调
void UART_DMA_RxComplete(void) {
    // 1. 使 Cache 失效:让 CPU 重新从内存读取数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf[active_buf], BUF_SIZE);
    
    // 2. 处理接收到的数据
    ProcessData(rx_buf[active_buf], BUF_SIZE);
    
    // 3. 切换缓冲区,重新启动 DMA 接收
    active_buf ^= 1;
    DMA_SetConfig(DMA2_Stream5, (uint32_t)&USART1->DR, rx_buf[active_buf], BUF_SIZE);
    DMA_Cmd(DMA2_Stream5, ENABLE);
}

注意事项

  • 缓冲区必须按 32 字节对齐(Cache 行大小),否则清 Cache 操作可能影响相邻数据。
  • 频繁调用清 Cache 函数会降低性能,适合数据量小、频率不高的场景。
  • 确保在 DMA 启动前完成 Clean,在 DMA 完成后进行 Invalidate。

3. 策略二:配置 MPU 设置内存为非 Cacheable(硬件隔离)

原理

通过 MPU(内存保护单元)将 DMA 使用的内存区域配置为“非 Cacheable”属性,这样 CPU 访问该区域时直接读写物理内存,绕过 Cache。虽然会牺牲部分性能,但彻底避免了不一致问题,且无需软件干预。

配置步骤

  1. 初始化 MPU,设置一个区域覆盖 DMA 缓冲区。
  2. 设置区域属性:非 Cacheable、非 Bufferable(或 Write-Through)。
  3. 使能 MPU。

代码示例

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    // 使能 MPU
    MPU_DeInit();
    
    // 配置区域:基地址为缓冲区地址,大小 4KB(覆盖两个 256B 缓冲区)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)rx_buf;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_REGION_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;
    
    MPU_Init(&MPU_InitStruct);
    
    // 使能 MPU
    MPU_Cmd(ENABLE);
}

// 在主函数中调用 MPU_Config()

注意事项

  • 非 Cacheable 区域访问速度较慢,但 DMA 缓冲区通常不大,影响有限。
  • 确保缓冲区地址和大小符合 MPU 区域对齐要求(通常为 32 字节倍数)。
  • 如果同时使用多个缓冲区,可配置多个区域或使用一个大区域覆盖。

4. 策略三:使用 CCM RAM(无 Cache 的专用内存)

原理

STM32F4 系列内部有一块 CCM(Core Coupled Memory)RAM,它直接连接到内核,不经过总线矩阵,因此也不受 Cache 影响。将 DMA 缓冲区放置在 CCM RAM 中,可以天然避免一致性问题,且访问速度极快(与内核同频)。

配置步骤

  1. 在链接脚本中定义 CCM RAM 段(通常地址为 0x10000000)。
  2. 将缓冲区变量指定到该段。

代码示例

// 在链接脚本(如 stm32f407_flash.ld)中添加:
// .ccmram :
// {
//   . = ALIGN(4);
//   *(.ccmram)
//   . = ALIGN(4);
// } > CCMRAM

// 定义缓冲区到 CCM RAM
__attribute__((section(".ccmram"))) uint8_t rx_buf[2][BUF_SIZE];
__attribute__((section(".ccmram"))) uint8_t tx_buf[2][BUF_SIZE];

// 使用方式与普通变量相同,无需额外 Cache 操作

注意事项

  • CCM RAM 容量有限(通常 64KB 或 128KB),需合理分配。
  • CCM RAM 不支持 DMA 访问?注意:在 STM32F4 中,DMA 控制器无法访问 CCM RAM,因为 CCM 只连接到内核。因此,此策略仅适用于 CPU 访问缓冲区,不适用于 DMA 直接读写。但可以用于 CPU 侧缓冲,DMA 使用另一块普通 RAM,通过软件拷贝数据,但这样会引入额外开销。

修正:实际上,STM32F4 的 DMA 无法访问 CCM RAM,所以此策略不适合直接用于 DMA 缓冲区。但可以作为辅助:将处理数据的临时缓冲区放在 CCM,DMA 缓冲区放在普通 RAM,通过 memcpy 在两者间拷贝(需配合 Cache 操作)。因此,此策略更适合对性能要求极高且数据量小的场景。

5. 三种策略对比与选型建议

| 策略 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件清 Cache | 简单、无需硬件配置 | 性能损耗,需注意对齐 | 数据量小、频率低 | | MPU 非 Cacheable | 硬件隔离,无需软件干预 | 访问速度稍慢,配置稍复杂 | 中等数据量,要求实时性 | | CCM RAM | 速度最快,无一致性问题 | 容量小,DMA 无法直接访问 | 需要 CPU 高频处理的小缓冲 |

推荐:对于大多数串口双缓冲应用,策略一(软件清 Cache)足够且易于实现;若数据吞吐量大,建议使用策略二(MPU 配置);若对延迟极端敏感且数据量小,可结合策略三(CCM RAM)作为辅助。

6. 总结

在 STM32F4 高速主频下,Cache 一致性是 DMA 通信不可忽视的问题。本文介绍了三种实用策略:软件清 Cache 简单直接,MPU 配置硬件隔离,CCM RAM 提供极致速度但需注意 DMA 限制。开发者应根据实际需求选择,并在代码中严格遵循操作顺序,确保数据一致性。希望本文能帮助你在嵌入式开发中少踩坑,提升系统可靠性。