一、DRAM初始化:芯片上电后的第一件事

各位同学,今天我们来聊聊DRAM控制器的初始化。说实话,这部分内容看起来简单,但我在项目中踩过的坑,有一半都跟初始化时序有关。

DRAM芯片上电后,它不会自动进入工作状态。你得按照JEDEC标准规定的序列,一步步把它「唤醒」。这个过程,我们称之为初始化序列。

1.1 上电初始化序列

先说说标准流程。DDR3/DDR4的初始化,大致分这么几步:

  1. 上电与时钟稳定:先给VDD和VDDQ供电,然后提供稳定的参考时钟。这里有个坑——时钟必须比电源晚200μs以上。
  2. 复位信号释放:CKE保持低电平,RESET#信号拉高。我记得第一次做DDR4项目时,复位时序没算准,芯片死活不响应命令。
  3. 等待CKE稳定:复位释放后,至少等500μs,CKE才能拉高。
  4. 发送NOP命令:CKE拉高后,先发至少200个时钟周期的NOP。
  5. 加载模式寄存器:按顺序配置MR0、MR1、MR2、MR3。
  6. ZQ校准:DDR3/DDR4需要ZQCL命令完成阻抗校准。

核心要点:整个初始化过程,说白了就是「等」——等电源稳定、等时钟稳定、等内部电路稳定。你急不得。

我习惯把初始化序列画成状态机。这样在Verilog里实现时,思路特别清晰。下面是我常用的状态转移图:

DRAM初始化状态机 上电复位 等待时钟稳定 CKE拉高 NOP等待 配置MR0 配置MR1 配置MR2 配置MR3 ZQ校准完成 tCK稳定 tXPR 200个时钟 完成 完成 完成 完成 完成 ZQCL完成

1.2 模式寄存器MR0~MR3详解

初始化过程中,最关键的一步就是配置模式寄存器。DDR3有4个模式寄存器:MR0、MR1、MR2、MR3。每个寄存器12位,但实际用到的位各有不同。

MR0:核心时序配置

MR0主要控制CAS延迟(CL)、突发长度(BL)和突发类型。我个人觉得,CL是这里最重要的参数,它直接影响读延迟。

位域 名称 说明
Bit[1:0] BL 突发长度:00=8, 01=4, 10=8, 11=保留
Bit[2] BT 突发类型:0=顺序, 1=交错
Bit[6:4] CL CAS延迟:具体值见JEDEC标准
Bit[7] DLL复位 1=复位DLL,初始化时必须置1
Bit[11:9] WR 写恢复时间

经验之谈:配置MR0时,DLL复位位(Bit[7])一定要先置1,等初始化完成后清0。我曾经见过有人忘了清这个位,结果DLL一直处于复位状态,读数据全是乱的。

MR1:驱动与终端配置

MR1控制输出驱动强度、ODT(片上终端)和附加延迟(AL)。

  • Bit[2:0]:输出驱动阻抗,常见值RZQ/6(34Ω)或RZQ/7(40Ω)
  • Bit[9,6,2]:ODT阻抗配置,我一般设为RZQ/4(60Ω)
  • Bit[4:3]:附加延迟AL,0表示AL=0,1表示AL=CL-1

嗯,这里要注意。MR1的配置跟PCB走线阻抗密切相关。你想想看,如果ODT设得太小,信号反射会严重;设得太大,功耗又上去了。我建议先做仿真,再定具体值。

MR2:刷新与时序控制

MR2主要控制刷新相关参数和CAS写延迟(CWL)。

位域 名称 说明
Bit[2:0] CWL CAS写延迟,与频率相关
Bit[5:3] 刷新模式 000=标准刷新,001=四倍刷新
Bit[6] 自刷新温度范围 0=正常,1=扩展

MR3:多功能控制

MR3在DDR3中比较简单,主要用于MPR(多用途寄存器)和读数据格式控制。DDR4中MR3的功能更丰富,增加了CRC、DBI等控制位。

避坑指南:我曾经在DDR4项目中,因为MR3的CRC位配置错误,导致写数据时CRC校验一直失败。查了两天才发现是初始化时MR3的Bit[12]没置对。所以,每次配置完MR3,建议回读验证一下。

1.3 时序参数配置:tRCD、tCL、tRP、tRAS

初始化完成后,DRAM进入正常工作状态。但控制器必须知道一组关键时序参数,才能正确发出读写命令。这些参数通常存储在SPD(串行存在检测)中,或者由用户根据频率手动配置。

tRCD:行地址到列地址延迟

