引言

STM32F4 系列搭载 Cortex-M4 内核,支持 D-Cache(数据缓存)和 I-Cache。D-Cache 能显著提升 CPU 访问外部 RAM 的性能,但同时也引入了缓存与主存之间的数据一致性问题。当 DMA 控制器直接访问内存时,它绕过 CPU 和缓存,导致 CPU 看到的缓存数据与 DMA 读写的主存数据可能不一致。本文针对五种典型场景,提供系统性的解决方案。

场景一:CPU 写数据,DMA 读数据(外设发送)

问题描述:CPU 在内存中构造发送缓冲区,然后启动 DMA 将数据发送到外设(如 UART、SPI)。由于 D-Cache 的存在,CPU 写入的数据可能仍留在缓存中,尚未写回主存,DMA 从主存读取时得到旧数据。

解法:在启动 DMA 前,对缓冲区执行 Cache Clean 操作,将缓存数据写回主存。

// 假设缓冲区为 uint8_t tx_buf[256]; 长度为 len
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// 启动 DMA 传输
HAL_UART_Transmit_DMA(&huart, tx_buf, len);

注意事项

  • Clean 操作必须在 DMA 启动之前完成。
  • 地址需 32 字节对齐(Cortex-M4 缓存行大小),若不对齐,可整体 Clean 或使用 SCB_CleanDCache() 全清。
  • 若频繁操作,建议使用 DMA 双缓冲或乒乓缓冲,避免每次 Clean 全缓冲区。

场景二:DMA 写数据,CPU 读数据(外设接收)

问题描述:DMA 将外设接收的数据写入内存,CPU 随后读取。由于 D-Cache 可能包含旧数据,CPU 读到的不是 DMA 刚写入的新数据。

解法:在 CPU 读取前,对缓冲区执行 Cache Invalidate 操作,使缓存行失效,强制从主存重新加载。

// 假设接收缓冲区为 uint8_t rx_buf[256]; 长度为 len
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
// 现在 CPU 可以安全读取 rx_buf

注意事项

  • Invalidate 操作必须在 CPU 读取之前执行。
  • 如果缓冲区同时被 CPU 和 DMA 访问(如半字中断),需小心处理,避免覆盖未写回的数据。
  • 建议在 DMA 传输完成中断中执行 Invalidate。

场景三:双缓冲(Ping-Pong)模式

问题描述:双缓冲常用于高速数据流(如 ADC、以太网),两个缓冲区交替使用。一个缓冲区被 DMA 填充时,另一个被 CPU 处理。若不加处理,切换时可能发生数据错乱。

解法:在切换缓冲区时,对即将使用的缓冲区执行 Invalidate,对即将交给 DMA 的缓冲区执行 Clean。

// 假设两个缓冲区 buf0, buf1,大小均为 BUF_SIZE
volatile uint8_t *active_buf;

void DMA_Complete_IRQHandler(void) {
    // 当前 DMA 完成,切换缓冲区
    if (active_buf == buf0) {
        // 处理 buf0 前,先 Invalidate
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf0, BUF_SIZE);
        process(buf0);
        // 准备 buf1 给下一次 DMA,先 Clean
        SCB_CleanDCache_by_Addr((uint32_t*)buf1, BUF_SIZE);
        start_dma(buf1);
        active_buf = buf1;
    } else {
        SCB_InvalidateDCache_by_Addr((uint32_t*)buf1, BUF_SIZE);
        process(buf1);
        SCB_CleanDCache_by_Addr((uint32_t*)buf0, BUF_SIZE);
        start_dma(buf0);
        active_buf = buf0;
    }
}

注意事项

  • 确保 Clean 和 Invalidate 的顺序正确:先 Clean 再启动 DMA,先 Invalidate 再读取。
  • 缓冲区大小最好为缓存行(32 字节)的整数倍,避免部分失效。
  • 使用 __DMB() 内存屏障确保操作顺序。

场景四:DMA 描述符链(Scatter-Gather)

问题描述:DMA 描述符链中,每个描述符包含缓冲区地址和长度。CPU 更新描述符后,DMA 读取描述符时可能因缓存不一致而使用旧描述符。

解法:在修改描述符后,对描述符区域执行 Clean;在读取 DMA 状态(如传输完成标志)前,执行 Invalidate。

// 描述符结构体(假设 32 字节对齐)
typedef struct {
    uint32_t addr;
    uint32_t len;
    uint32_t ctrl;
    uint32_t status;
} DMA_Desc;

DMA_Desc desc[2];

// 更新描述符
void update_desc(int idx, uint32_t addr, uint32_t len) {
    desc[idx].addr = addr;
    desc[idx].len = len;
    desc[idx].ctrl |= DMA_CTRL_VALID;
    // 确保描述符写回主存
    SCB_CleanDCache_by_Addr((uint32_t*)&desc[idx], sizeof(DMA_Desc));
    __DMB(); // 内存屏障
}

// 在中断中检查状态
void DMA_IRQHandler(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)desc, sizeof(desc));
    if (desc[0].status & DMA_STATUS_DONE) { ... }
}

注意事项

  • 描述符必须对齐到缓存行边界,否则 Clean/Invalidate 可能影响相邻数据。
  • 使用 __DMB() 确保 Clean 完成后才启动 DMA。
  • 若描述符频繁更新,考虑将描述符放在非缓存区域(如 TCM RAM 或配置 MPU 为 non-cacheable)。

场景五:共享内存(CPU 与 DMA 并发访问)

问题描述:在多任务或中断中,CPU 和 DMA 同时访问同一内存区域(如共享状态标志、环形缓冲区),导致数据竞争。

解法:使用 MPU 将共享内存区域配置为 non-cacheable(或 write-through),彻底避免缓存一致性问题。

// 配置 MPU 区域,使 0x20010000 开始的 4KB 为 non-cacheable
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    HAL_MPU_Disable();
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20010000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意事项

  • 非缓存区域访问速度较慢,仅对关键共享数据使用。
  • 若使用 write-through 策略,CPU 写操作会直接更新主存,但读操作仍可能缓存,需结合 Invalidate。
  • MPU 配置需在系统初始化时完成,并确保区域不重叠。

总结

D-Cache 与 DMA 的一致性问题是 STM32F4 高性能应用的常见挑战。核心原则是:

  • CPU 写、DMA 读:Clean 缓存。
  • DMA 写、CPU 读:Invalidate 缓存。
  • 双缓冲和描述符链:在切换时执行相应操作,并配合内存屏障。
  • 共享内存:使用 MPU 配置为 non-cacheable,简化设计。

实际开发中,建议结合具体外设和性能要求,选择最合适的方案。对于极端实时性要求,可考虑将关键缓冲区放置于 TCM RAM(如 ITCM/DTCM),它们不经过 D-Cache,天然一致。希望本文能帮助你规避这些陷阱,写出更健壮的嵌入式代码。