目录
以下内容学习自开源骚客教程: SDRAM那些事儿第一季。
一、数据手册相关信息
1.1 命令真值表
关于这个表,博主特意核对了好几款不同型号的 SDRAM,结果不能说完全相同,只能说高度一致,最多在一些可有可无的小功能上有所区别,貌似在遵循公共的协议一样。
把需要用到的抓出来:
Cmd | CS | RAS | CAS | WE |
---|---|---|---|---|
Precharge | 0 | 0 | 1 | 0 |
Auto-Refresh | 0 | 0 | 0 | 1 |
NOP | 0 | 1 | 1 | 1 |
Mode-Set | 0 | 0 | 0 | 0 |
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,满足设计要求: