前言
本篇文章主要记录如何实现SDRAM的数据读模块,关于SDRAM的引脚、预充电命令和自动刷新命令介绍以及SDRAM初始化流程、自动刷新模块、SDRAM写数据模块见前面的文章。
SDRAM初始化部分
SDRAM自动刷新模块
SDRAM数据写模块
数据读模块和数据写模块类似,下面进行数据读模块的详细介绍。
一、SDRAM数据读模块用到的命令介绍
在初始化部分我们已经进行了模式寄存器配置,配置SDRAM的读写方式为突发读和突发写,突发传输方式为顺序传输,突发传输的长度为512。接下来对SDRAM在数据读模块用到的命令进行介绍。
激活(ACTIVE)命令
在初始化部分介绍预充电命令时提到, 预充电命令会使打开的行处于非激活状态。激活命令和预充电命令作用相反,下面是激活命令的介绍。
The ACTIVE command is used to open (or activate) a row in a particular bank for a subsequent access. The value on the BA0, BA1 inputs selects the bank, and the address provided on inputs A0-A12 selects the row. This row remains active (or open) for accesses until a PRECHARGE command is issued to that bank. A PRECHARGE command must be issued before opening a different row in the same bank.
ACTIVE命令用于打开(或激活)特定bank中的一行,以便进行后续访问。BA0、BA1输入上的值选择bank,而A0-A12输入上的地址选择行。该行保持活动状态(或打开)以供访问,直到向该bank发出PRECHARGE命令。在打开同一bank的另一行之前,必须先发出PRECHARGE命令。
根据这段话的介绍可知,在对SDRAM进行数据的读操作之前,我们需要先激活我们要读出的数据所在的行,当需要读取该bank的下一行存储的数据时,我们需要先发预充电命令关闭当前行,再去激活下一行进行数据的读出。
由此可知,读出SDRAM存储的数据的顺序为:激活命令(打开行)→读出数据→预充电(关闭行)
读(READ)命令
The READ command is used to initiate a burst read access to an active row. The value on the BA0, BA1 inputs selects the bank, and the address provided on inputs A0-A9, A11 (x4), A0-A9 (x8), or A0-A8 (x16) selects the starting column location. The value on input A10 determines whether or not auto precharge is used.
If auto precharge is selected, the row being accessed will be precharged at the end of the READ burst; if auto precharge is not selected, the row will remain open for subsequent accesses. Read data appears on the DQs subject to the logic level on the DQM inputs two clocks earlier. If a given DQM signal was registered HIGH, the corresponding DQs will be High-Z two clocks later; if the DQM signal was registered LOW, the DQs will provide valid data.
READ命令用于启动对一个活动行的突发读取访问。输入BA0、BA1的值选择bank,输入A0-A9的地址选择起始列位置。输入A10的值决定是否使用自动预充电。如果选择了自动预充电,所访问的行将在READ突发结束时被预充电;如果没有选择自动预充电,该行将保持打开状态,供后续访问使用。读取数据将根据DQM输入的逻辑电平显示在DQ线上,且这个逻辑电平是在两时钟周期之前注册的。如果某个DQM信号被注册为HIGH,两个时钟周期后相应的DQ将为High-Z;如果DQM信号被注册为LOW,相应的DQ将提供有效数据。
根据这段话的介绍可知,在发出SDRAM读命令的时候,我们可以通过A10的输入设置SDRAM在读突发结束后是否进行自动预充电。并且可以使用DQM进行数据掩码
在本文中,由于我们配置的突发长度为全页突发,没有办法使用自动预充电(数据手册的规定,自动预充电对全页突发不起作用),所以我们在进行完数据读后,在发送预充电命令即可。本例程用不到DQM进行掩码,从SDRAM读出的数据所有位都是有效的
读数据潜伏期(CAS)介绍
During READ bursts, the valid data-out element from the starting column address will be available following the CAS latency after the READ command. Each subsequent data-out element will be valid by the next positive clock edge. Figure 6 shows general timing for each possible CAS latency setting.
在READ突发期间,从起始列地址输出的有效数据元素将在READ命令后的CAS延迟之后可用。每个后续的数据输出元素将在下一个正时钟沿时有效。
在初始化阶段,我们配置了CAS Latency为3,其时序图如下所示。
时序图
根据上图可知,在发出读命令后,经过CAS Latency个周期后,读出的数据开始有效。
可以和写进行比较一下,在写数据的时候,写命令和第一个要写入的数据同时有效,但是在读的时候,读命令和第一个被读出的数据有效之间差了CAS Latency个时钟周期,为什么会有这样的区别?
解析:在发出读命令后,SDRAM就会对该命令进行响应,但是由于芯片体积的原因,存储单元中的电容容量很小,所以需要经过放大才可以保证其有效的识别性,并经过一定的驱动时间最终传向数据I/O总线进行输出,所以会有一个CAS Latency的延迟
在写的时候我们还记得,写完数据后需要等待tWR的时间才可以进行预充电,这是因为虽然在发出写命令是,数据被送进SDRAM,但是每笔数据的真正写入则需要一个足够的时间来保证,这个时间就是tWR
*需要注意的点:上述的时序图是针对SDRAM芯片而言的,其中CLK指的是SDRAM芯片的时钟,和用户的逻辑时钟通常会有90°的相移,那么在用户侧从发出READ命令开始到采到有效数据,需要经过CAS Latency+1个时钟周期。其关键时序如下图所示。
突发停止(BURST TERMINATE)命令
为什么会用到突发停止命令呢?是因为我们在初始化时设置的SDRAM突发长度为512,那么在实际项目中进行读的时候,可能一次不必读512个数据,比如一次只读256个数据,那么这个时候我们就需要用到突发停止命令,在读完256个数据后告诉SDRAM,本次读操作结束,可以进行后续的操作了。
Full-page READ bursts can be truncated with the BURST TERMINATE command, and fixed-length READ bursts may be truncated with a BURST TERMINATE command, provided that auto precharge was not activated.The BURST TERMINATE command should be issued x cycles before the clock edge at which the last desired data element is valid, where x equals the CAS latency minus one.This is shown in Figure 12 for each possible CAS latency; data element n + 3 is the last desired data element of a longer burst.
全页READ突发可以通过BURST TERMINATE命令来截断,固定长度READ突发也可以通过BURST TERMINATE命令来截断,前提是未启用自动预充电。BURST TERMINATE命令应在最后一个所需数据元素有效的时钟沿之前的x个周期发出,其中x等于CAS延迟减一。
由于我们的CAS latency =3 ,所以BURST TERMINATE命令应在最后一个所需数据元素有效的时钟沿之前的2个周期发出
时序图
SDRAM读数据流程总结
根据上文的介绍以及该图可知,SDRAM读数据流程如下:
激活需要读的bank和行→等待TRCD时间→发出读命令→潜伏期等待(在初始化阶段配置的潜伏期为CAS latency为3,所以当读命令与第一个数据读出之间隔了3个时钟周期)→读出rd_burst_len个数据→发出突发停止命令→发出预充电命令→等待tRP时间→一次突发读传输结束
二、代码设计
根据分析可知,在数据读阶段各个状态跳转已经非常明确了,我们可以使用状态机去设计数据读模块的流程,并设计一个计数器用于判断每个命令等待的时间是否满足,如果满足就可以跳转至下一个状态。完整的数据读模块代码以及仿真测试文件可以从github下载,SDRAM-Controller。
状态机定义如下
//读阶段的状态机
localparam RD_IDLE = 4'b0000 ;//初始状态,初始化结束并且读使能跳转至激活状态
localparam RD_ACTIVE = 4'b0001 ;//发送激活命令
localparam RD_TRCD = 4'b0010 ;//激活状态,等待tRCD时间跳转至读状态
localparam RD_READ = 4'b0011 ;//发送读命令
localparam RD_CAS = 4'b0100 ;//潜伏期等待数据有效
localparam RD_RDATA = 4'b0101 ;//读数据状态,数据读完后发送突发停止命令
localparam RD_PRE = 4'b0110 ;//预充电命令
localparam RD_TRP = 4'b0111 ;//预充电状态,等待tRP时间读结束
localparam RD_END = 4'b1000 ;//读结束
sdram_read.v模块代码
/*
* @Author: bit_stream
* @Date: 2024-12-15 18:18:35
* @Last Modified by: bit_stream
* @Last Modified time: 2024-12-15 19:37:17
*/
//读和写类似,读出数据的流程如下:激活需要读的bank和行→等待TRCD时间→发出读命令→潜伏期等待(在初始化阶段配置的潜伏期为CAS latency为3,所以当读命令与第一个数据读出之间隔了3个时钟周期)、
//→读出rd_burst_len个数据→发出突发停止命令→发出预充电命令→等待tRP时间→一次突发读传输结束
module sdram_read
(
input wire sys_clk , //系统时钟,频率100MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire init_end , //初始化结束信号
input wire rd_en , //读使能
input wire [23:0] rd_addr , //读SDRAM地址
input wire [15:0] rd_data , //自SDRAM中读出的数据
input wire [9:0] rd_burst_len , //读突发SDRAM字节数
output reg rd_fifo_wr_en , //SDRAM读侧的FIFO 写使能信号
output wire rd_end , //一次突发读结束
output reg [3:0] read_cmd , //读数据阶段写入sdram的指令,{cs_n,ras_n,cas_n,we_n}
output reg [1:0] read_ba , //读数据阶段Bank地址
output reg [12:0] read_addr , //地址数据,辅助预充电操作,行、列地址,A12-A0,13位地址
output wire [15:0] rd_fifo_wr_data //写入SDRAM读侧的FIFO数据
);
//读阶段使用到的SDRAM 指令集
localparam NOP = 4'b0111 ;//空命令
localparam ACTIVE = 4'b0011 ;//激活命令
localparam READ = 4'b0101 ;//读数据命令
localparam B_TERM = 4'b0110 ;//突发停止命令
localparam PRECHARGE = 4'b0010 ;//预充电命令
//读阶段的状态机
localparam RD_IDLE = 4'b0000 ;//初始状态,初始化结束并且读使能跳转至激活状态
localparam RD_ACTIVE = 4'b0001 ;//发送激活命令
localparam RD_TRCD = 4'b0010 ;//激活状态,等待tRCD时间跳转至读状态
localparam RD_READ = 4'b0011 ;//发送读命令
localparam RD_CAS = 4'b0100 ;//潜伏期等待数据有效
localparam RD_RDATA = 4'b0101 ;//读数据状态,数据读完后发送突发停止命令
localparam RD_PRE = 4'b0110 ;//预充电命令
localparam RD_TRP = 4'b0111 ;//预充电状态,等待tRP时间读结束
localparam RD_END = 4'b1000 ;//读结束
//各个阶段等待的周期数,一个时钟周期为10ns,0.01us
localparam RD_TRP_CLK = 2 ; //根据数据手册可知,最小时间为20ns
localparam RD_TRCD_CLK = 2 ; //根据数据手册可知,最小时间为20ns
localparam RD_CAS_CLK = 3 ; //初始化阶段配置为3
//定义状态机
reg [3:0] rd_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 (rd_state)
RD_TRCD,RD_CAS,RD_RDATA,RD_TRP:begin
cnt_clk_en <= 1'b1;
end
RD_IDLE,RD_ACTIVE,RD_READ,RD_PRE,RD_END:begin
cnt_clk_en <= 1'b0;
end
default: begin
cnt_clk_en <= cnt_clk_en;
end
endcase
end
end
//读状态机跳转
always @(posedge sys_clk) begin
if (~sys_rst_n) begin
rd_state <= RD_IDLE;
end else begin
case (rd_state)
RD_IDLE:begin
if (rd_en & init_end) begin
rd_state <= RD_ACTIVE;
end
end
RD_ACTIVE:begin
rd_state <= RD_TRCD;
end
RD_TRCD:begin
if (cnt_clk == RD_TRCD_CLK - 'd1) begin
rd_state <= RD_READ;
end
end
RD_READ:begin
rd_state <= RD_CAS;
end
RD_CAS:begin
if (cnt_clk == RD_CAS_CLK ) begin //cnt_clk从RD_CAS状态开始计数,在其跳转到RD_RDATA状态时,其计数值为3
rd_state <= RD_RDATA;
end
end
RD_RDATA:begin
if (cnt_clk == rd_burst_len + RD_CAS_CLK) begin//所以这里是rd_burst_len + 'd3,计数范围是3~rd_burst_len + 'd3
rd_state <= RD_PRE;
end
end
RD_PRE:begin
rd_state <= RD_TRP;
end
RD_TRP:begin
if (cnt_clk == RD_TRP_CLK - 'd1) begin
rd_state <= RD_END;
end
end
RD_END:begin
rd_state <= RD_IDLE;
end
default: begin
rd_state <= RD_IDLE;
end
endcase
end
end
always @(posedge sys_clk) begin
if (~sys_rst_n) begin
read_cmd <= NOP;
read_ba <= 2'b11;
read_addr <= 13'h1fff;
end else begin
case (rd_state)
RD_IDLE,RD_TRCD,RD_CAS,RD_TRP,RD_END:begin
read_cmd <= NOP;
read_ba <= 2'b11;
read_addr <= 13'h1fff;
end
RD_ACTIVE:begin
read_cmd <= ACTIVE;
read_ba <= rd_addr[23:22];//地址高2bit代表哪一个bank
read_addr <= rd_addr[21:9];
end
RD_READ:begin
read_cmd <= READ;
read_ba <= rd_addr[23:22];
read_addr <= {4'b0000,rd_addr[8:0]};//其实列地址只需9位,但是,需要补4个零,因为数据长度为13,A10为0,不进行自动预充电
end
RD_RDATA:begin
read_ba <= 2'b11;
read_addr <= 13'h1fff;
if (cnt_clk == rd_burst_len - 'd1) begin //提前两个时钟周期发送突发停止命令
read_cmd <= B_TERM;
end else begin
read_cmd <= NOP;
end
end
RD_PRE:begin
read_cmd <= PRECHARGE;
read_ba <= 2'b11;
read_addr <= 13'h1fff;
end
default: begin
read_cmd <= NOP;
read_ba <= 2'b11;
read_addr <= 13'h1fff;
end
endcase
end
end
always @(posedge sys_clk) begin
if (~sys_rst_n) begin
rd_fifo_wr_en <= 0;
end else if (rd_state == RD_CAS && cnt_clk == RD_CAS_CLK ) begin //通过计数器,控制rd_fifo_wr_en何时有效
rd_fifo_wr_en <= 1;
end else if (rd_state == RD_RDATA && cnt_clk == rd_burst_len + RD_CAS_CLK) begin
rd_fifo_wr_en <= 0;
end
end
//对rd_data寄存一下,有利于布局布线,但是rd_data_reg并没有相对于rd_data延迟一个周期,因为rd_data是同步于sys_clk_shift(sdram的时钟)时钟
reg [15:0] rd_data_reg;
always @(posedge sys_clk) begin
rd_data_reg <= rd_data;
end
assign rd_fifo_wr_data = rd_fifo_wr_en? rd_data_reg : 0;
assign rd_end = (rd_state == RD_END)? 1'b1: 1'b0;
endmodule
三、Modelsim仿真结果
总结
以上是关于SDRAM控制器数据读模块的内容,下一篇进行SDRAM仲裁模块的介绍。