一、问题背景: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_AddrSCB_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 的数据一致性问题。该方案在保证性能的同时,提供了可靠的同步机制。实际项目中,务必根据内存区域特性灵活配置,并遵循上述注意事项,以避免踩坑。