【SDRAM】从官方数据手册出发,使用verilog实现SDRAM控制器(初始化部分)

前言

“温故而知新,可以为师矣。” 回首一年前初涉 FPGA 学习领域之际,首个学习项的目便是 AD 采集卡,当时就用到了 SDRAM 控制器来进行数据存储。忆及那段学习历程,实可谓备受煎熬、苦不堪言,然不可否认的是,自身能力亦获大幅提升。现在重新审视这段过往,我打算开启一个专栏,再次深入探索 SDRAM 控制器。这一次,我将从最基础的部分开始,一步一个脚印,扎扎实实地重新构建对它的理解与应用。(当时学习的时候参考的是野火的SDRAM教程,关于SDRAM的介绍可以点击跳转查看,SDRAM控制器实现

一、SDRAM存储器类型

本文以MT48LC16M16A2 – 4 Meg x 16 x 4 banks 为例,一步一步介绍SDRAM控制器的实现。想要对SDRAM存储器更深入的了解,最好的办法就是阅读官方的数据手册,后文出现的英文部分均来自于该芯片的数据手册。数据手册以及SDRAM控制器源码我已上传至Github,点击跳转
Each of the x16’s 67,108,864-bit banks is organized as 8,192 rows by 512 columns by 16 bits.(每一个bank包括8192行和512列的存储单元)。该存储器的数据位宽为16bit。
SDRAM框图

二、引脚介绍

1、CLK时钟引脚,CKE为高电平时,时钟信号有效,CS#,WE#,CAS#,RAS#的不同组合构成了不同的SDRAM指令集。在SDRAM初始化以及数据读写阶段会使用到不同的指令,每个指令对应的CS#,WE#,CAS#,RAS#电平见下图,在后文会介绍初始化阶段用到的指令含义

指令集

2、23-26,29-34,22,35,36这13个引脚控制了行地址,列地址,以及预充电时对哪个bank进行预充电,A0-A12代表行地址,A0-A8代表列地址,A10决定预充电的bank,通过行地址和列地址可以唯一确定存储单元。
引脚

3、BA0和BA1可以确定激活4个bank中的哪一个
引脚

4、DQ数据输入/输出引脚,16位宽,其是双向的
引脚

三、初始化流程

下面给出官方数据手册关于初始化部分的介绍,我会给出英文原文并附上对应的理解。
SDRAMs must be powered up and initialized in a predefined manner. Operational procedures other than those specified may result in undefined operation. Once power is applied to VDD and VDDQ (simultaneously) and the clock is stable (stable clock is defined as a signal cycling within timing constraints specified for the clock pin), the SDRAM requires a 100µs delay prior to issuing any command other than a COMMAND INHIBIT or NOP. Starting at some point during this 100µs period and continuing at least through the end of this period, COMMAND INHIBIT or NOP commands should be applied.

这段话的意思是上电并且时钟稳定后,需要一个100us的等待时间,即保持NOP命令100us。

Once the 100µs delay has been satisfied with at least one COMMAND INHIBIT or NOP command having been applied, a PRECHARGE command should be applied. All banks must then be precharged, thereby placing the device in the all banks idle state.

在保持NOP命令100us后,应施加预充电(PRECHARGE)命令。随后,所有bank必须进行预充电,从而将sdram置于所有bank都处于初始化状态。

Once in the idle state, two AUTO REFRESH cycles must be performed. After the AUTO REFRESH cycles are complete, the SDRAM is ready for mode register programming. Because the mode register will power up in an unknown state, it should be loaded prior to applying any operational command.

预充电命令完成(将sdram置于所有bank都处于初始化状态)后,必须执行两个自动刷新(AUTO REFRESH)周期。在执行完两个自动刷新命令后,就可以进行模式寄存器配置

总结一下初始化命令发送顺序:空(NOP)命令→预充电(PRECHARGE)命令→自动刷新(AUTO REFRESH)命令→模式寄存器配置(LOAD MODE REGISTER)命令

下面给出用到的每个命令的介绍

空(NOP)命令

The NO OPERATION (NOP) command is used to perform a NOP to an SDRAM which is selected (CS# is LOW). This prevents unwanted commands from being registered during idle or wait states. Operations already in progress are not affected.

空操作(NOP)命令用于对被选中的 SDRAM(CS# 为 LOW)执行空操作。这样可以防止在空闲或等待状态期间注册不必要的命令。已经进行中的操作不会受到影响。

空命令何时使用?在SDRAM空闲时我们发送的均是空(NOP)命令

预充电(PRECHARGE)命令

The PRECHARGE command is used to deactivate the open row in a particular bank or the open row in all banks. The bank(s) will be available for a subsequent row access a specified time (tRP) after the PRECHARGE command is issued. Input A10 determines whether one or all banks are to be precharged, and in the case where only one bank is to be precharged, inputs BA0, BA1 select the bank. Otherwise, BA0, BA1 are treated as “Don’t Care.” Once a bank has been precharged, it is in the idle state and must be activated prior to any READ or WRITE commands being issued to that bank.

预充电命令会使打开的行处于非激活状态。预充电(PRECHARGE)命令发出后,需要等待tRP时间,才可以执行下一个命令;输入 A10 决定是预充电单个bank还是所有bank;如果只需预充电一个bank,则输入 BA0、BA1 选择该bank。A10为高电平代表对所有bank进行预充电 一旦一个bank被预充电后,它会处于初始化状态,在执行读写命令之前必须使其先激活。

预充电命令何时使用?根据手册介绍,在自动刷新命令之前需要先执行预充电命令,在执行完预充电命令后需要等待tRP时间才可以执行下一个命令

自动刷新(AUTO REFRESH)命令

AUTO REFRESH is used during normal operation of the SDRAM and is analogous to CAS#-BEFORE-RAS# (CBR) REFRESH in conventional DRAMs. This command is nonpersistent, so it must be issued each time a refresh is required. All active banks must be precharged prior to issuing an AUTO REFRESH command. The AUTO REFRESH command should not be issued until the minimum tRP has been met after the PRECHARGE command as shown in the operations section.

自动刷新用于 SDRAM 的正常操作,类似于传统 DRAM 中的 CAS#-BEFORE-RAS#(CBR)刷新。此命令是非持久性的,因此每次需要刷新时都必须发出该命令。在发出自动刷新命令之前,所有激活的bank必须先进行预充电命令顺序 先预充电 在自动刷新 )。自动刷新命令不应在预充电命令后的最小 tRP 时间未满足之前发出

The addressing is generated by the internal refresh controller. This makes the address bits “Don’t Care” during an AUTO REFRESH command. The 256Mb SDRAM requires 8,192 AUTO REFRESH cycles every 64ms (tREF), regardless of width option. Providing a distributed AUTO REFRESH command every 7.81µs will meet the refresh requirement and ensure that each row is refreshed. Alternatively, 8,192 AUTO REFRESH commands can be issued in a burst at the minimum cycle rate (tRFC), once every 64ms.

地址由内部刷新控制器生成。因此,在自动刷新命令期间,地址位是“无关的”。256Mb SDRAM 每 64 毫秒(tREF)需要 8,192 次自动刷新周期,无论宽度选项如何(这句话的意思是不管SDRAM的数据位宽是16bit还是8bit、4bit,它们均是8192行,都需要每64 毫秒(tREF)进行8,192 次自动刷新)。每 7.81 微秒发出一次自动刷新命令,将满足刷新要求并确保每行都得到刷新。另外,在64ms内,这8192次自动刷新命令之间的最小间隔为tRFC

自动刷新命令何时使用?根据手册介绍,在初始化阶段需要执行两次自动刷新命令,除此之外,每 64 毫秒(tREF)需要 8,192 次自动刷新(这一部分我们单独放在自动刷新模块实现),在执行完自动刷新命令后,需要等待tRFC的时间才能执行下一个命令

模式寄存器配置(LOAD MODE REGISTER)命令

The mode register is loaded via inputs A0-A11 (A12 should be driven LOW.) See mode register heading in the Register Definition section. The LOAD MODE REGISTER command can only be issued when all banks are idle, and a subsequent executable command cannot be issued until tMRD is met.

模式寄存器通过输入 A0-A11 加载(A12 应拉低)。发出模式寄存器配置命令后,经过 tMRD 时间才可以执行下一个命令

模式寄存器位 M0–M2 指定突发长度,M3 指定突发类型(顺序或交错),M4–M6 指定 CAS 延迟,M7 和 M8 指定操作模式,M9 指定写入突发模式,M10 和 M11 保留供未来使用。地址 A12(M12)未定义,但在加载模式寄存器时应拉低。每一位对应的含义如下图所示。
在这里插入图片描述
在这里插入图片描述
假设在模式寄存器配置时,对A12~A0的设置为:

init_addr<={
               3'b000,//A12-A10,保留位
               1'b0,//A9设置为0,读写方式为突发读和突发写
               2'b00,//A8,A7参考芯片手册设置为00,默认
               3'b011,//A6,A5,A4设置011,则CAS latency为3
               1'b0,//A3为0,突发传输方式为顺序传输
               3'b111//A2-A0,突发长度为全页
         };

对应的含义见注释。

模式寄存器配置命令何时使用?根据手册介绍,在初始化阶段执行完自动刷新后,需要进行模式寄存器配置,配置SDRAM的读写方式,突发长度、CAS latency等参数,相关参数在用到时我们在进行介绍。发出模式寄存器配置命令后,需要等待 tMRD 时间才可以执行下一个命令

初始化流程总结

在这里插入图片描述

根据上文的介绍以及该图可知,SDRAM初始化流程如下:

上电→100us空命令→预充电命令→等待tRP时间→自动刷新命令→等待tRFC时间→自动刷新命令→等待tRFC时间→模式寄存器配置命令→等待tMRD时间→初始化结束。

关于tRP、tRFC、tMRD的时间也可在数据手册中直接查找得到。在代码中设置如下

 //各个阶段等待的周期数,一个时钟周期为10ns,0.01us
    localparam INIT_WAIT_CLK  = 10_000 ; //100us
    localparam INIT_TRP_CLK   = 2      ; //根据数据手册可知,最小时间为20ns
    localparam INIT_TRFC_CLK  = 7      ; //根据数据手册可知,最小时间为66ns
    localparam INIT_TMRD_CLK  = 2      ; //根据数据手册可知,最小时间为2个CLK

四、代码设计

根据分析可知,在初始化阶段各个状态跳转已经非常明确了,我们可以使用状态机去设计初始化流程,并设计一个计数器用于判断每个命令等待的时间是否满足,如果满足就可以跳转至下一个状态。完整的初始化阶段代码以及仿真测试文件可以从github下载,SDRAM-Controller

状态机定义如下

//初始化阶段的状态机
    localparam INIT_IDLE  = 3'b000 ;//初始状态,等待100us跳转至预充电状态
    localparam INIT_PRE   = 3'b001 ;//预充电命令
    localparam INIT_TRP   = 3'b010 ;//预充电状态,等待tRP时间跳转至自动刷新
    localparam INIT_A_R   = 3'b011 ;//自动刷新命令
    localparam INIT_TRFC  = 3'b100 ;//等待tRFC时间,两次自动刷新后跳转至模式寄存器配置命令
    localparam INIT_LMR   = 3'b101 ;//模式寄存器配置命令
    localparam INIT_TMRD  = 3'b110 ;//等待tMRD时间,初始化结束
    localparam INIT_END   = 3'b111 ;//初始化结束

sdram_init.v模块代码

/*
 * @Author: bit_stream 
 * @Date: 2024-12-12 10:51:27 
 * @Last Modified by:   bit_stream 
 * @Last Modified time: 2024-12-12 10:51:27 
 */

`timescale  1ns/1ns

 //初始化流程如下
//上电→100us空命令→预充电命令→等待tRP时间→自动刷新命令→等待tRFC时间→自动刷新命令→等待tRFC时间→模式寄存器配置命令→等待tMRD时间→初始化结束。
module sdram_init (
    input   wire            sys_clk     ,   //系统时钟,频率100MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效

    output  reg     [3:0]   init_cmd    ,   //初始化阶段写入sdram的指令,对应的引脚为{cs#,ras#,cas#,we#}
    output  reg     [1:0]   init_ba     ,   //初始化阶段Bank地址
    output  reg     [12:0]  init_addr   ,   //初始化阶段地址数据,辅助预充电操作
                                            //和配置模式寄存器操作,A12-A0,共13位
    output  reg            init_end        //初始化结束信号
);
    //初始化阶段使用到的SDRAM  指令集
    localparam NOP                = 4'b0111 ;//空命令
    localparam PRECHARGE          = 4'b0010 ;//预充电命令
    localparam AUTO_REFRESH       = 4'b0001 ;//自动刷新命令
    localparam LOAD_MODE_REGISTER = 4'b0000 ;//模式寄存器配置命令
    
    //初始化阶段的状态机
    localparam INIT_IDLE  = 3'b000 ;//初始状态,等待100us跳转至预充电状态
    localparam INIT_PRE   = 3'b001 ;//预充电命令
    localparam INIT_TRP   = 3'b010 ;//预充电状态,等待tRP时间跳转至自动刷新
    localparam INIT_A_R   = 3'b011 ;//自动刷新命令
    localparam INIT_TRFC  = 3'b100 ;//等待tRFC时间,两次自动刷新后跳转至模式寄存器配置命令
    localparam INIT_LMR   = 3'b101 ;//模式寄存器配置命令
    localparam INIT_TMRD  = 3'b110 ;//等待tMRD时间,初始化结束
    localparam INIT_END   = 3'b111 ;//初始化结束

    //各个阶段等待的周期数,一个时钟周期为10ns,0.01us
    localparam INIT_WAIT_CLK  = 10_000 ; //100us
    localparam INIT_TRP_CLK   = 2      ; //根据数据手册可知,最小时间为20ns
    localparam INIT_TRFC_CLK  = 7      ; //根据数据手册可知,最小时间为66ns
    localparam INIT_TMRD_CLK  = 2      ; //根据数据手册可知,最小时间为2个CLK

    //自动刷新次数
    localparam INIT_A_R_TIME  = 2 ;


    //定义状态机
    reg [2:0] init_state;

    //定义计数器,计数需要等待的时间
    reg [14:0] cnt_clk;

    //定义计数器使能信号,在特定的状态使能计数器计数
    reg cnt_clk_en; //其为1时,使能计数,其为0时,计数器清零

    always @(posedge sys_clk) begin
        if (~sys_rst_n) begin
            cnt_clk <= 'd0;
        end else if (cnt_clk_en) begin
            cnt_clk <= cnt_clk + 1'b1;
        end else if (~cnt_clk_en) begin
            cnt_clk <= 'd0;
        end 
    end 


    always @(*) begin //信号立刻变化
        if (~sys_rst_n) begin
            cnt_clk_en <= 0;
        end else begin
            case (init_state)
                INIT_IDLE,INIT_TRP,INIT_TRFC,INIT_TMRD:begin
                    cnt_clk_en <= 1'b1;
                end 
                INIT_PRE,INIT_A_R,INIT_LMR,INIT_END:begin
                    cnt_clk_en <= 1'b0;
                end
                default: begin
                    cnt_clk_en <= cnt_clk_en;
                end
            endcase
        end
    end

    //定义计数器,计数自动刷新的次数
    reg [1:0] cnt_a_r_times;
    always @(posedge sys_clk) begin
        if (~sys_rst_n) begin
            cnt_a_r_times <= 'd0;
        end else if (init_state == INIT_A_R) begin
            cnt_a_r_times <= cnt_a_r_times + 1'b1;
        end
    end



    
    always @(posedge sys_clk) begin
        if (~sys_rst_n) begin
            init_state <= INIT_IDLE;
        end else begin
            case (init_state)
                INIT_IDLE:begin
                    if (cnt_clk == INIT_WAIT_CLK - 'd1) begin 
                        init_state <= INIT_PRE;
                    end else begin
                        init_state <= INIT_IDLE;
                    end
                end 
                INIT_PRE:begin
                    init_state <= INIT_TRP;
                end
                INIT_TRP:begin
                    if (cnt_clk == INIT_TRP_CLK - 'd1) begin
                        init_state <= INIT_A_R;
                    end else begin
                        init_state <= INIT_TRP;
                    end
                end
                INIT_A_R:begin
                    init_state <= INIT_TRFC;
                end
                INIT_TRFC:begin
                    if ((cnt_clk == INIT_TRFC_CLK - 'd1)&&(cnt_a_r_times < INIT_A_R_TIME)) begin
                        init_state <= INIT_A_R;
                    end else if ((cnt_clk == INIT_TRFC_CLK - 'd1)&&(cnt_a_r_times == INIT_A_R_TIME)) begin
                        init_state <= INIT_LMR;
                    end else begin
                        init_state <= INIT_TRFC;
                    end
                end
                INIT_LMR:begin
                    init_state <= INIT_TMRD;
                end
                INIT_TMRD:begin
                    if (cnt_clk == INIT_TMRD_CLK - 'd1) begin
                        init_state <= INIT_END;
                    end else begin
                        init_state <= INIT_TMRD;
                    end
                end
                INIT_END:begin
                    init_state <= INIT_END;
                end
                default: begin
                    init_state <= init_state;
                end
            endcase
        end
    end


    always @(posedge sys_clk) begin
        if (~sys_rst_n) begin
            init_cmd  <= NOP;
            init_ba   <= 2'b11;
            init_addr <= 13'h1fff;
            init_end  <= 1'b0;
        end else begin
            case (init_state)
                INIT_IDLE:begin
                    init_cmd  <= NOP;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end 
                INIT_PRE:begin //A10为1,所有bank进行预充电
                    init_cmd  <= PRECHARGE;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end 
                INIT_TRP:begin
                    init_cmd  <= NOP;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end 
                INIT_A_R:begin
                    init_cmd  <= AUTO_REFRESH;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end 
                INIT_TRFC:begin
                    init_cmd  <= NOP;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end 
                INIT_LMR:begin
                    init_cmd  <= LOAD_MODE_REGISTER;
                    init_ba   <= 2'b11;
                    init_addr<={
                        3'b000,//A12-A10,保留位
                        1'b0,//A9设置为0,读写方式为突发读和突发写
                        2'b00,//A8,A7参考芯片手册设置为00,默认
                        3'b011,//A6,A5,A4设置011,则CAS latency为3
                        1'b0,//A3为0,突发传输方式为顺序传输
                        3'b111//A2-A0,突发长度为全页
                    };
                    init_end  <= 1'b0;
                end 
                INIT_TMRD:begin
                    init_cmd  <= NOP;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b0;
                end
                INIT_END:begin
                    init_cmd  <= NOP;
                    init_ba   <= 2'b11;
                    init_addr <= 13'h1fff;
                    init_end  <= 1'b1;
                end
                default: begin
                    init_cmd  <= init_cmd;
                    init_ba   <= init_ba;
                    init_addr <= init_addr;
                    init_end  <= init_end;
                end
            endcase
        end
    end

    
endmodule

五、Modelsim仿真结果

在这里插入图片描述
在这里插入图片描述

总结

以上是关于SDRAM控制器初始化阶段的内容,下一篇进行SDRAM自动刷新模块的介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值