引言

在 STM32F4 系列(如 STM32F407、STM32F429)中,Cortex-M4 内核集成了可配置的 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,当 DMA 控制器直接访问内存时,D-Cache 的存在会导致 CPU 与 DMA 看到的数据不一致,从而引发难以排查的 bug。本文将从原理出发,结合实际案例,给出系统性的排查与修复方案。

D-Cache 与 DMA 的工作原理

为什么需要 D-Cache?

D-Cache 是 CPU 与主存(如 SRAM)之间的小容量高速缓存。CPU 读取数据时,优先从 Cache 中获取;写入时,数据可能暂存在 Cache 中(写回策略),而非立即更新主存。这显著提升了 CPU 访问速度,但引入了数据一致性问题。

DMA 的“旁路”特性

DMA 控制器直接访问主存,不经过 CPU 的 Cache。因此,当 DMA 向内存写入数据时,CPU 可能从 Cache 中读到旧值;反之,CPU 修改数据后,DMA 可能从主存中读到旧值。

一致性问题的两种场景

  • 场景 A:DMA 写入,CPU 读取(如 ADC 采样数据由 DMA 搬运到内存)
    • DMA 更新主存,但 Cache 中保留旧数据,CPU 读 Cache 得到陈旧值。
  • 场景 B:CPU 写入,DMA 读取(如 CPU 准备数据包,DMA 发送)
    • CPU 写 Cache,但主存未更新,DMA 读主存得到旧数据。

典型问题现象

  • 数据包发送时,偶尔出现首字节错误或丢包。
  • ADC 采样数据时,读取的数组始终是旧值,除非禁用 Cache。
  • 调试时,观察变量显示正确,但实际运行结果异常(因为调试器可能强制刷新 Cache)。

解决方案:Clean 与 Invalidate

核心操作

  • Clean(清干净):将 Cache 中的脏数据(修改过但未写回主存)写回主存,确保主存最新。
  • Invalidate(使无效):将 Cache 中的行标记为无效,下次访问时重新从主存读取。

CMSIS 提供的 API

在 STM32F4 的 CMSIS 设备头文件中,提供了以下函数(定义于 core_cm4.h):

void SCB_CleanDCache(void);          // 清理整个 D-Cache
void SCB_InvalidateDCache(void);     // 使整个 D-Cache 无效
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

注意:dsize 参数是字节数,且地址需 32 字节对齐(Cache line 大小)。

实战案例:DMA 接收数据

硬件配置

  • MCU:STM32F407ZGT6
  • 外设:USART2,DMA1 通道 6 接收数据
  • 缓冲区:uint8_t rx_buf[256] __attribute__((aligned(32)));

错误代码(未处理一致性)

uint8_t rx_buf[256];

void DMA1_Stream5_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) {
        DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
        // 直接处理 rx_buf,但可能读到旧数据
        process_data(rx_buf);
    }
}

修复后的代码

// 使用 __attribute__((aligned(32))) 确保对齐
uint8_t rx_buf[256] __attribute__((aligned(32)));

void DMA1_Stream5_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) {
        DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
        // 关键:使 Cache 无效,强制从主存重新读取
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
        process_data(rx_buf);
    }
}

发送场景修复

// CPU 填充数据后,发送前 Clean
uint8_t tx_buf[128] __attribute__((aligned(32)));

void send_data(uint8_t *data, uint16_t len) {
    memcpy(tx_buf, data, len);
    // 将 Cache 中的数据写回主存,确保 DMA 能读到最新值
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    // 启动 DMA 发送
    DMA_SetCurrDataCounter(DMA1_Stream4, len);
    DMA_Cmd(DMA1_Stream4, ENABLE);
}

注意事项

  • 对齐要求SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址按 32 字节对齐,否则可能导致断言失败或操作无效。建议使用 __attribute__((aligned(32))) 定义缓冲区。
  • 长度处理dsize 参数是字节数,但内部会按 Cache line 对齐处理,建议传入实际长度,但确保缓冲区大小是 32 的倍数,避免越界。
  • 性能开销:频繁 Clean/Invalidate 会降低性能,建议仅在 DMA 传输完成或开始前调用,避免在中断中频繁操作。
  • 多缓冲区策略:使用双缓冲(Ping-Pong)时,需分别对每个缓冲区进行一致性操作。
  • 调试技巧:如果问题难以复现,可临时禁用 D-Cache(SCB_DisableDCache())对比测试,确认是否由 Cache 引起。

总结

D-Cache 与 DMA 的一致性问题是 STM32F4 开发中的经典陷阱。通过理解 Cache 的写回机制和 DMA 的旁路特性,并合理使用 Clean 和 Invalidate 操作,可以彻底解决此类问题。记住:DMA 写入后,CPU 读取前需 Invalidate;CPU 写入后,DMA 读取前需 Clean。希望本文能帮助你少走弯路,让嵌入式开发更加顺畅。