前言
“温故而知新,可以为师矣。” 回首一年前初涉 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。
二、引脚介绍
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自动刷新模块的介绍。