引言:DMA 与 D-Cache 的“隐形战争”
在嵌入式开发中,DMA 负责高效搬运数据,而 CPU 依赖 D-Cache 加速访问。然而,当两者操作同一内存时,D-Cache 的“懒惰”写回策略会引发数据不一致:CPU 修改的数据可能仍留在 Cache 中,DMA 却从主存读取旧数据;反之,DMA 写入主存后,CPU 可能读到 Cache 中的过期数据。STM32F4 系列(Cortex-M4)默认不启用 D-Cache,但若使用外部 SDRAM 或开启 D-Cache 加速,问题便浮现。本文以 STM32F429 为例,通过 MPU 配置解决这一经典难题。
原理剖析:Cache 与 DMA 的冲突根源
1. D-Cache 的工作机制
- 写回(Write-back):CPU 写数据时仅更新 Cache,标记为脏(Dirty),延迟写回主存。
- 写分配(Write-allocate):读未命中时,将主存数据加载到 Cache。
2. DMA 的“旁路”特性
DMA 直接访问主存(SRAM 或 SDRAM),不经过 Cache。因此,当 CPU 与 DMA 共享缓冲区时,Cache 中的脏数据或过期数据会导致不一致。
3. 解决方案思路
- 禁用 Cache:简单粗暴,但牺牲性能。
- MPU 配置区域为非缓存(Device 或 Strongly-ordered):强制 CPU 直接访问主存,适用于 DMA 缓冲区。
-
软件维护 Cache:使用
SCB_CleanDCache或SCB_InvalidateDCache,但需谨慎时机。
实战配置:MPU 设置非缓存区域
1. 硬件环境
- 芯片:STM32F429ZIT6(Cortex-M4,带 D-Cache)
- 外部 SDRAM:IS42S16400J(1M×16bit,位于 Bank1)
- 开发环境:Keil MDK 5.27 + STM32CubeF4 固件库
2. 配置步骤
步骤 1:启用 D-Cache 和 MPU
在系统初始化时,先使能 MPU,再使能 D-Cache(顺序不可颠倒)。
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct;
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 SDRAM 区域(地址 0xC0000000,大小 4MB)为非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4MB;
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
void Cache_Init(void)
{
// 使能 D-Cache(需在 MPU 之后)
SCB_EnableDCache();
}
步骤 2:定义 DMA 缓冲区并放置到非缓存区域
将 DMA 缓冲区定义在 SDRAM 中(或通过链接脚本指定),确保其地址落在 MPU 配置的非缓存区域。
// 使用 __attribute__ 指定段,或直接定义在 SDRAM 地址
uint8_t dma_rx_buffer[1024] __attribute__((section(".sdram")));
// 在链接脚本中,将 .sdram 段定位到 0xC0000000 之后
步骤 3:DMA 与 CPU 交互示例
以 UART DMA 接收为例,演示数据一致性保证。
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer));
// 在 DMA 传输完成回调中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 由于缓冲区是非缓存的,CPU 直接读取主存,无需 Cache 操作
ProcessData(dma_rx_buffer);
// 重新启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer));
}
}
步骤 4:软件维护 Cache(备用方案)
若无法使用 MPU,可在 DMA 操作前后手动维护 Cache。
// 在 CPU 写数据后,启动 DMA 前,清 Cache
SCB_CleanDCache();
// 在 DMA 完成,CPU 读数据前,无效化 Cache
SCB_InvalidateDCache();
完整代码示例:MPU + DMA 环形缓冲区
以下是一个更完整的示例,包含 MPU 配置、DMA 环形缓冲区实现。
#include "stm32f4xx_hal.h"
// 定义环形缓冲区结构(位于非缓存区域)
typedef struct {
uint8_t data[256];
uint16_t head;
uint16_t tail;
} RingBuffer;
// 将缓冲区放置到 SDRAM 非缓存区域
RingBuffer g_rb __attribute__((section(".sdram")));
// MPU 配置函数(如前所述)
void MPU_Config(void);
// 初始化 DMA 接收
void DMA_Init(void)
{
// 配置 UART DMA 等,省略具体细节
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
SCB_EnableDCache();
DMA_Init();
// 启动 DMA 接收,数据直接写入 g_rb.data
HAL_UART_Receive_DMA(&huart1, g_rb.data, sizeof(g_rb.data));
while (1)
{
// 主循环处理数据,无需 Cache 操作
if (g_rb.tail != g_rb.head)
{
ProcessByte(g_rb.data[g_rb.tail++]);
g_rb.tail %= sizeof(g_rb.data);
}
}
}
注意事项与调试技巧
- MPU 区域大小对齐:MPU 区域大小必须是 2 的幂次,且基地址对齐。例如 4MB 区域要求基地址 0xC0000000 对齐到 4MB。
-
链接脚本:确保
.sdram段正确放置在 SDRAM 起始地址,否则 MPU 不生效。 - Cache 操作时机:若使用软件维护,务必在 DMA 启动前 Clean,完成后 Invalidate,顺序错误会导致数据错乱。
- 调试技巧:使用逻辑分析仪或断点观察内存值,确认 Cache 行为。也可临时禁用 D-Cache 对比测试。
- 性能权衡:非缓存区域访问速度较慢,仅对 DMA 缓冲区使用,其他高频数据仍可缓存。
结语
通过 MPU 将 DMA 缓冲区配置为非缓存,从根源上避免了 Cache 与 DMA 的数据一致性冲突,既保证了正确性,又保留了对其他区域的缓存加速。掌握这一技巧,将使你在处理高速数据采集、网络通信等场景时更加游刃有余。希望本文能成为你嵌入式开发路上的有力工具。