STM32F4 系列 SDRAM 运行内存的总线冲突与性能调优实战指南
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列中,将 SDRAM 用作运行内存可大幅扩展 RAM 空间,但若忽视总线冲突与访问效率,性能将大打折扣。本文深入剖析 FMC 总线架构下的 SDRAM 访问机制,揭示冲突根源,并给出从硬件配置到软件调优的完整方案,包括时序优化、DMA 分流、Cache 策略等,助你榨干 SDRAM 的每一分带宽。
# STM32F4 系列 SDRAM 运行内存的总线冲突与性能调优
## 一、为什么 SDRAM 会引发总线冲突?
STM32F4 内部 SRAM 位于 CPU 核心总线(如 I-Bus、D-Bus)上,访问延迟极低(零等待)。而外部 SDRAM 挂在 FMC(Flexible Memory Controller)上,通过 AHB 总线矩阵与 CPU、DMA 等主设备相连。当 CPU 频繁访问 SDRAM 时,FMC 需要占用 AHB 总线,同时 SDRAM 自身的刷新(Refresh)操作也会抢占总线周期,导致冲突。
**核心冲突点:**
- **CPU 与 DMA 竞争**:DMA 传输(如 ADC 采样、UART 接收)与 CPU 执行代码(从 SDRAM 取指)同时访问 FMC,总线矩阵按优先级仲裁,造成等待。
- **SDRAM 刷新开销**:SDRAM 必须周期性刷新(如 64ms 刷新 4096 行),刷新期间 FMC 暂停所有访问,若刷新配置不当,会显著增加延迟。
- **FMC 数据线宽度**:若配置为 16 位数据总线,一次 32 位访问需拆分为两次,增加总线占用时间。
## 二、硬件与 FMC 配置优化
### 1. 正确设置 FMC 时序参数
SDRAM 时序参数(如 TRCD、TRP、TRC)直接影响访问速度。以 W9825G6KH 为例(133MHz 时钟):
```c
// FMC SDRAM 初始化结构体(以 72MHz FMC 时钟为例)
FMC_SDRAM_TimingTypeDef Timing = {0};
Timing.LoadToActiveDelay = 2; // tRSC: 加载模式寄存器到激活
Timing.ExitSelfRefreshDelay = 7; // tXS: 退出自刷新
Timing.SelfRefreshTime = 4; // tSR: 自刷新时间
Timing.RowCycleDelay = 7; // tRC: 行周期
Timing.WriteRecoveryTime = 2; // tWR: 写恢复
Timing.RPDelay = 2; // tRP: 预充电
Timing.RCDDelay = 2; // tRCD: 行到列延迟
// 使能 SDRAM 时钟
__HAL_RCC_FMC_CLK_ENABLE();
// 初始化 FMC SDRAM 控制器(Bank1,16位数据线)
// 注意:若使用 32 位数据线,需确保 SDRAM 芯片支持并正确连接
```
**要点:**
- 时序参数需根据 SDRAM 数据手册和 FMC 时钟频率计算,过紧会导致数据错误,过松则浪费性能。
- 建议使用 `HAL_SDRAM_Init()` 后,通过 `HAL_SDRAM_SendCommand()` 发送模式寄存器配置(如 CAS=3,突发长度=1)。
### 2. 数据总线宽度选择
- **16 位总线**:节省引脚,但 32 位访问需两次传输,适合对容量要求高、性能要求中等的场景。
- **32 位总线**:匹配 CPU 字长,单次访问 32 位,但需更多引脚(如 F4 系列 100 脚以上封装)。
**推荐**:若性能优先,使用 32 位模式,并确保 SDRAM 芯片支持(如两片 16 位并联)。
## 三、软件层面的性能调优
### 1. 利用 Cache 减少访问次数
STM32F4 内置 4KB I-Cache 和 4KB D-Cache(需启用)。将 SDRAM 区域配置为 Cacheable,可缓存常用数据,减少 FMC 访问频率。
```c
// 启用 D-Cache 和 I-Cache
SCB_EnableICache();
SCB_EnableDCache();
// 将 SDRAM 区域设置为 Cacheable(MPU 配置)
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000; // SDRAM 起始地址
MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 可缓存
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);
```
**注意**:D-Cache 启用后,DMA 访问 SDRAM 时需注意缓存一致性。若 DMA 写入 SDRAM,CPU 读取前需 `SCB_CleanDCache()` 或 `SCB_InvalidateDCache()`。
### 2. 使用 DMA 分流 CPU 负载
对于大数据块传输(如 LCD 帧缓冲、文件系统读写),使用 DMA 而非 CPU 逐字拷贝,可释放 CPU 并减少总线占用时间。
```c
// 示例:使用 DMA2 从 SDRAM 传输数据到 LCD
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;
HAL_DMA_Init(&hdma);
// 启动传输(源地址为 SDRAM,目的地址为外设)
HAL_DMA_Start(&hdma, (uint32_t)sdram_buffer, (uint32_t)lcd_buffer, size);
```
**要点:**
- 配置 DMA 优先级为高,确保实时性。
- 若同时有多个 DMA 流,合理分配优先级(如音频 > 显示 > 存储)。
### 3. 调整 FMC 调度策略
FMC 支持写入突发(Write Burst)和读取突发(Read Burst),启用后可连续访问多个地址,减少总线切换开销。
```c
// 在 FMC SDRAM 初始化时设置突发模式
sdramHandle.Init.SDBank = FMC_SDRAM_BANK1;
sdramHandle.Init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_8;
sdramHandle.Init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_12;
sdramHandle.Init.MemoryDataWidth = FMC_SDRAM_MEM_BUS_WIDTH_32;
sdramHandle.Init.BurstReadMode = FMC_SDRAM_BURST_READ_ENABLE; // 启用突发读
sdramHandle.Init.WriteProtection = FMC_SDRAM_WRITE_PROTECTION_DISABLE;
```
**注意**:突发长度需与 SDRAM 模式寄存器一致(通常为 1 或 8),且 CPU 访问地址需对齐。
### 4. 避免频繁的 SDRAM 刷新冲突
SDRAM 刷新是自动的,但可通过调整刷新率(`RefreshRate`)来平衡。若刷新过于频繁,会占用大量总线时间;若过少,数据可能丢失。
```c
// 设置刷新计数(以 72MHz 为例,64ms 刷新 4096 行)
sdramHandle.Init.RefreshRate = 1386; // 计算公式:刷新周期 = (刷新时间 * 时钟频率) / 行数 - 20
```
**经验值**:在 72MHz 下,`RefreshRate` 通常为 1386 左右,可根据实际稳定性微调。
## 四、完整代码示例:SDRAM 初始化和性能测试
```c
#include "stm32f4xx_hal.h"
SDRAM_HandleTypeDef hsdram1;
void SDRAM_Init(void) {
// 1. 配置 FMC 时钟和引脚(略,参考 HAL 示例)
// 2. 初始化 SDRAM 控制器
hsdram1.Instance = FMC_SDRAM_BANK1;
hsdram1.Init.SDBank = FMC_SDRAM_BANK1;
hsdram1.Init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_8;
hsdram1.Init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_12;
hsdram1.Init.MemoryDataWidth = FMC_SDRAM_MEM_BUS_WIDTH_32;
hsdram1.Init.BurstReadMode = FMC_SDRAM_BURST_READ_ENABLE;
hsdram1.Init.WriteProtection = FMC_SDRAM_WRITE_PROTECTION_DISABLE;
hsdram1.Init.RefreshRate = 1386;
FMC_SDRAM_TimingTypeDef Timing = {0};
Timing.LoadToActiveDelay = 2;
Timing.ExitSelfRefreshDelay = 7;
Timing.SelfRefreshTime = 4;
Timing.RowCycleDelay = 7;
Timing.WriteRecoveryTime = 2;
Timing.RPDelay = 2;
Timing.RCDDelay = 2;
if (HAL_SDRAM_Init(&hsdram1, &Timing) != HAL_OK) {
Error_Handler();
}
// 3. 发送模式寄存器命令(CAS=3,突发长度=1)
FMC_SDRAM_CommandTypeDef Command = {0};
Command.CommandMode = FMC_SDRAM_CMD_LOAD_MODE;
Command.ModeRegisterDefinition = 0x230; // CAS=3, 突发长度=1
Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
Command.AutoRefreshNumber = 1;
Command.ModeRegisterDefinition = 0x230;
HAL_SDRAM_SendCommand(&hsdram1, &Command, 100);
// 4. 发送预充电命令
Command.CommandMode = FMC_SDRAM_CMD_PRECHARGE_ALL;
HAL_SDRAM_SendCommand(&hsdram1, &Command, 100);
// 5. 自动刷新
Command.CommandMode = FMC_SDRAM_CMD_AUTOREFRESH;
Command.AutoRefreshNumber = 8;
HAL_SDRAM_SendCommand(&hsdram1, &Command, 100);
}
// 性能测试:比较 SDRAM 和内部 SRAM 的读写速度
void PerformanceTest(void) {
uint32_t *sdram_ptr = (uint32_t*)0xC0000000;
uint32_t *sram_ptr = (uint32_t*)0x20000000;
volatile uint32_t sum = 0;
// 写入测试(10000 次)
uint32_t start = HAL_GetTick();
for (int i = 0; i < 10000; i++) sdram_ptr[i] = i;
uint32_t sdram_write_time = HAL_GetTick() - start;
start = HAL_GetTick();
for (int i = 0; i < 10000; i++) sram_ptr[i] = i;
uint32_t sram_write_time = HAL_GetTick() - start;
// 读取测试
start = HAL_GetTick();
for (int i = 0; i < 10000; i++) sum += sdram_ptr[i];
uint32_t sdram_read_time = HAL_GetTick() - start;
start = HAL_GetTick();
for (int i = 0; i < 10000; i++) sum += sram_ptr[i];
uint32_t sram_read_time = HAL_GetTick() - start;
printf("SDRAM Write: %d ms, SRAM Write: %d ms\r\n", sdram_write_time, sram_write_time);
printf("SDRAM Read: %d ms, SRAM Read: %d ms\r\n", sdram_read_time, sram_read_time);
}
```
## 五、注意事项与常见陷阱
- **缓存一致性**:启用 D-Cache 后,DMA 与 CPU 共享 SDRAM 时,必须手动维护缓存(`SCB_CleanDCache` / `SCB_InvalidateDCache`),否则数据错乱。
- **内存对齐**:SDRAM 访问建议 32 位对齐,避免非对齐访问导致总线多次传输。
- **初始化顺序**:必须先初始化 SDRAM 控制器,再发送模式寄存器命令,否则 SDRAM 无法正常工作。
- **刷新率调整**:若系统时钟变化(如降频),需重新计算 `RefreshRate`,否则数据丢失。
- **调试技巧**:使用逻辑分析仪观察 FMC 总线波形,确认时序是否满足 SDRAM 要求。
## 六、总结
通过合理配置 FMC 时序、启用 Cache、使用 DMA 分流以及调整刷新策略,可以显著降低 STM32F4 使用 SDRAM 时的总线冲突,提升系统整体性能。实际项目中,建议结合具体应用场景(如 LCD 显示、音频缓冲)进行针对性优化,并利用性能测试代码量化效果。