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 时钟):

// 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 访问频率。

// 启用 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 并减少总线占用时间。

// 示例:使用 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),启用后可连续访问多个地址,减少总线切换开销。

// 在 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)来平衡。若刷新过于频繁,会占用大量总线时间;若过少,数据可能丢失。

// 设置刷新计数(以 72MHz 为例,64ms 刷新 4096 行)
sdramHandle.Init.RefreshRate = 1386; // 计算公式:刷新周期 = (刷新时间 * 时钟频率) / 行数 - 20

经验值:在 72MHz 下,RefreshRate 通常为 1386 左右,可根据实际稳定性微调。

四、完整代码示例:SDRAM 初始化和性能测试

#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 显示、音频缓冲)进行针对性优化,并利用性能测试代码量化效果。