# 引言 在嵌入式系统中,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区域设为**写回、读分配**,并启用。 ```c // 使能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通道(例如从内存到内存)。 ```c // 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写入数据** ```c 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** ```c SCB_CleanDCache_by_Addr((uint32_t *)sdram_buf, 1024 * 4); // 参数:地址和字节数 // 或者使用HAL函数:HAL_DCache_CleanByAddr(...) ``` **步骤3:启动DMA传输** ```c 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缓冲区)** ```c 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重新加载** ```c SCB_InvalidateDCache_by_Addr((uint32_t *)sdram_buf, 1024 * 4); // 或 HAL_DCache_InvalidateByAddr(...) ``` **步骤3:CPU读取数据** ```c uint32_t value = sdram_buf[0]; // 现在读到的是DMA写入的新数据 ``` ## 4. 注意事项 - **地址对齐**:Cache操作要求地址按32字节对齐(缓存行大小),否则可能影响相邻数据。 - **大小匹配**:操作长度应为32字节的倍数,否则可能遗漏部分行。 - **使用HAL函数**:HAL库提供了`HAL_DCache_CleanByAddr`和`HAL_DCache_InvalidateByAddr`,内部处理了对齐和屏障,推荐使用。 - **性能权衡**:频繁Clean/Invalidate会降低性能,应尽量批量操作,或考虑将SDRAM区域配置为不可缓存(但牺牲速度)。 - **多核/中断**:若在中断中访问SDRAM,需确保缓存操作原子性,必要时关中断。 # 完整代码示例 以下是一个综合示例,演示了CPU写、DMA读,以及DMA写、CPU读的完整流程。 ```c #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项目将更加稳健高效。