以下内容学习自开源骚客教程: SDRAM那些事儿第一季。
一、模块时序图
时间参数和写模块是一样的:
二、模块源码
2.1 sdram_read.v
可以偷个懒直接修改 sdram_write.v 的副本。
module sdram_read
(
// system signals
input sclk , // 板载系统时钟 50MHz
input s_rst_n , // 复位信号,低电平有效
// communicate with ARBIT
input rd_en ,
output wire rd_req ,
output reg flag_rd_end ,
// others
input ref_req ,
input rd_trig ,
output reg [3:0] rd_cmd ,
output reg [12:0] rd_addr ,
output wire [1:0] bank_addr
);
/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// 状态定义
localparam S_IDLE = 5'b0_0001 ;
localparam S_REQ = 5'b0_0010 ;
localparam S_ACT = 5'b0_0100 ;
localparam S_RD = 5'b0_1000 ;
localparam S_PRE = 5'b1_0000 ;
// SDRAM Command
localparam CMD_NOP = 4'b0111 ;
localparam CMD_PRE = 4'b0010 ;
localparam CMD_AREF = 4'b0001 ;
localparam CMD_ACT = 4'b0011 ;
localparam CMD_RD = 4'b0101 ;
reg flag_rd ;
reg [4:0] state ;
// 大框架信号定义
reg flag_act_end ;
reg flag_pre_end ;
reg sd_row_end ;
reg [1:0] burst_cnt ;
reg [1:0] burst_cnt_t ;
reg rd_data_end ;
// 读命令产生信号定义
reg [3:0] act_cnt ;
reg [3:0] break_cnt ;
reg [6:0] col_cnt ;
reg [12:0] row_addr ;
wire [8:0] col_addr ;
/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
// flag_rd:外部提供读触发,且本身并不在模块读状态时进入模块读状态,数据读完时退出模块读状态
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
flag_rd <= 1'b0;
else if(rd_trig == 1'b1 && flag_rd == 1'b0)
flag_rd <= 1'b1;
else if(rd_data_end == 1'b1)
flag_rd <= 1'b0;
end
// burst_cnt:状态机读状态下才进行自加,2 位位宽(一个 CLK 对应一个数据)
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
burst_cnt <= 'd0;
else if(state == S_RD)
burst_cnt <= burst_cnt + 1'b1;
else
burst_cnt <= 'd0;
end
// burst_cnt_t
always @(posedge sclk)
begin
burst_cnt_t <= burst_cnt;
end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
state <= S_IDLE;
else
case(state)
S_IDLE:
if(rd_trig == 1'b1)
state <= S_REQ;
else
state <= S_IDLE;
S_REQ:
if(rd_en == 1'b1)
state <= S_ACT;
else
state <= S_REQ;
S_ACT:
if(flag_act_end == 1'b1)
state <= S_RD;
else
state <= S_ACT;
S_RD:
if(rd_data_end == 1'b1) // 数据写完了
state <= S_PRE;
else if(ref_req == 1'b1 && burst_cnt == 'd3 && flag_rd == 1'b1) // 模块写状态下需要进行刷新,且已经写完了当前批次的 burst
state <= S_PRE;
else if(sd_row_end == 1'b1 && flag_rd == 1'b1) // 写完一行了但是还在模块写状态(数据没写完)
state <= S_PRE;
S_PRE:
if(ref_req == 1'b1 && flag_rd == 1'b1) // 这里可以继续使用 ref_req 信号来判断的原因是顶层仲裁模块的状态还没有进行切换,刷新请求就会一直保持
state <= S_REQ;
/* 这个位置的跳转条件不够严谨,且此处状态机设计与写模块内部时序图设计实际上存在相悖之处。
** 状态机下从 S_PRE 跳转到 S_ACT 的条件是 sd_row_end 信号,而在时序图中 sd_row_end 信
** 号在从 S_RD 跳出的时刻就动作了,所以不能作为这里的直接判断依据。而 flag_pre_end 信号
** 只是 S_PRE 结束的标志信号而已,并非是 “换行退出状态机写状态” 的唯一标志。
*/
else if(flag_pre_end == 1'b1 && flag_rd == 1'b1)
state <= S_ACT;
else if(rd_data_end == 1'b1)
state <= S_IDLE;
default:
state <= S_IDLE;
endcase
end
// rd_cmd:只在各状态的一开始才给命令
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rd_cmd <= CMD_NOP;
else
case(state)
S_ACT:
if(act_cnt == 'd0)
rd_cmd <= CMD_ACT;
else
rd_cmd <= CMD_NOP;
S_RD:
if(burst_cnt == 'd0)
rd_cmd <= CMD_RD;
else
rd_cmd <= CMD_NOP;
S_PRE:
if(break_cnt == 'd0)
rd_cmd <= CMD_PRE;
else
rd_cmd <= CMD_NOP;
default:
rd_cmd <= CMD_NOP;
endcase
end
// rd_addr:S_ACT 状态给行地址,S_RD 状态给列地址
always @(*)
begin
case(state)
S_ACT:
if(act_cnt == 'd0)
rd_addr <= row_addr;
S_RD:
rd_addr <= {4'b0000,col_addr};
S_PRE:
if(break_cnt == 'd0)
rd_addr <= 13'b0_0100_0000_0000;
endcase
end
// flag_act_end:在 S_ACT 状态计时 3 个 CLK
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
flag_act_end <= 1'b0;
else if(act_cnt == 'd3)
flag_act_end <= 1'b1;
else
flag_act_end <= 1'b0;
end
// act_cnt
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
act_cnt <= 'd0;
else if(state == S_ACT)
act_cnt <= act_cnt + 1'b1;
else
act_cnt <= 'd0;
end
// flag_pre_end:在 S_PRE 状态计时 3 个 CLK
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
flag_pre_end <= 1'b0;
else if(break_cnt == 'd3)
flag_pre_end <= 1'b1;
else
flag_pre_end <= 1'b0;
end
// flag_rd_end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
flag_rd_end <= 1'b0;
else if((state == S_PRE && ref_req == 1'b1) || // 仿真调试后的判断条件
(state == S_PRE && rd_data_end == 1'b1))
flag_rd_end <= 1'b1;
else
flag_rd_end <= 1'b0;
end
// break_cnt
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
break_cnt <= 'd0;
else if(state == S_PRE)
break_cnt <= break_cnt + 1'b1;
else
break_cnt <= 'd0;
end
// rd_data_end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rd_data_end <= 1'b0;
else if(row_addr == 'd2 && col_addr == 'd511) // 写两行数据特有标志(第二行也写完了才算数据写完)
rd_data_end <= 1'b1;
else
rd_data_end <= 1'b0;
end
// col_cnt:实际上被视为一个四进制计数器的高位(低位是 burst_cnt_t 的 0~3)
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
col_cnt <= 'd0;
else if(col_addr == 'd511)
col_cnt <= 'd0;
else if(burst_cnt_t == 'd3)
col_cnt <= col_cnt + 1'b1;
end
// sd_row_end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
sd_row_end <= 1'b0;
else if(col_addr == 'd509) // 这里提前是因为 col_addr 使用的是 burst_cnt 延拍
sd_row_end <= 1'b1;
else
sd_row_end <= 1'b0;
end
// row_addr
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
row_addr <= 'd0;
else if(sd_row_end == 1'b1)
row_addr <= row_addr + 1'b1;
end
assign col_addr = {col_cnt,burst_cnt_t};
assign bank_addr = 2'b00;
assign rd_req = state[1];
endmodule
2.2 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 reg [12:0] sdram_addr ,
output wire [1:0] sdram_dqm ,
inout [15:0] sdram_dq ,
//others
input wr_trig , // 仿真测试用
input rd_trig
);
/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
localparam IDLE = 5'b0_0001 ; // 空闲状态
localparam ARBIT = 5'b0_0010 ; // 仲裁状态
localparam AREF = 5'b0_0100 ; // 刷新状态
localparam WRITE = 5'b0_1000 ; // 写状态
localparam READ = 5'b1_0000 ; // 读状态
reg [3:0] sd_cmd ;
// 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 ;
// write module
reg wr_en ;
wire wr_req ;
wire flag_wr_end ;
wire [3:0] wr_cmd ;
wire [12:0] wr_addr ;
wire [1:0] wr_bank_addr ;
wire [15:0] wr_data ;
// read module
reg rd_en ;
wire rd_req ;
wire flag_rd_end ;
wire [3:0] rd_cmd ;
wire [12:0] rd_addr ;
wire [1:0] rd_bank_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 if(wr_en == 1'b1)
state <= WRITE;
else if(rd_en == 1'b1) // 写优先级高于读
state <= READ;
else
state <= ARBIT;
AREF:
if(flag_ref_end == 1'b1)
state <= ARBIT;
else
state <= AREF;
WRITE:
if(flag_wr_end == 1'b1)
state <= ARBIT;
else
state <= WRITE;
READ:
if(flag_rd_end == 1'b1)
state <= ARBIT;
else
state <= READ;
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
// wr_en
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
wr_en <= 1'b0;
else if(state == ARBIT && ref_req == 1'b0 && wr_req == 1'b1) // 只有在不和刷新操作冲突的时候才允许进行写操作
wr_en <= 1'b1;
else
wr_en <= 1'b0;
end
// rd_en
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rd_en <= 1'b0;
else if(state == ARBIT && ref_req == 1'b0 && wr_req == 1'b0 && rd_req == 1'b1) // 只有在不和刷新操作、写操作冲突的时候才允许进行写操作
rd_en <= 1'b1;
else
rd_en <= 1'b0;
end
// sd_cmd
always @(*)
begin
case(state)
IDLE:
begin
sd_cmd <= init_cmd;
sdram_addr <= init_addr;
end
AREF:
begin
sd_cmd <= ref_cmd;
sdram_addr <= ref_addr;
end
WRITE:
begin
sd_cmd <= wr_cmd;
sdram_addr <= wr_addr;
end
READ:
begin
sd_cmd <= rd_cmd;
sdram_addr <= rd_addr;
end
default:
begin
sd_cmd <= 4'b0111; // NOP
sdram_addr <= 'd0;
end
endcase
end
assign sdram_cke = 1'b1;
assign {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} = sd_cmd;
assign sdram_dqm = 2'b00;
assign sdram_clk = ~sclk; // sdram 命令生成时钟与 sdram 命令采集时钟反向,保证命令采集时命令已经稳定生成
assign sdram_dq = (state == WRITE) ? wr_data : {16{1'bz}};
assign sdram_bank = (state == WRITE) ? wr_bank_addr : rd_bank_addr;
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)
);
sdram_write sdram_write_inst
(
// system signals
.sclk (sclk), // 板载系统时钟 50MHz
.s_rst_n (s_rst_n), // 复位信号,低电平有效
// communicate with ARBIT
.wr_en (wr_en),
.wr_req (wr_req),
.flag_wr_end (flag_wr_end),
// others
.ref_req (ref_req),
.wr_trig (wr_trig),
.wr_cmd (wr_cmd),
.wr_addr (wr_addr),
.bank_addr (wr_bank_addr),
.wr_data (wr_data)
);
sdram_read sdram_read_inst
(
// system signals
.sclk (sclk), // 板载系统时钟 50MHz
.s_rst_n (s_rst_n), // 复位信号,低电平有效
// communicate with ARBIT
.rd_en (rd_en),
.rd_req (rd_req),
.flag_rd_end (flag_rd_end),
// others
.ref_req (ref_req),
.rd_trig (rd_trig),
.rd_cmd (rd_cmd),
.rd_addr (rd_addr),
.bank_addr (rd_bank_addr)
);
endmodule
2.3 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 ;
reg wr_trig ;
reg rd_trig ;
initial
begin
wr_trig <= 0;
rd_trig <= 0;
#205000
wr_trig <= 1;
#20
wr_trig <= 0;
#25000
rd_trig <= 1;
#20
rd_trig <= 0;
end
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),
//ohters
.wr_trig (wr_trig),
.rd_trig (rd_trig)
);
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
三、Modelsim仿真
3.1 run.do脚本
同样是在写模块的基础上做些小修改:
// run.do 源码
# 在当前路径下创建 work 文件夹
vlib work
# 编译下列文件
vlog "./tb_sdram_top.v"
vlog "./sdram_model_plus.v"
vlog "../../rtl/sdram/*.v"
# 优化部分参数(-voptargs=+acc),链接到默认的 work 库,启动仿真顶层测试逻辑库(work)里面的 tb_sdram_top 文件
vsim -voptargs=+acc work.tb_sdram_top
# Set the window types
view wave
view structure
view signals
# 添加分割线,在不同的信号之间进行分割,语法格式是:add wave -divider {分割线的名字}
add wave -divider {sdram_top}
# 添加指定例化模块下的所有信号
add wave tb_sdram_top/sdram_top_inst/*
add wave -divider {sdram_write}
add wave tb_sdram_top/sdram_top_inst/sdram_write_inst/*
add wave -divider {sdram_read}
add wave tb_sdram_top/sdram_top_inst/sdram_read_inst/*
add wave -divider {sdram_model}
add wave tb_sdram_top/sdram_model_plus_inst/*
# 清屏
.main clear
run 205us
3.2 补充
仿真波形就不贴了,这里只简单提一下在读数据时出现的特有现象,即从读状态退出到仲裁状态时,在给了 Precharge 命令后还是出来了两个数据,这属于正常现象(因为有潜伏期,即手册中的参数 CL):