引言

STM32F4 系列(如 STM32F407、F429 等)内置了 Cortex-M4 内核,带有可选的 D-Cache(数据缓存)和 I-Cache(指令缓存)。当外扩 SDRAM 作为大容量数据缓冲区时,D-Cache 能显著减少 CPU 访问 SDRAM 的延迟。然而,D-Cache 与 SDRAM 之间的数据一致性(Cache Coherency)问题,常常让开发者陷入“数据莫名丢失”或“DMA 传输错误”的困境。本文结合实战经验,带你从原理到代码,彻底掌握正确的维护策略。

1. 原理:为什么 D-Cache 会导致数据不一致?

D-Cache 是 CPU 与主存(SDRAM)之间的一层高速缓存。当 CPU 读取 SDRAM 数据时,会先检查 D-Cache 是否命中;写入时,则可能采用“写回”(Write-back)策略,即数据先写入 Cache,标记为脏(Dirty),直到缓存行被替换或显式清理时才写回 SDRAM。

这种机制带来两个典型问题:

  • CPU 写入后,DMA 外设读取到旧数据:CPU 修改了 SDRAM 中的数据,但数据还停留在 D-Cache 中,DMA 直接读取 SDRAM,拿到的是未更新的旧值。
  • DMA 写入后,CPU 读取到旧数据:DMA 将新数据写入 SDRAM,但 D-Cache 中仍保留着旧数据的副本,CPU 读取时命中了 Cache,得到过期数据。

因此,在涉及 DMA、外设共享内存的场景下,必须手动维护 Cache 的一致性。

2. 实战踩坑:一个典型的 DMA 接收错误

某项目使用 STM32F429 外扩 SDRAM 作为图像帧缓冲,通过 DCMI 接口接收摄像头数据,DMA 直接写入 SDRAM。开启 D-Cache 后,图像出现随机花屏,且 CPU 读取帧数据时经常得到全零或旧帧。

排查过程:

  • 关闭 D-Cache 后,问题消失,确认与 Cache 相关。
  • 进一步分析:DMA 写入 SDRAM 后,CPU 读取时命中了 D-Cache 中的旧数据,导致花屏。

解决思路:在 DMA 传输完成后,执行 Cache 清理(Clean)和无效化(Invalidate)操作。

3. 配置步骤:使能 D-Cache 与 SDRAM 控制器

3.1 使能 D-Cache

在系统初始化代码中,使能 D-Cache 和 I-Cache(可选):

SCB_EnableDCache();
SCB_EnableICache();

注意:使能 D-Cache 后,所有对 SRAM 和 SDRAM 的访问都会被缓存。若外设(如 DMA)需要直接访问内存,必须进行一致性维护。

3.2 配置 SDRAM 控制器(FMC)

使用 STM32CubeMX 配置 FMC 的 SDRAM 接口,时序参数需根据具体 SDRAM 芯片调整。初始化后,SDRAM 的基地址通常为 0xC0000000(Bank1)或 0xD0000000(Bank2)。

// FMC SDRAM 初始化示例(简化)
FMC_SDRAM_InitTypeDef sdram_init;
sdram_init.SDBank = FMC_SDRAM_BANK1;
sdram_init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_8;
sdram_init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_12;
// ... 其他时序参数
HAL_SDRAM_Init(&hsdram, &sdram_init);

4. 核心:数据一致性维护策略

4.1 关键 API

Cortex-M4 提供了 CMSIS 函数:

  • SCB_CleanDCache():清理整个 D-Cache,将脏数据写回内存。
  • SCB_InvalidateDCache():无效化整个 D-Cache,丢弃缓存内容。
  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):按地址清理指定区域。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):按地址无效化指定区域。

注意:地址必须按 32 字节对齐(Cache line 大小),长度也需为 32 的倍数。

4.2 场景一:CPU 写入,DMA 读取(如发送数据)

在启动 DMA 发送前,需要将 CPU 写入的数据从 Cache 清理到 SDRAM:

uint8_t buffer[1024] __attribute__((aligned(32)));
// 填充 buffer 数据...

// 清理 Cache,确保数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer, sizeof(buffer));

// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart, buffer, sizeof(buffer));

4.3 场景二:DMA 写入,CPU 读取(如接收数据)

在 DMA 接收完成后,需要无效化 Cache,使 CPU 重新从 SDRAM 读取最新数据:

uint8_t buffer[1024] __attribute__((aligned(32)));
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 无效化 Cache,丢弃旧缓存
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, sizeof(buffer));
    // 现在可以安全读取 buffer
}

4.4 场景三:双向共享(如帧缓冲)

对于频繁交互的缓冲区,建议采用“双缓冲 + 手动同步”策略:

  • CPU 写入时,先 Clean 再交给 DMA。
  • DMA 完成后,先 Invalidate 再让 CPU 读取。

5. 完整代码示例:DMA 收发与 Cache 维护

以下代码演示了在 STM32F429 上使用 SDRAM 作为缓冲区,通过 UART DMA 收发数据,并正确维护 D-Cache。

#include "stm32f4xx_hal.h"

// 缓冲区定义,32 字节对齐
uint8_t tx_buffer[256] __attribute__((aligned(32)));
uint8_t rx_buffer[256] __attribute__((aligned(32)));

void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_FMC_Init(void);
void MX_USART2_UART_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_FMC_Init();
    MX_USART2_UART_Init();

    // 使能 D-Cache(必须在所有外设初始化后)
    SCB_EnableDCache();

    // 填充发送数据
    for (int i = 0; i < sizeof(tx_buffer); i++) {
        tx_buffer[i] = i;
    }

    // 发送前清理 Cache
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
    HAL_UART_Transmit_DMA(&huart2, tx_buffer, sizeof(tx_buffer));

    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer));

    while (1) {
        // 主循环
    }
}

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        // 无效化 Cache,确保读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
        // 处理接收数据...
    }
}

6. 注意事项与最佳实践

  • 对齐与长度:使用 __attribute__((aligned(32))) 确保缓冲区地址 32 字节对齐,且长度是 32 的倍数,否则 SCB_CleanDCache_by_Addr 可能无法正确操作。
  • 性能权衡:频繁的 Clean/Invalidate 会降低性能,应尽量批量操作,避免逐字节调用。
  • DMA 描述符:如果使用 DMA 的链表模式,确保描述符也位于非缓存区域或进行一致性维护。
  • MPU 配置:可以将 SDRAM 区域配置为“非缓存”或“写通”模式,但会牺牲性能。推荐保持缓存,手动维护一致性。
  • 调试技巧:若出现诡异数据问题,先关闭 D-Cache 测试,若问题消失,则基本可断定是缓存一致性问题。

结语

D-Cache 是提升系统性能的利器,但必须理解其工作原理并正确维护一致性。通过本文的实战案例和代码示例,相信你已经掌握了 STM32F4 上 D-Cache 与 SDRAM 协同工作的正确姿势。记住:缓存不是玄学,而是需要精确控制的工程细节。在后续项目中,建议将一致性维护封装为统一的底层接口,避免在业务代码中散落 Cache 操作,提高可维护性。