引言

在嵌入式系统中,SDRAM作为大容量外部存储器,常用于LCD显存、音频缓冲或数据采集。STM32F4系列内置D-Cache(哈佛架构下CPU与总线之间的高速缓存),能减少CPU访问慢速SDRAM的延迟。然而,缓存与DMA(直接内存访问)之间的数据一致性若处理不当,轻则数据错乱,重则系统崩溃。本文从原理出发,结合实战代码,彻底解决这一痛点。

D-Cache工作原理

什么是D-Cache?

D-Cache是CPU与主存(如SDRAM)之间的小容量高速SRAM,以缓存行(通常32字节)为单位存储数据。当CPU读取数据时,先查缓存;若命中(Hit),直接返回,避免访问慢速SDRAM;若未命中(Miss),则从SDRAM加载整行到缓存。写入时,采用**写回(Write-back)**策略:数据先写入缓存,标记为脏(Dirty),仅在缓存行被替换或显式Clean时,才写回SDRAM。

数据一致性问题

  • DMA写SDRAM,CPU读:DMA直接写入SDRAM,但缓存中可能保留旧数据(Stale),CPU读缓存得到过时值。
  • CPU写SDRAM,DMA读:CPU写入缓存,但尚未写回SDRAM,DMA从SDRAM读到旧数据。
  • 外设(如LTDC)读SDRAM:显示控制器直接读取SDRAM,若缓存未同步,画面出现撕裂或花屏。

Cache Clean与Invalidate

  • Clean(清脏):将缓存中脏数据写回SDRAM,确保外部存储器最新。
  • Invalidate(失效):使缓存行失效,下次访问强制从SDRAM重新加载,丢弃可能过时的数据。

硬件与软件准备

  • 硬件:STM32F429开发板(带SDRAM,如W9825G6KH),或F407+外部SDRAM模块。
  • 软件:STM32CubeIDE(或Keil),HAL库。
  • 关键外设:FMC(灵活存储控制器)驱动SDRAM,DMA用于数据传输。

配置步骤

1. 使能D-Cache和MPU

在系统初始化时,使能D-Cache,并配置MPU(内存保护单元)将SDRAM区域设置为非缓存(Non-cacheable)写-through,但为了性能,我们通常保持缓存,并手动维护。以下配置将SDRAM区域设为写回、读分配,并启用。

// 使能D-Cache
SCB_EnableDCache();

// 配置MPU,SDRAM基址0xC0000000,大小16MB
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
MPU_InitStruct.SubRegionDisable = 0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 写回,读分配
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_REGION_DISABLE_EXEC;
MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_CACHEABLE; // 允许缓存
MPU_InitStruct.IsBufferable = MPU_REGION_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);

2. 初始化SDRAM和DMA

使用HAL库初始化FMC和SDRAM,并配置DMA通道(例如从内存到内存)。

// SDRAM初始化(略,参考HAL例程)
// DMA配置:从SDRAM缓冲区到内部SRAM
DMA_HandleTypeDef hdma;
hdma.Instance = DMA2_Stream0;
hdma.Init.Channel = DMA_CHANNEL_0;
hdma.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma.Init.PeriphInc = DMA_PINC_ENABLE;
hdma.Init.MemInc = DMA_MINC_ENABLE;
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma.Init.Mode = DMA_NORMAL;
hdma.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma);

3. 数据一致性操作实战

场景:CPU写数据到SDRAM,然后DMA读取该数据发送到外设(如串口)

步骤1:CPU写入数据

uint32_t *sdram_buf = (uint32_t *)0xC0000000; // SDRAM基址
for (int i = 0; i < 1024; i++) {
    sdram_buf[i] = i * 2;
}
// 此时数据可能在缓存中,尚未写回SDRAM

步骤2:Clean D-Cache,确保数据写回SDRAM

SCB_CleanDCache_by_Addr((uint32_t *)sdram_buf, 1024 * 4); // 参数:地址和字节数
// 或者使用HAL函数:HAL_DCache_CleanByAddr(...)

步骤3:启动DMA传输

