SDRAM驱动接口
SDRAM驱动模块接口大概设计为这种形式,用户侧发送过来操作的指令(写/读),地址(包括bank地址和行列地址),长度以及握手信号,驱动模块解析完成后发送到芯片引脚上。其中读写数据用的同一个inout接口。写数据是突发写模式,字节连续被写入,所以是数据流的形式,就不用ready信号,只用一个last信号指示数据流发送完成。
参数方面设置了几个重要的时序参数,比如TRCD,CL,TWR等。行数为4096。
module SDRAM_Drive#(
parameter P_SDRAM_ROW_NUM = 4096 ,
P_TIME_INIT = 20000 ,//10nS
P_TIME_TRP = 2 ,//10ns 预充电时间
P_TIME_TRC = 6 ,//10ns 刷新时间间隔
P_TIME_TRSC = 2 ,//cycle
P_TIME_TRCD = 2 ,
P_TIME_CL = 2 ,
P_TIME_TWR = 2
)(
input i_clk ,
input i_rst ,
input [1 :0] i_operation_cmd ,//操作指令 1-读 2-写
input [23:0] i_operation_addr ,//操作地址-SDRAM中的存储地址 bank+row+col = 2+13+12
input [9 :0] i_operation_len ,//操作长度-读写数据的长度
input i_operation_valid ,//操作有效信号-握手
output o_operation_ready ,//操作准备信号-准备
input [15:0] i_write_data ,//写数据
input i_write_last ,//写数据last
input i_write_valid ,//写数据有效
output [15:0] o_read_data ,//读数据
output o_read_valid ,//读数据有效
output o_sdram_cke ,//sdram的时钟有效
output o_sdram_cs_n ,//sdram的片选
output o_sdram_ras_n ,//sdram的行有效
output o_sdram_cas_n ,//sdram的列有效
output o_sdram_we_n ,//sdram的写使能
output [12:0] o_sdram_addr ,//sdram的操作地址
output [1 :0] o_sdram_ba ,//sdram的Bank地址
inout [15:0] io_sdram_data ,//sdram的读写数据
output [1 :0] o_sdram_dqm
);
wire w_operation_active ;//握手信号
assign w_operation_active = i_operation_valid & o_operation_ready;
状态机参数设置
状态转移图:
状态机跳转:
always@(*)
begin
case(r_st_current)
P_INIT_WAIT : r_st_next = r_st_cnt == P_TIME_INIT ? P_ALL_PRE : P_INIT_WAIT ;
P_ALL_PRE : r_st_next = r_st_cnt == P_TIME_TRP ? P_AREFRESH : P_ALL_PRE ;
P_AREFRESH : r_st_next = r_st_cnt == P_TIME_TRC ? P_AR_CHECK : P_AREFRESH ;
P_AR_CHECK : r_st_next = r_ar_cnt == 7 ? P_MR_SET : P_AREFRESH ;
P_MR_SET : r_st_next = r_st_cnt == P_TIME_TRSC ? P_IDLE : P_MR_SET ;
P_IDLE : r_st_next = r_ap_req_cnt == P_AP_CNT ? P_IDLE_AR :
w_operation_active ? P_ROW_ACT : P_IDLE;
P_IDLE_AR : r_st_next = r_st_cnt == P_TIME_TRC ? P_IDLE : P_IDLE_AR ;
P_ROW_ACT : r_st_next = r_st_cnt == P_TIME_TRCD ?
ri_operation_cmd == 'd1 ? P_READ_CMD : P_WRITE_CMD
: P_ROW_ACT ;
P_READ_CMD : r_st_next = r_st_cnt == P_TIME_CL ? P_READ_DATA : P_READ_CMD ;
P_READ_DATA : r_st_next = r_st_cnt == ri_operation_len - 1 ? P_BURST_STOP : P_READ_DATA ;
P_WRITE_CMD : r_st_next = P_WRITE_DATA;
P_WRITE_DATA : r_st_next = r_st_cnt == ri_operation_len ? P_BURST_STOP : P_WRITE_DATA ;
P_WRITE_WAIT : r_st_next = r_st_cnt == P_TIME_TWR + 1 ? P_PER_WAIT : P_WRITE_WAIT ;
P_BURST_STOP : r_st_next = r_st_cnt == P_TIME_CL ? P_PER_WAIT : P_BURST_STOP ;
P_PER_WAIT : r_st_next = r_st_cnt == P_TIME_TRP + 5 ? P_IDLE : P_PER_WAIT ;
default : r_st_next = 'd0;
endcase
end
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
r_st_cnt <= 'd0;
else if(r_st_current != r_st_next)
r_st_cnt <= 'd0;
else
r_st_cnt <= r_st_cnt + 1;
end
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
r_ar_cnt <= 'd0;
else if(r_st_current == P_AR_CHECK)
r_ar_cnt <= r_ar_cnt + 1;
else
r_ar_cnt <= r_ar_cnt;
end
r_st_cnt为状态计数器,主要是用来计数比如静默200us,预充电时间,自动刷新周期这些,所以状态跳转时清零。r_ar_cnt为刷新计数器,用来计数刷新次数是否到了8次。
r_ap_req_cnt为刷新请求计数器,它有什么作用呢?因为目前SDRAM的存储电容的刷新周期最大为64ms,由于自刷新指令一次只能刷新一行,因此我们要在64ms内刷新完所有行。SDRAM的行数为4096,那么就需要在64ms内刷新4096次,我们在64ms内均匀的刷新4096行,那么自刷新的指令间隔:64ms/4096 = 15.6us。所以这个计数器计数到15.6us后控制状态机跳转到刷新状态。
localparam P_AP_CNT = (6400000/P_SDRAM_ROW_NUM) - 600;//15.6US - 6US = 13.6US 10nS
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
r_ap_req_cnt <= 'd0;
else if(r_st_current == P_IDLE_AR)
r_ap_req_cnt <= 'd0;
else if(r_st_current > P_MR_SET && r_ap_req_cnt < P_AP_CNT)
r_ap_req_cnt <= r_ap_req_cnt + 1;
else
r_ap_req_cnt <= r_ap_req_cnt;
end
值得注意的是,如果计数到15.6us时,此时SDRAM正工作在写或者读状态,那么就跳不到刷新状态。解决方法是将计数器计数值降低,减去读写最大耗费的时间。比如假设此时工作在读状态,读完数据最多需要100ns,那就把值设置为15.6us - 100ns,这样即使等待读写完成,也不会超出最大刷新周期。
指令和地址控制
localparam P_CMD_INIT = 5'b01111,
P_CMD_NOP = 5'b10111,
P_CMD_RACT = 5'b10011,
P_CMD_READ = 5'b10101,//A10 = 1;
P_CMD_WRITE = 5'b10100,//A10 = 1;
P_CMD_PER = 5'b10010,//A10 = 1:ALL;
P_CMD_AP = 5'b10001,
P_CMD_MR = 5'b10000,
P_CMD_B_STOP = 5'b10110;
assign o_sdram_cke = r_sdram_cmd[4] ;
assign o_sdram_cs_n = r_sdram_cmd[3] ;
assign o_sdram_ras_n = r_sdram_cmd[2] ;
assign o_sdram_cas_n = r_sdram_cmd[1] ;
assign o_sdram_we_n = r_sdram_cmd[0] ;
assign o_sdram_addr = ro_sdram_addr[12: 0] ;
assign o_sdram_ba = ro_sdram_addr[14:13] ;
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
r_sdram_cmd <= 'd0;
else if(r_st_current == P_INIT_WAIT)
r_sdram_cmd <= P_CMD_INIT;
else if(r_st_current == P_ALL_PRE && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_PER;
else if((r_st_current == P_AREFRESH || r_st_current == P_IDLE_AR) && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_AP;
else if(r_st_current == P_ROW_ACT && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_RACT;
else if(r_st_current == P_READ_CMD && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_READ;
else if(r_st_current == P_WRITE_CMD && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_WRITE;
else if(r_st_current == P_MR_SET && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_MR;
else if(r_st_current == P_BURST_STOP && r_st_cnt == 0)
r_sdram_cmd <= P_CMD_B_STOP;
else
r_sdram_cmd <= P_CMD_NOP;
end
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
ro_sdram_addr <= 'd0;
else if(r_st_current == P_ALL_PRE && r_st_cnt == 0)
ro_sdram_addr <= {4'd0,1'b1,10'd0};
else if(r_st_current == P_MR_SET && r_st_cnt == 0)
ro_sdram_addr <= {5'd0,1'b0,2'b00,3'b010,1'b0,3'b111};
else if(r_st_current == P_ROW_ACT && r_st_cnt == 0)
ro_sdram_addr <= ri_operation_addr[23:9];
else if((r_st_current == P_READ_CMD || r_st_current == P_WRITE_CMD) && r_st_cnt == 0)
ro_sdram_addr <= {ri_operation_addr[23:22],2'd0,1'b1,1'b0,ri_operation_addr[8:0]};
else
ro_sdram_addr <= 15'h7fff;
end
大概思路是因为DRAM的指令主要由CKE、CS、RAS、CAS、WE、A10这几个引脚控制,所以可以参数化设置一下,将不同状态下产生的指令存到r_sdram_cmd这个寄存器中,然后把这个寄存器的不同位接到CKE、CS、RAS、CAS、WE、A10这几个引脚上。
读写数据
读写数据线用了三态门控制。
assign io_sdram_data = r_sdata_wH_rL ? ri_write_data_3d : {16{1'bz}} ;
assign w_sdata_r = r_sdata_wH_rL ? 'd0 : io_sdram_data ;
always@(posedge i_clk,posedge i_rst)
begin
if(i_rst)
r_sdata_wH_rL <= 'd0;
else if(r_st_current == P_WRITE_DATA && r_st_cnt == ri_operation_len - 1)
r_sdata_wH_rL <= 'd0;
else if(r_st_current == P_WRITE_CMD)
r_sdata_wH_rL <= 'd1;
else
r_sdata_wH_rL <= r_sdata_wH_rL;
end
大概实现思路就是这些,剩下的就是对几个寄存器处理,按照时序调试,完整代码篇幅过长就不贴了。SDRAM驱动不难,主要是其中有很多时间要求比如TRCD,TWR等需要控制一下。
Testbench
我们用两个task按照最大边界和最小边界以及中间情况取三种情况:
- 做一个读写512Byte数据的仿真
- 做一个读写1Byte数据的仿真
- 做一个读写8Byte数据的仿真
initial begin
ri_operation_cmd = 'd0;
ri_operation_addr = 'd0;
ri_operation_len = 'd0;
ri_operation_valid = 'd0;
ri_write_data = 'd0;
ri_write_last = 'd0;
ri_write_valid = 'd0;
wait(!rst);
repeat(10)@(posedge clk);
/****最大边界****/
wirte_data(512,10);
read_data(512 ,10);
/****最小边界****/
wirte_data(1,10);
read_data(1 ,10);
/****中间情况****/
wirte_data(8,10);
read_data (8 ,10);
end
task wirte_data(input [9:0] op_len,input [26:0] op_addr);
begin:wirte_data
integer i;
ri_operation_cmd <= 'd0;
ri_operation_addr <= 'd0;
ri_operation_len <= 'd0;
ri_operation_valid <= 'd0;
@(posedge clk);
wait(wo_operation_ready);
ri_operation_cmd <= 'd2;
ri_operation_addr <= op_addr;
ri_operation_len <= op_len;
ri_operation_valid <= 'd1;
@(posedge clk);
ri_operation_cmd <= 'd0;
ri_operation_addr <= 'd0;
ri_operation_len <= 'd0;
ri_operation_valid <= 'd0;
for(i = 0;i < op_len;i = i + 1)
begin
ri_write_data <= i;
if(i == op_len - 1)ri_write_last <= 'd1; else ri_write_last <= 'd0;
ri_write_valid <= 'd1;
@(posedge clk);
end
ri_write_data <= 'd0;
ri_write_last <= 'd0;
ri_write_valid <= 'd0;
@(posedge clk);
end
endtask
task read_data(input [9:0] op_len,input [26:0] op_addr);
begin:read
ri_operation_cmd <= 'd0;
ri_operation_addr <= 'd0;
ri_operation_len <= 'd0;
ri_operation_valid <= 'd0;
@(posedge clk);
wait(wo_operation_ready);
ri_operation_cmd <= 'd1;
ri_operation_addr <= op_addr;
ri_operation_len <= op_len;
ri_operation_valid <= 'd1;
@(posedge clk);
ri_operation_cmd <= 'd0;
ri_operation_addr <= 'd0;
ri_operation_len <= 'd0;
ri_operation_valid <= 'd0;
@(posedge clk);
end
endtask