引言
在嵌入式系统中,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_CleanByAddr和HAL_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项目将更加稳健高效。