HAL_DMA_Start(&hdma, (uint32_t)sdram_buf, (uint32_t)internal_buf, 1024);
// 等待DMA完成
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

场景:DMA从外设接收数据到SDRAM,然后CPU读取

步骤1:DMA写入SDRAM(例如从ADC缓冲区)

HAL_DMA_Start(&hdma, (uint32_t)adc_buf, (uint32_t)sdram_buf, 1024);
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
// DMA直接写SDRAM,但缓存中可能有旧数据

步骤2:Invalidate D-Cache,使缓存失效,强制从SDRAM重新加载

SCB_InvalidateDCache_by_Addr((uint32_t *)sdram_buf, 1024 * 4);
// 或 HAL_DCache_InvalidateByAddr(...)

步骤3:CPU读取数据

uint32_t value = sdram_buf[0]; // 现在读到的是DMA写入的新数据

4. 注意事项

  • 地址对齐:Cache操作要求地址按32字节对齐(缓存行大小),否则可能影响相邻数据。
  • 大小匹配:操作长度应为32字节的倍数,否则可能遗漏部分行。
  • 使用HAL函数:HAL库提供了HAL_DCache_CleanByAddrHAL_DCache_InvalidateByAddr,内部处理了对齐和屏障,推荐使用。
  • 性能权衡:频繁Clean/Invalidate会降低性能,应尽量批量操作,或考虑将SDRAM区域配置为不可缓存(但牺牲速度)。
  • 多核/中断:若在中断中访问SDRAM,需确保缓存操作原子性,必要时关中断。

完整代码示例

以下是一个综合示例,演示了CPU写、DMA读,以及DMA写、CPU读的完整流程。

#include "main.h"

// 假设SDRAM基址0xC0000000,大小16MB
#define SDRAM_BASE 0xC0000000
#define BUF_SIZE 1024

void SystemClock_Config(void);
static void MPU_Config(void);
static void SDRAM_Init(void);
static void DMA_Init(void);

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();
    SDRAM_Init();
    DMA_Init();

    // 使能D-Cache(在MPU配置后)
    SCB_EnableDCache();

    uint32_t *sdram_buf = (uint32_t *)SDRAM_BASE;
    uint32_t internal_buf[BUF_SIZE];

    // 场景1:CPU写,DMA读
    for (int i = 0; i < BUF_SIZE; i++) {
        sdram_buf[i] = i + 1;
    }
    // Clean缓存,确保数据写回SDRAM
    HAL_DCache_CleanByAddr(sdram_buf, BUF_SIZE * 4);

    // 启动DMA从SDRAM到内部SRAM
    HAL_DMA_Start(&hdma, (uint32_t)sdram_buf, (uint32_t)internal_buf, BUF_SIZE);
    HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

    // 场景2:DMA写,CPU读(模拟外设数据)
    // 假设DMA从外设缓冲区写入SDRAM
    uint32_t ext_data[BUF_SIZE];
    for (int i = 0; i < BUF_SIZE; i++) ext_data[i] = i * 3;
    HAL_DMA_Start(&hdma, (uint32_t)ext_data, (uint32_t)sdram_buf, BUF_SIZE);
    HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

    // Invalidate缓存,使CPU读取最新数据
    HAL_DCache_InvalidateByAddr(sdram_buf, BUF_SIZE * 4);
    uint32_t first = sdram_buf[0]; // 应为0

    while (1) {}
}

static void MPU_Config(void)
{
    // 配置SDRAM区域为缓存、写回
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = SDRAM_BASE;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsCacheable = MPU_REGION_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_REGION_BUFFERABLE;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);
}

// 其他初始化函数省略

总结

D-Cache是双刃剑,提升性能的同时要求开发者理解并管理数据一致性。通过MPU配置SDRAM为可缓存区域,并在关键操作前后执行Clean/Invalidate,可确保CPU与DMA/外设之间的数据同步。实战中,务必注意地址对齐和操作长度,并优先使用HAL库函数。掌握这些技巧,你的STM32F4项目将更加稳健高效。