实现FPGA对SDRAM的控制
目录
1.首先为什么深入学习SDRAM?
因为SDRAM存储量大,存取速度快,价格相对便宜,很多应用都需要用到SDRAM作为数据缓存器,因此 学习FPGA的道路的上学会SDRAM操作时必要的一步。
2. SDRAM存取的基本原理?
这部分参考网站资料或者正点原子开发指南等各个平台的开发资料中都有很全面的介绍。开发平台的资料,对于这部分的描述,我觉得很详细很好理解,之前走了不少弯路呀。
这里,我简单描述下。此部分更偏重我对不熟悉知识点的整理,想要了解完全的SDRAM操作原理的可以绕过。
SDRAM的结构如下图所示。首先,对SDRAM操作前需要上电初始化,初始化完成后可以对SDRAM进行读写以及刷新操作;其次,对SDRAM的读写是以突发的方式进行的(初始化过程中会对模式寄存器进行配置,配置内容包括了突发的长度以及突发类型,还有CL列选通潜伏期设置),从一个指定的地址开始,按照配置的突发长度的地址,顺序读写数据。对于固定地址(寻址:指定存储单元地址)的读写,即对这个地址对应的存储单元的数据读写,对于SDRAM来说,一个存储单元的容量等于数据总线的位宽;最后,在对SDRAM的操作时,行地址和列地址是共用地址线的(采取分时复用)。对SDRAM的读写,是以一个激活命令开始,然后跟随一个读写命令。在发送激活命令时,地址线上期望获取位置的行地址和bank地址,而在发送读写命令时,地址线上是期望获取位置的列地址的首地址。还需注意:SDRAM与其余存储器的最大不同处在于,完成读写操作,关闭行时需要采取预充电的形式。在操作过程中需要定时去刷新。
3. SDRAM的代码实现
由于quartus中没有现成的ADRAM IP核可用,所以整个SDRAM的控制需要自己实现。
实现前梳理模块梳理:
1)所有延时时间参数:
: 初始化过程中,等待时钟稳定的时间;
:初始化过程中,发送完预充电命令到发送自动刷新命令所需等待时间;
:初始化过程中,发送完自动刷新命令到下一个命令的等待时间,下一个命令可能为自动刷新或者设置模式寄存器命令;
:初始化过程中,等到模式寄存器完成配置所需等待时间;
:读写过程中,发送完行激活命令到发送列读写命令时所需等待的时间(因为行激活命令发出后,芯片存储阵列电子原件响应需要一定时间),为时钟周期数,需根据SDRAM的同步时钟频率确定。
CL :CAS潜伏期,读命令发出后到实际数据输出所需的时间,在初始化过程中在模式寄存器中设置;
:写回时间,写命令和数据同时发送,但数据并不是立即写入存储单元,数据真正的写入需要一定的写回时间,这个时间至少为一个周期,频率越高,需要的周期数越多。
时序图如下:
![](https://img-blog.csdnimg.cn/20210514130834144.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTE1NTQ2Mg==,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20210514130951859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTE1NTQ2Mg==,size_16,color_FFFFFF,t_70)
2)重点概念理解
突发:指在同一行相邻的储存单元连续进行数据传输的方式,连续传输所涉及到存储单元(列)的数量就是突发长度(BL)。意思是我们在设置突发长度后,只需要发送一个读写命令和地址即可连续读写突发长度个数据。BL可以是1,2,4,8和full page(一整行的数据,即所有列)。
时序如下:
![](https://img-blog.csdnimg.cn/20210514132042615.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTE1NTQ2Mg==,size_16,color_FFFFFF,t_70)
如上图,虽然数据输出在I/O端是连续的,但它占用了大量的内存控制资源,在数据连续传输时无法输入新的命令,效率很低。
![](https://img-blog.csdnimg.cn/20210514132217127.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTE1NTQ2Mg==,size_16,color_FFFFFF,t_70)
预充电操作:
自动预充电只能和读命令放在一起,在对SDRAM发出读命令时,使用A10指明是否在突发读写完成后立即执行预充电操作。但在写操作时,由于每组数据真正写入需要一个足够的周期来保证(),所以必须等在这段时间结束后才能发出预充电命令,以确保数据可靠写入。
预充电和刷新操作区别:
相似处是两者均是重写存储体重的数据。但预充电是对一个或所有bank中的工作行(处于激活状态的行,可以所有bank中不同的行)操作,且是不定期的。而刷新有固定的周期,并依次对所有行操作(与预充电不同的是,这里的行是所有bank中同一地址的行),以保留那些长时间没重写的存储体中的数据。
刷新命令,每次仅对一行有效,也就是说在公认的标准时间64ms内,需要对所有行进行刷新。刷新时不需要并不需要外部提供行地址信息,因此是内部的一个自动操作。
3)代码设计框架
将SDRAM所有模块需要用到的参数专门建一个sdram_params.h文件进行命名和定义;
对于SDRAM初始化及读写过程中各个状态切换的等待时间,可以通过两种方式实现判断及计数。一是通过直接定义各个判断时刻的时间,例如发送预充电命令的时刻、发送刷新命令的时刻;另一个是通过对各个判断过程的计数,计数满后转换,未满则继续计数。
一共分为4个状态,一上电位IDLE,上电初始化后进入IDLE完成后进去刷新状态,之后便会根据刷新请求、读请求、写请求在三个状态之间相互转换,如果没有请求则停留在刷新状态。而刷新,读和写操作通过task来完成,具体操作过程中不同时刻的命令及等待通过关键时刻点计数来完成,参考代码如下。同时对每个操作过程生成标志性判断信号,例如在操作过程中,操作介绍,数据有效位等等,方便主状态之间转换时进行判断。
parameter rd_ACT_TIME =1'b1,
rd_READ_TIME =T_RCD+1'b1,
rd_PRE_TIME =T_RCD+SC_BL+SC_CL+1'b1,
rd_END_TIME =T_RCD+SC_BL+SC_CL+T_RP;
/*读任务*/
task read_data;
begin
case(rd_cnt)
rd_ACT_TIME:begin
command<=C_ACT;
sa<=raddr_r; //在激活命令时,将行的地址发送
ba<=baddr_r;
end
rd_READ_TIME:begin
command<=C_RD;
sa<={1'b0,caddr_r[8:0]}; //在读的命令时,将列地址发送
ba<=baddr_r;
end
rd_PRE_TIME:begin
command<=C_PR;
sa[10]<=1'b1;
end
rd_END_TIME:begin
command <= C_NOP;
FF=1'b1;
end
default:command <= C_NOP;
endcase
end
endtask
重点是在对SDRAM操作过程中,定时刷新请求、读写请求的冲突问题,正常需要建立优先级,无论在那个主状态下,刷新>写>读。同时若在任务中收到请求信号,需要先对请求信号进行寄存,等任务结束后再执行。例如读过程的请求处理:
//主状态读时收到请求后状态的转换
READ:begin
if(FF==1'b0) read_data;
else begin
if(ref_req)begin
main_state<=AREF;
FF=1'b0;
end
else if(rd_opt_done & wr_req)begin
main_state<=WRITE;
FF=1'b0;
end
else if(rd_opt_done & rd_req)begin
main_state<=READ;
FF=1'b0;
end
else if(rd_opt_done & !wr_req & !rd_req)begin
main_state<=AREF;
end
else main_state<=WRITE;
end
end
//不同任务中收到读请求后的处理
always @(*) begin
case(main_state)
AREF:begin
if((!rd_break_ref) && (!wr_break_ref) && !ref_time_flag && !wr && rd )
rd_req<=1'b1; //无刷新操作时遇到du请求信号
else if(rd_break_ref && ref_opt_done && (!wr_break_ref)) rd_req<=1'b1; //在刷新过程中遇到读写请求,记住读写请求
else rd_req=1'b0;
end
WRITE:begin
if(rd && wr_opt_done && !ref_break_wr && !wr) rd_req<=1'b1; //在写操作完成后有写请求信号,并且无刷新信号时
else rd_req=1'b0;
end
READ:begin
if(rd && rd_opt_done && !ref_break_rd && !wr) rd_req<=1'b1;
else rd_req=1'b0;
end
default:rd_req=1'b0;
endcase
end
//在刷新过程中遇到读写请求,记住读写请求,并在刷新结束后进入读写
assign wr_break_ref=(wr && ref_opt)?1'b1:((!ref_opt)?1'b0:wr_break_ref);
assign rd_break_ref=(rd && ref_opt)?1'b1:((!ref_opt)?1'b0:rd_break_ref);