FPGA学习历程(四):SDRAM 控制器(初始化与刷新)

本文详细介绍SDRAM的初始化及刷新模块设计过程,包括命令真值表、时间参数配置、模式寄存器设置等内容,并提供了Verilog源码及Modelsim仿真结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


以下内容学习自开源骚客教程: SDRAM那些事儿第一季。

一、数据手册相关信息

1.1 命令真值表

  关于这个表,博主特意核对了好几款不同型号的 SDRAM,结果不能说完全相同,只能说高度一致,最多在一些可有可无的小功能上有所区别,貌似在遵循公共的协议一样。
在这里插入图片描述
在这里插入图片描述
  把需要用到的抓出来:

CmdCSRASCASWE
Precharge0010
Auto-Refresh0001
NOP0111
Mode-Set0000

1.2 时间参数

  这个也是从数据手册上找的。不过因为用的是 Micron 的时序图,里面还是有一些参数只能在 Micron 的手册上才能找到定义和具体值,这里就统一一下贴 Micron 的出来吧:
在这里插入图片描述
在这里插入图片描述

1.3 模式寄存器配置

  博主使用的是 13 位地址、16位数据的 SDRAM,但是店家给的手册上没有这个,所以借用了其他 SDRAM 芯片的模式寄存器位分布说明(一般来说是通用的)。这里先将模式寄存器配置为 13‘b0_0000_0011_0010,即 CL = 3,Burst Length = 4。
在这里插入图片描述

二、初始化模块

2.1 模块时序图

  这是数据手册上的初始化时序图,暂时只用到 tRP 和 tRFC(tMRD 在刷新部分才会用到):
在这里插入图片描述
  这是实际设计中 SDRAM 初始化模块的时序图:
在这里插入图片描述

  • cnt_200us 是 SDRAM 上电稳定期的计时。手册上说最少 100us,咱稳妥点给它多留一倍的时间;
  • Flag_200us 是计满 200us 后开始初始化操作的标志;
  • cnt_cmd 是初始化操作期间的时钟周期计数器,使它来起到一个时间轴的作用;
  • cmd_reg 就是命令寄存器了,从命令真值表可以看出这里需要 4 位位宽的寄存器;
  • sdram_addr 很明显是 SDRAM 的地址线;
  • Flag_init_end 是初始化完成的标志。

2.2 模块源码

2.2.1 sdram_init.v

