STM32F4 D-Cache 与 DMA 数据一致性:五种失效场景及修复方案

1. 背景与原理

STM32F4 系列(Cortex-M4)内置 D-Cache(数据缓存),用于加速 CPU 对 SRAM 的访问。但当 DMA 直接访问内存时,DMA 不经过 Cache,直接读写物理 RAM。这导致 Cache 中的数据与 RAM 中的数据可能不一致,即“数据一致性问题”。

  • DMA 写入 RAM:DMA 将外设数据写入 RAM,但 Cache 中仍保留旧数据,CPU 读取时命中 Cache,得到过期数据。
  • CPU 写入 RAM:CPU 修改数据后,数据可能仍留在 Cache(写回策略),DMA 读取 RAM 时得到旧值。

STM32F4 的 D-Cache 默认是写回(write-back)模式,因此必须手动维护一致性。

2. 五种失效场景与修复

场景 1:DMA 接收数据后,CPU 读取旧数据

现象:UART 或 SPI 通过 DMA 接收数据到缓冲区,CPU 在 DMA 完成中断中读取缓冲区,却得到旧数据。

原因:DMA 写入 RAM,但 Cache 中对应行仍为旧值,CPU 读 Cache 命中。

修复:在 CPU 读取前,使 Cache 失效(Invalidate)。

// 假设 DMA 接收完成中断
void DMA_RX_IRQHandler(void) {
    // 使缓冲区对应的 Cache 行失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    // 现在 CPU 读取 rx_buffer 将获得 DMA 写入的新数据
    process_data(rx_buffer);
}

场景 2:CPU 修改数据后,DMA 发送旧数据

现象:CPU 填充发送缓冲区,启动 DMA 发送,但 DMA 发送的是旧数据。

原因:CPU 写入的数据还在 Cache 中,尚未写回 RAM,DMA 直接读 RAM 得到旧值。

修复:在启动 DMA 前,将缓冲区 Cache 行写回(Clean)。

void send_data(uint8_t* buf, uint32_t len) {
    // 填充 buf
    fill_buffer(buf, len);
    // 将 buf 写回 RAM,确保 DMA 可见
    SCB_CleanDCache_by_Addr((uint32_t*)buf, len);
    // 启动 DMA 发送
    DMA_Start_TX(buf, len);
}

场景 3:双缓冲交替使用,未同步维护

现象:使用双缓冲(Ping-Pong)时,一个缓冲区被 DMA 写入,另一个被 CPU 处理,切换时数据错乱。

原因:两个缓冲区可能映射到同一 Cache 行(若地址相邻),导致失效操作影响彼此。

修复:确保缓冲区按 Cache 行大小(32 字节)对齐,并分别维护。

// 使用 __ALIGNED(32) 对齐缓冲区
__ALIGNED(32) uint8_t buf_ping[1024];
__ALIGNED(32) uint8_t buf_pong[1024];

void DMA_Complete_IRQ(uint8_t active_buf) {
    if (active_buf == 0) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf_ping, sizeof(buf_ping));
        process(buf_ping);
        SCB_CleanDCache_by_Addr((uint32_t*)buf_ping, sizeof(buf_ping));
    } else {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf_pong, sizeof(buf_pong));
        process(buf_pong);
        SCB_CleanDCache_by_Addr((uint32_t*)buf_pong, sizeof(buf_pong));
    }
}

场景 4:DMA 描述符链更新,CPU 修改描述符后未写回

现象:使用 DMA 描述符(如以太网 DMA)时,CPU 更新描述符(如设置长度、状态),但 DMA 仍读取旧描述符。

原因:描述符结构体被 CPU 修改,但数据在 Cache 中,DMA 读 RAM 得到旧值。

修复:更新描述符后,必须 Clean 描述符所在内存区域,并添加内存屏障确保顺序。

typedef struct {
    uint32_t addr;
    uint32_t len;
    uint32_t ctrl;
} DMA_Desc;

void update_desc(DMA_Desc* desc, uint32_t addr, uint32_t len) {
    desc->addr = addr;
    desc->len = len;
    desc->ctrl |= DESC_CTRL_VALID;
    // 写回描述符
    SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(DMA_Desc));
    // 内存屏障,确保写回完成
    __DSB();
    // 启动 DMA
    DMA_Start(desc);
}

场景 5:外设寄存器映射到内存区域,被 Cache 缓存

现象:某些外设(如 FSMC 控制的 LCD、外部 SRAM)映射到内存地址,CPU 访问时被 Cache 缓存,导致外设状态更新不及时。

原因:外设寄存器或帧缓冲被标记为可缓存,CPU 写操作被缓存,外设看不到。

修复:将外设映射区域配置为不可缓存(使用 MPU),或手动 Clean/Invalidate。

// 使用 MPU 配置区域为不可缓存
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    // 配置区域基地址为外设地址,大小 1MB,属性为 Device
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x60000000; // FSMC 区域
    MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    __DSB();
}

3. 完整代码示例:UART DMA 接收 + 发送

以下示例展示如何安全使用 D-Cache 与 DMA 进行 UART 通信。

#include "stm32f4xx_hal.h"

__ALIGNED(32) uint8_t rx_buf[256];
__ALIGNED(32) uint8_t tx_buf[256];

void UART_DMA_Init(void) {
    // 使能 D-Cache(若未使能)
    SCB_EnableDCache();
    // 配置 UART DMA 等(省略)
}

void UART_RX_Complete_IRQ(void) {
    // 使 rx_buf 的 Cache 失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
    // 处理接收数据
    process_rx(rx_buf);
}

void UART_TX_Start(uint8_t* data, uint32_t len) {
    memcpy(tx_buf, data, len);
    // 写回 tx_buf
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart, tx_buf, len);
}

4. 注意事项

  • 对齐要求:缓冲区必须按 32 字节(Cache 行大小)对齐,否则 SCB_InvalidateDCache_by_Addr 可能破坏相邻数据。
  • 长度处理:操作长度最好为 32 的倍数,否则需手动处理边界。
  • 内存屏障:在 Clean/Invalidate 后,使用 __DSB() 确保操作完成。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能,建议使用 DMA 双缓冲并批量处理。
  • MPU 配置:对于外设映射区域,优先使用 MPU 设置为不可缓存,避免手动操作。

5. 总结

D-Cache 与 DMA 的一致性问题在 STM32F4 开发中极为常见,理解失效场景并正确使用 SCB_CleanDCacheSCB_InvalidateDCache 是解决问题的关键。记住:DMA 写入后要 Invalidate,DMA 读取前要 Clean,并配合对齐和内存屏障,即可确保数据一致。希望本文能帮助你避开这些坑,写出更稳定的嵌入式代码。