STM32F4 168MHz 下 DMA+双缓冲串口收发的 Cache 一致性维护:三种正确姿势
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列以 168MHz 主频运行时,CPU 与 DMA 通过 AHB 总线访问内存,而 Cortex-M4 内核的 Cache 可能导致 DMA 看到的缓冲区数据与 CPU 不一致。本文针对 DMA+双缓冲串口收发场景,深入剖析 Cache 一致性问题,并给出三种实用维护方案:软件清缓存、MPU 配置非缓存区域、以及硬件双缓冲对齐策略。每种方案均附原理、配置步骤和代码示例,助你避免数据错乱,提升系统稳定性。
# 引言
在 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. 注意:双缓冲时,需对两个缓冲区分别处理。
### 代码示例
```c
// 发送缓冲区
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。
### 代码示例
```c
// 定义缓冲区,放在特定段(如 .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 读取时不会命中旧数据)。
### 代码示例
```c
__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 函数定义)