引言

在嵌入式系统中,SDRAM 常被用作大容量数据缓冲或 LCD 显存。STM32F4 的 FMC(Flexible Memory Controller)支持 SDRAM,但时序参数配置复杂,稍有不慎便会导致随机性错误。本文将从数据手册出发,逐步推导时序寄存器值,并通过实测验证,最终实现稳定运行。

1. SDRAM 时序基础

SDRAM 的访问时序由多个延迟参数决定,核心包括:

  • tRCD(RAS to CAS Delay):行激活到列命令的延迟。
  • tRP(Row Precharge Time):预充电时间。
  • tWR(Write Recovery Time):写恢复时间。
  • tRC(Row Cycle Time):行周期时间。
  • tRAS(Active to Precharge Time):行激活到预充电的最小时间。

STM32F4 的 FMC 通过以下寄存器配置时序:

  • FMC_SDRTR(刷新定时器)
  • FMC_SDCR1/2(控制寄存器,含列选择、突发长度等)
  • FMC_SDTR1/2(时序寄存器,含 TMRD、TXSR、TRAS、TRC、TWR、TRP、TRCD)

2. 数据手册关键参数提取

以常见的 32MB SDRAM W9825G6KH 为例,其数据手册(-75 速度等级)给出:

  • 时钟频率最高 133MHz,我们使用 90MHz(STM32F429 的 FMC 时钟)。
  • 关键时序(单位 ns):
    • tRCD = 20ns
    • tRP = 20ns
    • tWR = 14ns(或 2 个时钟周期)
    • tRC = 63ns
    • tRAS = 42ns
    • tXSR(自刷新退出到命令) = 70ns
    • tMRD(模式寄存器加载到命令) = 2 个时钟周期

注意:不同速度等级数值不同,务必以实际芯片手册为准。

3. 时序寄存器值推导

FMC 时序寄存器中的值以时钟周期为单位,计算公式为:

  • 周期数 = ceil(时间 / 时钟周期)

时钟周期 = 1 / 90MHz ≈ 11.11ns。

3.1 计算各参数

  • TMRD(模式寄存器加载延迟):tMRD = 2 周期,直接取 2。
  • TXSR(自刷新退出时间):tXSR = 70ns → 70/11.11 ≈ 6.3 → 向上取整为 7。
  • TRAS:tRAS = 42ns → 42/11.11 ≈ 3.78 → 取 4。
  • TRC:tRC = 63ns → 63/11.11 ≈ 5.67 → 取 6。
  • TWR:tWR = 14ns → 14/11.11 ≈ 1.26 → 取 2(注意:TWR 最小为 2 个周期)。
  • TRP:tRP = 20ns → 20/11.11 ≈ 1.8 → 取 2。
  • TRCD:tRCD = 20ns → 取 2。

3.2 写入寄存器

在 STM32F4 的 HAL 库中,通过 FMC_SDRAM_TimingTypeDef 结构体配置:

FMC_SDRAM_TimingTypeDef Timing = {0};

Timing.LoadToActiveDelay    = 2;   // TMRD
Timing.ExitSelfRefreshDelay = 7;   // TXSR
Timing.SelfRefreshTime      = 4;   // TRAS(实际为 SelfRefreshTime,但对应 TRAS)
Timing.RowCycleDelay        = 6;   // TRC
Timing.WriteRecoveryTime    = 2;   // TWR
Timing.RPDelay              = 2;   // TRP
Timing.RCDDelay             = 2;   // TRCD

注意:HAL 库中 SelfRefreshTime 对应 TRAS,RowCycleDelay 对应 TRC。

4. 完整初始化代码示例

以下代码基于 STM32F429 和 HAL 库,初始化 FMC SDRAM 并测试读写。

#include "stm32f4xx_hal.h"

// SDRAM 基地址(Bank2)
#define SDRAM_BANK_ADDR  0xD0000000
#define SDRAM_BANK_SIZE  (32 * 1024 * 1024)

FMC_SDRAM_HandleTypeDef hsdram1;

void SDRAM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能 GPIO 时钟
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_GPIOE_CLK_ENABLE();
    __HAL_RCC_GPIOF_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();
    __HAL_RCC_GPIOH_CLK_ENABLE();
    
    // 配置 FMC 引脚(此处省略具体引脚映射,根据原理图配置)
    // ...
    
    // FMC 时钟使能
    __HAL_RCC_FMC_CLK_ENABLE();
    
    // SDRAM 控制器配置
    hsdram1.Instance = FMC_SDRAM_DEVICE;
    hsdram1.Init.SDBank             = FMC_SDRAM_BANK2;
    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_16;
    hsdram1.Init.SDClockPeriod      = FMC_SDRAM_CLOCK_PERIOD_2;
    hsdram1.Init.ReadBurst          = FMC_SDRAM_RBURST_ENABLE;
    hsdram1.Init.ReadPipeDelay      = FMC_SDRAM_RPIPE_DELAY_1;
    
    // 时序参数(根据推导)
    FMC_SDRAM_TimingTypeDef Timing = {0};
    Timing.LoadToActiveDelay    = 2;
    Timing.ExitSelfRefreshDelay = 7;
    Timing.SelfRefreshTime      = 4;
    Timing.RowCycleDelay        = 6;
    Timing.WriteRecoveryTime    = 2;
    Timing.RPDelay              = 2;
    Timing.RCDDelay             = 2;
    
    if (HAL_SDRAM_Init(&hsdram1, &Timing) != HAL_OK)
    {
        Error_Handler();
    }
    
    // 发送初始化命令序列
    FMC_SDRAM_CommandTypeDef Command = {0};
    
    // 时钟使能
    Command.CommandMode            = FMC_SDRAM_CMD_CLK_ENABLE;
    Command.CommandTarget          = FMC_SDRAM_CMD_TARGET_BANK2;
    Command.AutoRefreshNumber      = 1;
    Command.ModeRegisterDefinition = 0;
    HAL_SDRAM_SendCommand(&hsdram1, &Command, 1000);
    
    // 预充电全部
    Command.CommandMode            = FMC_SDRAM_CMD_PALL;
    Command.CommandTarget          = FMC_SDRAM_CMD_TARGET_BANK2;
    Command.AutoRefreshNumber      = 1;
    Command.ModeRegisterDefinition = 0;
    HAL_SDRAM_SendCommand(&hsdram1, &Command, 1000);
    
    // 自动刷新(两次)
    Command.CommandMode            = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
    Command.CommandTarget          = FMC_SDRAM_CMD_TARGET_BANK2;
    Command.AutoRefreshNumber      = 2;
    Command.ModeRegisterDefinition = 0;
    HAL_SDRAM_SendCommand(&hsdram1, &Command, 1000);
    
    // 加载模式寄存器(设置突发长度、CAS 延迟等)
    // 模式寄存器值:CAS=3,突发长度=1(或8),顺序突发
    uint32_t mode_reg = (3 << 4) | (0 << 7) | (0 << 9); // CAS=3, BL=1
    Command.CommandMode            = FMC_SDRAM_CMD_LOAD_MODE;
    Command.CommandTarget          = FMC_SDRAM_CMD_TARGET_BANK2;
    Command.AutoRefreshNumber      = 1;
    Command.ModeRegisterDefinition = mode_reg;
    HAL_SDRAM_SendCommand(&hsdram1, &Command, 1000);
    
    // 设置刷新率(典型值 64ms / 4096 行 = 15.625us,取 15us)
    // 刷新计数 = (15us * 90MHz) - 20 = 1330
    HAL_SDRAM_ProgramRefreshRate(&hsdram1, 1330);
}

// 测试函数:写入并读取 32 位数据
void SDRAM_Test(void)
{
    uint32_t *sdram = (uint32_t *)SDRAM_BANK_ADDR;
    uint32_t errors = 0;
    
    // 写入 0xDEADBEEF
    for (uint32_t i = 0; i < SDRAM_BANK_SIZE/4; i++)
    {
        sdram[i] = 0xDEADBEEF;
    }
    
    // 读取并校验
    for (uint32_t i = 0; i < SDRAM_BANK_SIZE/4; i++)
    {
        if (sdram[i] != 0xDEADBEEF)
        {
            errors++;
        }
    }
    
    if (errors == 0)
    {
        // 成功
    }
    else
    {
        // 失败,需调整时序
    }
}

5. 实测调优与验证

理论推导只是起点,实际 PCB 走线、温度、电压都会影响时序。推荐以下调优步骤:

  1. 基础测试:先用上述配置运行简单的读写测试,如全地址写读、随机地址写读。
  2. 边界扫描:逐步增大或减小关键时序参数(如 TRCD、TRP),观察错误率变化。
  3. 压力测试:长时间运行(如 24 小时)并循环读写,确保无偶发错误。
  4. 使用示波器:测量 SDRAM 时钟、数据线波形,检查建立/保持时间是否满足。

常见问题及对策:

  • 数据错误:优先增加 TRCD 和 TRP 值。
  • 刷新失败:调整刷新率,确保小于 64ms/行。
  • 初始化失败:检查模式寄存器设置,CAS 延迟必须与硬件匹配。

6. 注意事项

  • 不同 SDRAM 芯片的时序参数差异较大,务必查阅实际数据手册。
  • FMC 时钟频率越高,时序余量越小,建议先使用较低频率(如 90MHz)验证。
  • 模式寄存器中的 CAS 延迟必须与 FMC 的 ReadPipeDelay 匹配,否则读取数据会错位。
  • 刷新率设置不能过大或过小,过大会导致刷新频繁占用带宽,过小则数据丢失。
  • 在调试时,建议使用内存映射方式访问 SDRAM,便于观察变量。

结语

通过从数据手册提取参数、推导时序寄存器值,并结合实测调优,我们可以让 STM32F4 稳定运行外部 SDRAM。本文提供的流程和代码可作为模板,适用于大多数 SDRAM 芯片。记住,时序配置没有万能公式,唯有理解原理并反复验证,才能确保系统的可靠性。