module sdram_init
(
    // system signals
    input 				    sclk	        ,       // 板载系统时钟 50MHz
	input                   s_rst_n         ,       // 复位信号,低电平有效
    
    // others
    output  reg [3:0]       cmd_reg         ,       // 输出的命令(即 CS、RAS、CAS、WE 这四位)
    output  wire [12:0]     sdram_addr      ,       // SDRAM 地址
    output  wire            flag_init_end           // 初始化结束标志
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
localparam                  DEALY_200US =   10000   ;

//SDRAM Command
localparam                  NOP         =   4'b0111 ;
localparam                  PRE         =   4'b0010 ;
localparam                  AREF        =   4'b0001 ;
localparam                  MSET        =   4'b0000 ;                  

reg [13:0]                  cnt_200us               ;       // 200 * 1000 / 20 = 10000,转换为2进制表示有 14 位
wire                        flag_200us              ;       // 200us 计时结束标志
reg [3:0]                   cnt_cmd                 ;       // 初始化阶段一共要给4个命令,其中 tRP 占1个 CLK,2个 tRFC 共占8个 CLK,要计9个CLK

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
// cnt_200us:200us 计时未结束时持续自加
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        cnt_200us <= 'd0;
    else if(flag_200us == 1'b0)
        cnt_200us <= cnt_200us + 1'b1;
end

// cnt_cmd:只在初始化期间自加计时
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        cnt_cmd <= 'd0;
    else if(flag_200us == 1'b1 && flag_init_end == 1'b0)
        cnt_cmd <= cnt_cmd + 1'b1;
end

// cmd_reg
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        cmd_reg <= NOP;
    else if(flag_200us == 1'b1)
        case(cnt_cmd)
            0:          cmd_reg <=  PRE;
            1:          cmd_reg <=  AREF;
            5:          cmd_reg <=  AREF;
            9:          cmd_reg <=  MSET;
            default:    cmd_reg <=  NOP;
        endcase
end

assign  flag_init_end   =   (cnt_cmd >=  'd10) ? 1'b1 : 1'b0;                                   // 初始化期间命令全部输出完成
assign  flag_200us      =   (cnt_200us >= DEALY_200US) ? 1'b1 : 1'b0;
assign  sdram_addr      =   (cmd_reg == MSET) ? 13'b0_0000_0011_0010 : 13'b0_0100_0000_0000;    // Precharge All Banks 时,A10 需拉高,其余脚无关

endmodule 

2.2.2 sdram_top.v

  这次的仿真并不是直接仿真 init 模块,而是将 init 模块例化到 sdram_top 模块中,转而仿真 sdram_top 模块,因此还需要完成 sdram_top 模块的代码。

// sdram_top.v 源码,跟 Kevin 大佬敲下来发现这里没有配置 sdram_bank 信号的连接通路,不过只是仿真初始化模块的话则没有影响
module sdram_top
(
    // system signals
    input 				    sclk	    ,       // 板载系统时钟 50MHz
	input                   s_rst_n     ,       // 复位信号,低电平有效
    
    // SDRAM Interfaces
    output  wire            sdram_clk   ,
    output  wire            sdram_cke   ,
    output  wire            sdram_cs_n  ,
    output  wire            sdram_cas_n ,
    output  wire            sdram_ras_n ,
    output  wire            sdram_we_n  ,
    output  wire [1:0]      sdram_bank  ,
    output  wire [12:0]     sdram_addr  ,
    output  wire [1:0]      sdram_dqm   ,
    inout        [15:0]     sdram_dq
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// init module
wire                        flag_init_end   ;
wire    [3:0]               init_cmd        ;
wire    [12:0]              init_addr       ;

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
assign  sdram_cke       =   1'b1;
assign  sdram_addr      =   init_addr;
assign  {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} =   init_cmd;
assign  sdram_dqm       =   2'b00;
assign  sdram_clk       =   ~sclk;		// sdram 命令生成时钟与 sdram 命令采集时钟反向,保证命令采集时命令已经稳定生成

sdram_init sdram_init_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // others
    .cmd_reg        (init_cmd),         // 输出的命令(即 CS、RAS、CAS、WE 这四位)
    .sdram_addr     (init_addr),        // SDRAM 地址
    .flag_init_end  (flag_init_end)     // 初始化结束标志
);

endmodule 

2.2.3 tb_sdram_top.v

// tb_sdram_top.v 源码
`timescale 1ns/1ns

module tb_sdram_top;

reg             sclk		;
reg             s_rst_n		;

wire            sdram_clk   ;
wire            sdram_cke   ;
wire            sdram_cs_n  ;
wire            sdram_cas_n ;
wire            sdram_ras_n ;
wire            sdram_we_n  ;
wire [1:0]      sdram_bank  ;
wire [12:0]     sdram_addr  ;
wire [1:0]      sdram_dqm   ;
wire [15:0]     sdram_dq    ;

initial 
begin
    sclk    =   1;
    s_rst_n <=  0;
    #100
    s_rst_n <=  1;
end

always  #10     sclk    =   ~sclk;

sdram_top sdram_top_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // SDRAM Interfaces
    .sdram_clk      (sdram_clk),
    .sdram_cke      (sdram_cke),
    .sdram_cs_n     (sdram_cs_n),
    .sdram_cas_n    (sdram_cas_n),
    .sdram_ras_n    (sdram_ras_n),
    .sdram_we_n     (sdram_we_n),
    .sdram_bank     (sdram_bank),
    .sdram_addr     (sdram_addr),
    .sdram_dqm      (sdram_dqm),
    .sdram_dq       (sdram_dq)
);

defparam	sdram_model_plus_inst.addr_bits = 13;					// 13
defparam    sdram_model_plus_inst.data_bits = 16;
defparam	sdram_model_plus_inst.col_bits	= 9;
defparam    sdram_model_plus_inst.mem_sizes = 2*1024*1024;		    // 2M

sdram_model_plus sdram_model_plus_inst
(
    .Dq              (sdram_dq), 
    .Addr            (sdram_addr), 
    .Ba              (sdram_bank), 
    .Clk             (sdram_clk), 
    .Cke             (sdram_cke), 
    .Cs_n            (sdram_cs_n), 
    .Ras_n           (sdram_ras_n), 
    .Cas_n           (sdram_cas_n), 
    .We_n            (sdram_we_n), 
    .Dqm             (sdram_dqm),
    .Debug           (1'b1)
);

endmodule 

2.3 Modelsim仿真

  SDRAM 的仿真需要使用到一个模拟 SDRAM 的仿真插件(即 tb_sdram_top.v 源码中例化的 sdram_model_plus 例化模块),可以在 Kevin 大佬的交流群里获取。顺带一提,拿到手上的插件是适用于 11 位地址的 SDRAM,要用在 13 位地址的 SDRAM 上还得自行重定义一些参数(已经在 tb 文件中改完了)。另外,在 sdram_model_plus 中还有关于时间参数的配置,可以根据手册与实际配置酌情调整:
在这里插入图片描述
  之后创建 Modelsim 工程,将 tb_sdram_top.v 、sdram_model_plus.v、 sdram_top.v 和 sdram_init.v 加入工程进行编译,编译通过后检查仿真波形:
在这里插入图片描述
  仿真中的复位信号是延时 100ns 之后释放的,加上模块里的 200us 延时,正好 200100 ns 开始动作,仿真的波形变化正常,符合预期,且因为 SDRAM 仿真插件的缘故,可以在 Modelsim 的调试信息栏中看到仿真插件也识别出了 SDRAM 初始化模块的指令以及模式寄存器配置:
在这里插入图片描述

PS:根据初始化时序图要求,在给出模式寄存器的配置命令后还需要经过 tMRD 时间,也就是 2 个时钟周期以后才能进行 ACTIVE 操作,而初始化模块的完成标志实际上在进行模式寄存器配置的瞬间就拉高了。考虑到当前时间周期拉高,下一个周期信号能被其他模块正常接收,所以如果想要再稳妥一些,可以让初始化完成标志再晚 1 个时钟周期后拉高。

三、刷新模块

3.1 模块时序图

  Precharge 命令后实际上只需要给一次 Auto Refresh 命令就可以了,不用给两次:
在这里插入图片描述

3.2 模块源码

3.2.1 sdram_aref.v

module sdram_aref
(
    // system signals
    input 				    sclk	        ,       // 板载系统时钟 50MHz
	input                   s_rst_n         ,       // 复位信号,低电平有效
    
    // communicate with ARBIT
    input                   ref_en          ,
    output  wire            ref_req         ,
    output  wire            flag_ref_end    ,       // 刷新结束标志
    
    // others
    output  reg     [3:0]   aref_cmd        ,       // 刷新命令
    output  wire    [12:0]  sdram_addr      ,
    input                   flag_init_end
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
localparam      DELAY_7_8US     =       389     ;       // 64ms 刷新 8192 行,刷新周期为 64 * 1000 / 8192 = 7.8us,转换为 20ns 的时钟周期数约为 390 个

localparam      CMD_AREF        =       4'b0001 ;
localparam      CMD_NOP         =       4'b0111 ;
localparam      CMD_PRE         =       4'b0010 ;

reg [3:0]                   cmd_cnt             ;
reg [8:0]                   ref_cnt             ;       // 刷新时钟周期计数器
reg                         flag_ref            ;       // 刷新标志,表示刷新模块内部正处于刷新阶段

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
// ref_cnt:初始化结束开始计数,计满即清零
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        ref_cnt <= 'd0;
    else if(ref_cnt >= DELAY_7_8US)
        ref_cnt <= 'd0;
    else if(flag_init_end == 1'b1)
        ref_cnt <= ref_cnt + 1'b1;
end

// flag_ref:刷新结束后清零,仲裁允许刷新时进入刷新状态
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        flag_ref <= 1'b0;
    else if(flag_ref_end == 1'b1)
        flag_ref <= 1'b0;
    else if(ref_en == 1'b1)
        flag_ref <= 1'b1;
end

// cmd_cnt:刷新状态下保持自加
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        cmd_cnt <= 'd0;
    else if(flag_ref == 1'b1)
        cmd_cnt <= cmd_cnt + 1'b1;
    else
        cmd_cnt <= 'd0;
end

// aref_cmd
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        aref_cmd <= CMD_NOP;
    else
        case(cmd_cnt)
            1:          aref_cmd <=  CMD_PRE;
            2:          aref_cmd <=  CMD_AREF;
            default:    aref_cmd <=  CMD_NOP;
        endcase
end

assign  flag_ref_end    =   (cmd_cnt >= 'd5) ? 1'b1 : 1'b0;
assign  sdram_addr  =   13'b0_0100_0000_0000;
assign  ref_req =   (ref_cnt >= DELAY_7_8US) ?  1'b1 : 1'b0;

endmodule 

3.2.2 sdram_top.v

  修改 sdram_top.v 源码,在里面加上仲裁状态机(暂时先不考虑读和写):

module sdram_top
(
    // system signals
    input 				    sclk	    ,       // 板载系统时钟 50MHz
	input                   s_rst_n     ,       // 复位信号,低电平有效
    
    // SDRAM Interfaces
    output  wire            sdram_clk   ,
    output  wire            sdram_cke   ,
    output  wire            sdram_cs_n  ,
    output  wire            sdram_cas_n ,
    output  wire            sdram_ras_n ,
    output  wire            sdram_we_n  ,
    output  wire [1:0]      sdram_bank  ,
    output  wire [12:0]     sdram_addr  ,
    output  wire [1:0]      sdram_dqm   ,
    inout        [15:0]     sdram_dq
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
localparam          IDLE    =   5'b0_0001   ;   // 空闲状态
localparam          ARBIT   =   5'b0_0010   ;   // 仲裁状态
localparam          AREF    =   5'b0_0100   ;   // 刷新状态

// init module
wire                        flag_init_end   ;
wire    [3:0]               init_cmd        ;
wire    [12:0]              init_addr       ;

// 仲裁模块
reg     [4:0]               state           ;

// refresh module
wire                        ref_req         ;   // 刷新请求(刷新模块产生)
wire                        flag_ref_end    ;   // 刷新结束标志(刷新模块产生)
reg                         ref_en          ;   // 刷新使能(仲裁模块产生)
wire    [3:0]               ref_cmd         ;
wire    [12:0]              ref_addr        ;

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        state <= IDLE;
    else 
        case(state)
            IDLE:
                if(flag_init_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= IDLE;
            ARBIT:
                if(ref_en == 1'b1)
                    state <= AREF;
                else
                    state <= ARBIT;
            AREF:
                if(flag_ref_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= AREF;
            default:
                state <= IDLE;
        endcase
end

// ref_en
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        ref_en <= 1'b0;
    else if(state == ARBIT && ref_req == 1'b1)
        ref_en <= 1'b1;
    else
        ref_en <= 1'b0;
end

assign  sdram_cke       =   1'b1;
assign  sdram_addr      =   (state == IDLE) ? init_addr : ref_addr;
assign  {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} =    (state == IDLE) ? init_cmd : ref_cmd;
assign  sdram_dqm       =   2'b00;
assign  sdram_clk       =   ~sclk;     	// sdram 命令生成时钟与 sdram 命令采集时钟反向,保证命令采集时命令已经稳定生成

sdram_init sdram_init_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // others
    .cmd_reg        (init_cmd),         // 输出的命令(即 CS、RAS、CAS、WE 这四位)
    .sdram_addr     (init_addr),        // SDRAM 地址
    .flag_init_end  (flag_init_end)     // 初始化结束标志
);

sdram_aref sdram_aref_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // communicate with ARBIT
    .ref_en         (ref_en),
    .ref_req        (ref_req),
    .flag_ref_end   (flag_ref_end),     // 刷新结束标志
    
    // others
    .aref_cmd       (ref_cmd),          // 刷新命令
    .sdram_addr     (ref_addr),
    .flag_init_end  (flag_init_end)
);

endmodule 

  tb_sdram_top.v 与 sdram_model_plus 仿真插件无需修改,可直接使用。

3.3 Modelsim仿真

  检查单次刷新操作的时序波形,无异常。这里注意一下,有一个潜在隐患,即根据手册上给出的时序图,刷新操作中给出 Auto Refresh 命令后保持的 NOP 命令时长至少应为 66ns,也就是 4 个时钟周期(4 * 20ns = 80ns > 66ns),而跟着教程敲出来的代码在给出 Auto Refresh 命令后 2 个时钟周期就结束了刷新操作,退出刷新状态。对该问题已在代码中做了相应修改,可根据仿真波形图进行查验:
在这里插入图片描述
  刷新周期 7.8us 一次,15us 就是两次,细化到时间轴上,两次刷新间隔 215990 ns - 208190ns = 7800ns = 7.8us,满足设计要求:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值