一、问题背景:D-Cache 与 DMA 的冲突
STM32F4 内核(Cortex-M4)内置了 4KB 的 D-Cache 和 I-Cache,用于加速 CPU 对内存的访问。然而,当 DMA 控制器直接访问内存(如外部 SRAM、SDRAM)时,CPU 可能先将数据缓存在 D-Cache 中,而 DMA 直接读写物理内存,导致两者看到的数据不一致。
典型场景:
- 使用 DMA 从 ADC 采集数据到外部 SRAM,CPU 读取该缓冲区时可能读到旧数据(DMA 已更新内存,但 Cache 未失效)。
- CPU 先写入缓冲区,然后启动 DMA 发送,但 DMA 可能读到 Cache 中尚未写回内存的旧数据。
这种问题表现为随机性、偶发性错误,极难排查。
二、原理剖析:Cache 一致性模型
Cortex-M4 的 D-Cache 采用 Write-Back 策略(默认):CPU 写操作只更新 Cache,不立即写回内存,直到 Cache 行被替换或显式 Clean。而 DMA 不经过 Cache,直接访问物理内存。
因此,一致性维护需要两种操作:
- Clean:将 Cache 中脏数据写回内存。
- Invalidate:使 Cache 行失效,下次访问从内存重新加载。
STM32F4 提供了 SCB 相关函数(如 SCB_CleanDCache()、SCB_InvalidateDCache()),但手动管理繁琐且易出错。更优雅的方案是使用 MPU 配置内存区域的 Cache 策略。
三、MPU 配置方案
MPU(Memory Protection Unit)不仅可以设置访问权限,还能定义内存区域的 Cache 属性。对于 DMA 共享缓冲区,有两种推荐配置:
方案 A:关闭该区域的 Cache(Strongly-Ordered 或 Device)
将缓冲区所在区域配置为 Normal memory, Non-cacheable,即完全绕过 D-Cache。优点是简单可靠,缺点是性能下降(CPU 每次访问都直接读内存)。
方案 B:配置为 Write-Through 模式
Write-Through 策略下,CPU 写操作同时更新 Cache 和内存,读操作仍可缓存。这样 DMA 读内存时总能得到最新数据,而 CPU 读 DMA 写入的数据时,只需在 DMA 完成后执行一次 Invalidate(或依赖硬件自动失效,但 STM32F4 不支持硬件一致性,仍需软件处理)。
实际工程中,方案 B 更常用,兼顾性能与一致性。
四、实战配置步骤
以 STM32F407 为例,假设使用外部 SRAM(地址 0x68000000,大小 1MB)作为 DMA 缓冲区。
1. 使能 D-Cache 和 MPU
在 main() 函数初始化阶段:
#include "stm32f4xx.h"
void MPU_Config(void)
{
// 确保 MPU 未使能
MPU->CTRL = 0;
// 配置区域 0:外部 SRAM 区域,Write-Through
MPU->RBAR = 0x68000000 | MPU_REGION_SIZE_1MB | (0 << 0); // Region 0
MPU->RASR = (0x0 << 0) | // 无访问权限限制
(0x1 << 1) | // 允许执行
(0x0 << 3) | // 非共享
(0x1 << 4) | // Write-Through (TEX=0, C=1, B=0)
(0x0 << 5) | // 非缓存
(0x0 << 6) | // 非缓冲
(0x0 << 8) | // 无子区域禁用
(0x0 << 16) | // 无指令访问
(0x1 << 24); // 使能 Region
// 使能 MPU,使用默认内存映射作为后备
MPU->CTRL = (0x1 << 0) | (0x1 << 2); // Enable MPU, Enable default region
// 使能 D-Cache(如果尚未使能)
SCB_EnableDCache();
}
注意:MPU_RASR 的 TEX、C、B 位组合决定了 Cache 策略。对于 Write-Through,需要设置 TEX=0, C=1, B=0(即 Normal memory, Write-Through)。
2. 配置 DMA 缓冲区
将 DMA 缓冲区定义在外部 SRAM 区域,并确保编译器将其放置到正确地址:
#define BUF_SIZE 1024
uint8_t dma_buf[BUF_SIZE] __attribute__((section(".ext_sram")));
在链接脚本(.ld)中添加:
.ext_sram 0x68000000 :
{
*(.ext_sram)
} > EXTSRAM
3. DMA 传输中的一致性处理
即使配置了 Write-Through,在 DMA 写入完成后,CPU 读取前仍需执行 Invalidate,以确保 Cache 中可能存在的旧数据被清除:
// DMA 接收完成回调
void DMA_RxComplete(DMA_HandleTypeDef *hdma)
{
// 使缓冲区对应的 Cache 行失效
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 现在 CPU 可以安全读取 dma_buf
}
// CPU 写入后启动 DMA 发送
void DMA_SendData(void)
{
// 确保 CPU 写操作已通过 Write-Through 更新到内存(实际上已自动完成)
// 无需 Clean,但为了保险可执行 Clean
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart, dma_buf, BUF_SIZE);
}
五、完整代码示例
以下是一个完整的初始化与使用示例(基于 HAL 库):
// main.c
#include "stm32f4xx_hal.h"
void MPU_Config(void);
void DMA_Init(void);
uint8_t dma_buf[1024] __attribute__((section(".ext_sram")));
int main(void)
{
HAL_Init();
SystemClock_Config();
// 配置 MPU 和 D-Cache
MPU_Config();
// 初始化 DMA(略)
DMA_Init();
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart, dma_buf, sizeof(dma_buf));
while (1)
{
// 主循环
}
}
void MPU_Config(void)
{
// 禁用 MPU
MPU->CTRL = 0;
// 配置 Region 0:外部 SRAM 0x68000000,1MB,Write-Through
MPU->RBAR = 0x68000000 | (0 << 0); // 基地址 + Region 0
MPU->RASR = (0x0 << 0) | // 权限:全部可访问
(0x1 << 1) | // 可执行
(0x0 << 3) | // 非共享
(0x1 << 4) | // TEX=0, C=1, B=0 => Write-Through
(0x0 << 5) |
(0x0 << 6) |
(0x0 << 8) | // 子区域全部使能
(0x0 << 16) | // 无指令缓存
(0x1 << 24); // Region 使能
// 使能 MPU,并启用默认区域
MPU->CTRL = (0x1 << 0) | (0x1 << 2);
// 使能 D-Cache(如果之前未使能)
SCB_EnableDCache();
}
void DMA_Init(void)
{
// 初始化 DMA 流、通道等(略)
// 注意:DMA 的缓冲区地址必须指向外部 SRAM
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 使缓冲区失效,确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));
// 处理数据...
}
}
六、注意事项
-
地址对齐:
SCB_InvalidateDCache_by_Addr和SCB_CleanDCache_by_Addr要求地址按 32 字节对齐(Cache 行大小)。如果缓冲区未对齐,需手动调整或使用整 Cache 操作(如SCB_InvalidateDCache()),但效率较低。 - MPU 区域重叠:确保 MPU 区域不与其他区域重叠,否则行为未定义。建议使用默认内存映射作为后备。
- 性能权衡:Write-Through 模式会降低写性能(每次写都到内存),但对于 DMA 缓冲区这种低频访问场景,影响可忽略。
-
DMA 方向:
- 外设→内存:DMA 写入后,CPU 读取前必须 Invalidate。
- 内存→外设:CPU 写入后,DMA 读取前建议 Clean(Write-Through 下可省略,但保险起见执行)。
- 多缓冲区:如果使用双缓冲,需分别配置 MPU 区域或确保同一区域覆盖所有缓冲区。
- 调试技巧:若问题依旧,可暂时关闭 D-Cache 验证是否由 Cache 引起,再逐步优化。
七、总结
通过 MPU 将 DMA 共享缓冲区配置为 Write-Through 模式,并配合必要的 Cache 维护操作,可以彻底解决 STM32F4 上 DMA 与 CPU 的数据一致性问题。该方案在保证性能的同时,提供了可靠的同步机制。实际项目中,务必根据内存区域特性灵活配置,并遵循上述注意事项,以避免踩坑。