tRCD是激活命令(ACT)到读写命令(READ/WRITE)之间的最小延迟。说白了,就是打开一行后,要等多久才能访问这一行里的某个列。

计算公式:tRCD = tRCD(ns) × 时钟频率(MHz) / 1000

举个例子,DDR3-1600的tRCD典型值是13.125ns,换算成时钟周期就是13.125 × 800 / 1000 ≈ 10.5,向上取整为11个时钟周期。

tCL:CAS延迟

tCL就是读命令发出到第一个数据出现在DQ引脚上的延迟。这个参数在MR0里已经配好了,但控制器需要知道具体值来调整读数据采样点。

我习惯在初始化完成后,用读训练(Read Training)来验证tCL是否配置正确。如果采样点不对,读回来的数据全是错的。

tRP:预充电时间

tRP是预充电命令(PRE)到下一个激活命令(ACT)之间的最小延迟。它决定了关闭一行后,多久才能打开新的一行。

关键点:tRP直接影响行切换效率。如果tRP太长,频繁换行时性能会严重下降。我建议在满足时序要求的前提下,尽量选小的tRP值。

tRAS:行激活时间

tRAS是激活命令到预充电命令之间的最小时间。它保证了一行被激活后,至少要维持多长时间才能关闭。

tRAS的典型值在35ns~50ns之间。注意,tRAS不能设得太小,否则行内的数据可能还没稳定就被关闭了。但也不能设得太大,否则会浪费功耗。

1.4 时序参数配置示例

下面是一个DDR3-1600(时钟800MHz)的典型配置:

// 时序参数配置示例
parameter CLK_FREQ = 800;  // MHz

// 计算时钟周期
localparam T_CK = 1000 / CLK_FREQ;  // 1.25ns

// 时序参数(单位:ns)
localparam T_RCD_NS = 13.125;
localparam T_CL_NS  = 13.125;
localparam T_RP_NS  = 13.125;
localparam T_RAS_NS = 37.5;

// 转换为时钟周期数(向上取整)
localparam T_RCD = (T_RCD_NS + T_CK - 1) / T_CK;  // 11
localparam T_CL  = (T_CL_NS  + T_CK - 1) / T_CK;  // 11
localparam T_RP  = (T_RP_NS  + T_CK - 1) / T_CK;  // 11
localparam T_RAS = (T_RAS_NS + T_CK - 1) / T_CK;  // 30

个人习惯:我一般会在Verilog代码里用localparam定义这些时序参数,而不是用`define宏。这样在仿真时可以直接修改参数,方便调试。

1.5 初始化代码框架

最后,给一个简单的初始化状态机Verilog框架。实际项目中,你还需要加入错误处理、超时检测等逻辑。

module dram_init (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [2:0] init_state,  // 初始化状态
    output reg        init_done,   // 初始化完成标志
    // DRAM接口
    output reg        cke,
    output reg        cs_n,
    output reg        ras_n,
    output reg        cas_n,
    output reg        we_n,
    output reg [14:0] addr,
    output reg [2:0]  ba
);

// 状态定义
localparam IDLE      = 3'd0;
localparam WAIT_CKE  = 3'd1;
localparam NOP_WAIT  = 3'd2;
localparam MR0_CFG   = 3'd3;
localparam MR1_CFG   = 3'd4;
localparam MR2_CFG   = 3'd5;
localparam MR3_CFG   = 3'd6;
localparam ZQ_CAL    = 3'd7;

// 计数器
reg [15:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        init_state <= IDLE;
        init_done  <= 1'b0;
        cke        <= 1'b0;
        // 其他信号初始化...
    end else begin
        case (init_state)
            IDLE: begin
                // 等待复位释放
                if (rst_n) begin
                    init_state <= WAIT_CKE;
                    cnt <= 16'd0;
                end
            end
            WAIT_CKE: begin
                // 等待500μs后拉高CKE
                if (cnt >= 500_000) begin
                    cke <= 1'b1;
                    init_state <= NOP_WAIT;
                    cnt <= 16'd0;
                end else begin
                    cnt <= cnt + 1'b1;
                end
            end
            // ... 后续状态实现
        endcase
    end
end

endmodule

好了,关于DRAM初始化和模式寄存器配置,核心内容就这些。记住,初始化是DRAM控制器的「起跑线」——这一步没走稳,后面所有读写操作都会出问题。我建议你在仿真时,把初始化过程的每个状态都打印出来,确保时序完全符合JEDEC标准。


公众号:蓝海资料掘金营,微信deep3321