| **银杏科技有限公司旗下技术文档发布平台** ||||
|技术支持电话|**0379-69926675-801**|||
|技术支持邮件|Gingko@vip.163.com|||
^ 版本 ^ 日期 ^ 作者 ^ 修改内容 ^
| V1.0 | 2020-02-15 | gingko | 初次建立 |
=====实验二十六:SDRAM读写测试实验=====
==== 一、 实验目的与意义 ====
- 了解SDRAM的工作原理。
- 了解SDRAM的驱动方法。
- 掌握QuartusII 集成开发环境使用方法。
==== 二、 实验设备及平台 ====
- iCore3 双核心板。[[https://item.taobao.com/item.htm?id=524229438677|点击购买]]
- Blaster(或相同功能)仿真器和USB线缆。[[https://item.taobao.com/item.htm?id=554869837940|点击购买]]
- Micro USB线缆。
- 装有QuarusII 13.1开发环境的计算机。
==== 三、 实验原理 ====
* 本实验基于SDRAM的工作特点进行逻辑设计,通过FPGA内部逻辑对SDRAM进行读写操作。首先,FPGA向SDRAM的4个Bank中写入数据,写入的数据和其地址相同;当4个Bank全部写满后对SDRAM进行读操作,并将读取的数据与其对应的地址进行比较,判断读取器数据与写入数据是否相同,从而验证SDRAM读写是否成功。若数据读写成功,则FPGA_LED灯的红绿蓝三种颜色依次循环闪烁;若不成功,则FPGA_LED灯全灭。
=== 1、 SDRAM介绍 ===
* SDRAM是同步动态随机存储器。同步指的是其内部指令的发送和数据传输都与时钟保持同步。动态是指其存储阵列需要不断刷新以保持数据不丢失。随机指的是其数据的读写根据地址自由指定。
* SDRAM的内部有若干个存储阵列,一个这样的存储阵列叫做一个Bank。每个Bank又由行和列组成。SDRAM的访问方式即先访问Bank,再访问行,之后访问列,即可对某个存储单元进行读写操作。
* 以本次实验所用开发板iCore3上的SDRAM存储器为例,其型号为HY57V641260。HY57V641260内部有4个Bank,其行地址12bit,列地址8bit,存储单元宽度为16bit。即每个Bank由12行8列16bit宽的存储单元组成。
=== 2、SDRAM的工作原理 ===
* HY57V641260手册的叙述较为简洁,这里可以参考其他的SDRAM芯片手册了解和学习SDRAM的工作原理,其控制方式都是相同的。首先看一下下面这张图:
{{ :icore3:icore3_fpga_26_1.png?direct |}}
* 上图是SDRAM上电后的状态跳转图,图中标注了跳转方向和触发条件。其中粗黑线表示从当前状态自动跳转到另一个状态,细黑线表示需要命令才会跳转。说明SDRAM的操作主要包括上电初始化、空闲、读、写、刷新等状态跳转控制。
* SDRAM的状态跳转图中标示了一些状态的跳转是需要指令触发的,而SDRAM的指令是通过信号线的组合实现的。看下面表格,红框中的是信号控制引脚,绿框中是控制指令,通过控制信号线的电平组合实现不同的控制指令。
{{ :icore3:icore3_fpga_26_2.png?direct |}}
* 至于红框中各信号和指令所代表的含义,下面表格中给出了对各信号引脚的描述。
{{ :icore3:icore3_fpga_26_3.png?direct |}}
== (1) SDRAM的初始化 ==
* 因为SRAM的内部有一个逻辑控制单元和一个提供控制参数的模式寄存器,SDRAM芯片上电之后首先要进行初始化,然后才能进行其他操作。初始化操作的顺序是一定的,其步骤如下:
* 1)SDRAM上电并提供时钟后至等待100-200us,等待时间结束后还至少要执行一条空操作命令。
* 2) SDRAM执行一条“预充电”命令,控制所有Bank进行预充电,并执行一条空操作命令。
* 3) SDRAM执行两条“自刷新”命令,每一条刷新命令之后,都需要执行一条空操作命令。使SDRAM内部的刷新及计数器进入正常运行状态,准备配置模式寄存器。
* 4) 执行“加载模式寄存器”命令。配置SDRAM的工作参数。
* 上述4步即是SDRAM的初始化流程,SDRAM就进入正常工作状态,可进行读写操作。这里介绍一下SDRAM的模式寄存器,参考下图:
{{ :icore3:icore3_fpga_26_4.png?direct |}}
* 这里的BA1~A0为地址总线,即通过地址总线对模式寄存器进行相关参数配置。
* A9用来指定操作模式:1表示突发读/突发写,0表示突发读/单写;
* A6、A5、A4则是对CAS潜伏期进行配置。即进行SDRAM读操作时,读出来的数据相对于“read”指令延后的周期数。
* A3是对突发传输方式的配置,连续型和非连续型。
* A2、A1、A0配置突发传输长度。
* 整个初始化个过程比较好理解,系统上电后先等待100-200us,具体根据芯片手册要求设置,本实验中设置的等待时间为200us,使SDRAM处于稳定期。接着给SDRAM发送一个NOP指令。然后发送一个PRECHARGE命令,对SDRAM内部所有Bank进行预充电,之后再发送一个NOP指令,使内部的存储结构处于稳定状态。接着执行两次自刷新操作,每次自刷新之后都要跟一个NOP指令。最后是配置模式寄存器,控制SDRAM的操作模式以及传输方式等。
* 在SDRAM的初始化过程中,需要用到一些命令和地址总线,要保证command保持在时钟的上升沿有效,以及保证信号的建立时间和保持时间,这些参数参考芯片手册查阅。
{{ :icore3:icore3_fpga_26_5.png?direct |}}
== (2) SDRAM的读写操作 ==
* 先讲一下SDRAM的写操作。前面的状态跳转图中,可以发现,初始化完成后读写操作之前,还需要执行一个“ACT”命令,即“行激活”命令,其作用是激活SDRAM中的某一行,方便进行读写操作。首先看一下带自动刷新写操作的时序图:
{{ :icore3:icore3_fpga_26_6.png?direct |}}
* 结合这张时序图讲一下SDRAM的写操作。
* 在SDRAM完成初始化之后,向SDRAM发送一个“ACT”指令,同时指定是哪一个bank以及这个bank的哪一行,然后再经过 “tRCD”的时间给“WRITE”指令,同时指定bank地址和列地址并把A10拉高使能自动刷新,然后再根据模式寄存器设定好的突发长度(上面的时序图中突发长度为4)将对应数量的数据写进去,这样就完成了对SDRAM的一次写操作。
* 读操作与写操作是类似的,可以结合对写操作的分析进行读操作的理解,读操作的时序图参考如下:
{{ :icore3:icore3_fpga_26_7.png?direct |}}
== (3) SDRAM的刷新 ==
* SDRAM的存储结构物理特性要求不停对存储单元进行刷新,以保证存储的数据不会丢失。SDRAM内部电容保存数据的最长时间是64ms,也就是最多每隔64ms就要对所有存储单元进行一次刷新,每一次刷新操作可以刷新所有BANK的同一行,而HY57V641260一个BANK有4096行,64ms/4096~=15us,也就是说为了保证SDRAM内部的数据不被丢失,两次刷新之间的最大时间间隔为15us,所以为了能让SDRAM有更多的时间进行读或者写,我们就设定SDRAM刷新的周期为15us。每隔15us就要向SDRAM发送一个AUTO REFRESH的命令,刷新一行。刷新过程中,不能对SDRAM进行读写操作。
==== 四、 代码讲解 ====
* 本次实验设计的思路是通过FPGA控制,将和地址一致的数据存储到SDRAM中,然后读取出来,并验证数据的正确性的一个方案。通过前面的讲解,现在对SDRAM的控制原理有了一个清晰的认识,那么如何通过代码实现FPGA对SDRAM的控制呢?
* 设计时可以将整体划分为若干个子功能模块。这里将整体划分为SDRAM控制模块(SDRAM_Ctrl_Top)、数据处理模块(Data_Generator)和时钟分频模块(MY_PLL)。其中SDRAM控制模块又可以具体划分为状态跳转控制模块(State_Ctrl)、命令控制模块(CMD_Ctrl)和数据输入输出控制模块(Data_Ctrl),将复杂的逻辑控制划分成较为简单的逻辑功能块。这里主要介绍一下State_Ctrl模块、CMD_Ctrl模块和Data_Generator模块
=== 1、State_Ctrl模块 ===
* 此模块主要时控制SDRAM的状态跳转。要实现对SDRAM的驱动,首先要实现对SDRAM的初始化操作,具体过程前面有讲过。初始化完成之后,要实现数据的存取,还要对SDRAM进行读写操作,操作流程前面也有介绍。初始化过程以及读写状态的跳转,SDRAM的刷新操作,都是通过对状态跳转的控制实现的。
* 此模块代码包含了两部分的操作,一个是初始化操作,一个是读写状态跳转控制。
* SDRAM上电之后首先是200us的稳定期,通过对100MHz的SDRAM_CLK时钟计数,满足200us的稳定期之后拉高一个信号。之后就是按照初始化顺序流程进行bank预充电,预充电占用10个周期。接下来是8次自动刷新,每次也是占用10个周期。8次自动刷新执行完毕则是配置模式寄存器,完成SDRAM的初始化操作。代码如下:
//--------------------Init_FSM-------------------------------//
reg [4:0] cnt_clk_i = 5'd0;//初始化时钟计数器
reg [4:0] Init_State_r = 5'd0;//初始化状态寄存器
always @(posedge SDRAM_CLK or negedge RST_n)
if (!RST_n)
begin
cnt_clk_i <= 5'd0;
Init_State_r <= `Init_NOP;//上电复位后处于NOP状态
end
else
case (Init_State)
`Init_NOP: begin
cnt_clk_i <= 5'd0;
if (done_200us)//200us稳定期完成
Init_State_r <= `Init_PCH;//进入所有Bank预充电状态
else
Init_State_r <= `Init_NOP;
end
`Init_PCH: begin
if (cnt_clk_i == 5'd9)//预充电占用10个周期
begin
Init_State_r <= `Init_AR1;//进入8次自动刷新状态
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_PCH;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR1: begin //第一次自动刷新
if (cnt_clk_i == 5'd9)//自动刷新占用10个周期
begin
Init_State_r <= `Init_AR2;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR1;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR2: begin //第二次自动刷新
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR3;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR2;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR3: begin //第三次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR4;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR3;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR4: begin //第四次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR5;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR4;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR5: begin //第五次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR6;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR5;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR6: begin //第六次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR7;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR6;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR7: begin //第七次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_AR8;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR7;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_AR8: begin //第八次
if (cnt_clk_i == 5'd9)
begin
Init_State_r <= `Init_MRS;
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_AR8;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_MRS: begin //模式寄存器设置状态
if (cnt_clk_i == 5'd4) //占用5个周期
begin
Init_State_r <= `Init_DONE; //初始化完成
cnt_clk_i <= 5'd0;
end
else
begin
Init_State_r <= `Init_MRS;
cnt_clk_i <= cnt_clk_i + 1'd1;
end
end
`Init_DONE: begin //初始化完成
Init_State_r <= `Init_DONE;
end
default: Init_State_r <= `Init_NOP;
endcase
* 初始化之后就可以进行读写操作了。也是采用状态机控制,上电之后处于IDEL状态,等待初始化完成后,判断是否有读写请求和刷新请求。有读写请求,则进入ACT状态。在持续3个周期的ACT状态之后,根据指令,进入读状态或者写状态。有刷新请求,则进入自动刷新状态。当执行完指令后,再次回到IDEL状态。代码如下:
//--------------------Work_FSM-------------------------------//
reg [2:0] Work_State_r = 3'd0;//工作状态寄存器
reg [8:0] cnt_clk_w = 9'd0;//工作状态时钟计数器
reg wr_sig;
always @(posedge SDRAM_CLK or negedge RST_n)
if (!RST_n)
Work_State_r <= `Work_IDLE;//上电复位处于IDLE状态
else
case (Work_State_r)
`Work_IDLE: begin
cnt_clk_w <= 9'd0;
if (SD_Init_Done && SD_RD_Req) //初始化完成且
Work_State_r <= `Work_ACT_RD; //有读请求,进入读ACT状态
else if (SD_Init_Done && SD_WR_Req) //有写请求,进入写ACT状态
Work_State_r <= `Work_ACT_WR;
else if (SD_Init_Done && REF_Req) //初始化完成且有刷新请求
Work_State_r <= `Work_AR; //进入刷新状态
else Work_State_r <= `Work_IDLE;
end
`Work_ACT_RD: begin
if (cnt_clk_w == 9'd3)//ACT占用3个周期
begin
cnt_clk_w <= 9'd0; //时钟计数器清零
Work_State_r <= `Work_RD; //根据读写信号
end //决定是进入WR状态还是RD状态
else
begin
cnt_clk_w <= cnt_clk_w + 1'd1; //自加1
Work_State_r <= `Work_ACT_RD;
end
end
`Work_ACT_WR: begin
if (cnt_clk_w == 9'd2) //ACT占用3个周期
begin
cnt_clk_w <= 9'd0; //时钟计数器清零
Work_State_r <= `Work_WR; //根据读写信号
end //决定是进入WR状态还是RD状态
else
begin
cnt_clk_w <= cnt_clk_w + 1'd1;//自加1
Work_State_r <= `Work_ACT_WR;
end
end
`Work_WR: begin//写状态
if (cnt_clk_w == 9'd258) //写状态占用258个周期,发送写命令滞后占用1个周期
begin //写操作占用256个周期,写入最后一个数据的同时
cnt_clk_w <= 9'd0; //发生BST(突发停止)命令,然后预充电占用1个周期
Work_State_r <= `Work_IDLE;
end
else
begin
cnt_clk_w <= cnt_clk_w + 1'd1;
Work_State_r <= `Work_WR;
end
end
`Work_RD: begin //读状态
if (cnt_clk_w == 9'd5) //读状态占用260个周期,发送读命令滞后占用1个周期
begin //CL延时占用3个周期,读操作占用256个周期
cnt_clk_w <= 9'd0; //BST和预充电命令在CL延时的3个周期内发送,不额外占用时间
Work_State_r <= `Work_IDLE;//读完成后进入空闲状态
end
else
begin
cnt_clk_w <= cnt_clk_w + 1'd1;
Work_State_r <= `Work_RD;
end
end
`Work_AR: begin //自动刷新状态
if (cnt_clk_w == 9'd9) //占用10个周期
begin
cnt_clk_w <= 9'd0;
Work_State_r <= `Work_IDLE;//刷新完成后进入空闲状态
end
else
begin
cnt_clk_w <= cnt_clk_w + 1'd1;
Work_State_r <= `Work_AR;
end
end
default: begin
cnt_clk_w <= 9'd0;
Work_State_r <= `Work_IDLE;
end
endcase
=== 2、CMD_Ctrl模块 ===
* 再来看一下CMD_Ctrl模块。这个模块的主要功能是根据State_Ctrl模块的指令和状态,通过信号控制线和地址总线向SDRAM发送对应电平信号。此模块发出的控制信号包括了初始化过程中和读写过程中操作所用的信号,具体电平控制可以参考COMMAND TRUTH TABLE和MODE REGISTER两图介绍。此模块采用CASE语句,根据状态寄存器的值,跳转到对应语句执行相关指令控制操作。下面这段代码是SDRAM初始化过程中,根据初始化不同阶段发送不同指令的控制代码:
always @(posedge SDRAM_CLK or negedge RST_n)
if (!RST_n)
begin
SD_BA_r <= 2'b11;
SD_AB_r <= 12'hFFF;
SD_CMD <= `CMD_Init;//上电复位后,发送命令Init
end
else
case (Init_Stste)
`Init_NOP: begin
SD_BA_r <= 2'b11;
SD_AB_r <= 12'hFFF;
SD_CMD <= `CMD_NOP; //空闲状态发送NOP命令
end
`Init_PCH: begin
if (Init_Cnt_CLK == 5'd0) //进入预充电状态后第一个上升沿发送命令
begin //即为之前注释的发送命令滞后1个周期
SD_BA_r <= 2'b11;
SD_AB_r <= 12'hFFF;
SD_CMD <= `CMD_PCH; //发送PCH命令
end
else SD_CMD <= `CMD_NOP;
end
`Init_AR1,`Init_AR2,`Init_AR3,`Init_AR4,
`Init_AR5,`Init_AR6,`Init_AR7,`Init_AR8: begin
if (Init_Cnt_CLK == 5'd0)
begin
SD_BA_r <= 2'b11;
SD_AB_r <= 12'hFFF;
SD_CMD <= `CMD_REF; //刷新状态发送刷新命令
end
else
SD_CMD <= `CMD_NOP;
end
`Init_MRS: begin//模式寄存器设置状态
if (Init_Cnt_CLK == 5'd0)//同PCH状态
begin
SD_BA_r <= 2'b00; //必须为00
SD_AB_r <= {2'b00, //必须为00
1'b1, //写模式,0为突发读突发写,1为突发读单次写
2'b00, //必须为0
3'b011, //CL值,010为2,011为3
1'b0, //访问模式,0为顺序模式,1为交错模式
3'b000};//BL值,000为1,001为2,010为4,011为8,111为全页
SD_CMD <= `CMD_MRS; //进入MRS状态后,滞后1个周期发送MRS命令
end
else SD_CMD <= `CMD_NOP;
end
* 上面代码可以看到对模式寄存器配置的相关参数。当SDRAM初始化完成后,对应的读写操作命令的控制,于此类似,这里不再详述。
=== 3、Data_Generator模块。 ===
* 此模块功能较为简单,包括自动生成连续数据,将数据存入SDRAM和从SDRAM中读取出来并进行对比,以及控制LED灯的闪烁。首先是对刷新请求的应答处理。然后是根据SDRAM_CLK生成连续的数据。然后向SDRAM发送写请求指令并等待应答,再发送读指令并等待应答。读取数据后进行数据的判断并控制LED灯的亮灭。最后是对读写数据不一致情况的处理,通过LED直观显示,方便验证。部分代码如下:
always @(posedge SDRAM_CLK or negedge RST_n)
if (!RST_n)
begin
i <= 4'd0;
SD_WR_Req_r <= 1'd0;
WR_AB <= 20'd0;
RD_AB <= 20'd0;
Sys_Data_out_r <= 16'd0;
BANK <= 2'd0;
bank <= 2'd0;
data_in <= 20'd0;
data_out <= 20'd0;
led1 <= 1'd1;
led2 <= 1'd1;
led3 <= 1'd1;
p <= 20'd0;
q <= 4'd0;
counter <= 28'd0;
end
else case(i)
4'd0:
begin
if ((!SD_Busy) && (cnt >= 24'hfffff))
begin
BANK <= bank;
WR_AB <= data_in;
Sys_Data_out_r <= data_in[15:0];
SD_WR_Req_r <= 1'd1; //发出写请求,同时发出写地址
led1 <= 1'd0;
i <= 4'd1;
end
else i <= 4'd0;
end
4'd1:
begin
if (SD_WR_Ack) //等待SDRAM控制模块发出写应答信号
begin
if (data_in == 20'hfffff)
begin
if(bank == 2'd3)
begin
data_in <= 20'd0;
SD_WR_Req_r <= 1'd0;//写请求信号撤销
bank <= 2'd0;
i <= 4'd2;
end
else
begin
data_in <= 20'd0;
SD_WR_Req_r <= 1'd0;//写请求信号撤销
bank <= bank + 1'd1;
i <= 4'd0;
end
end
else
begin
SD_WR_Req_r <= 1'd0;//写请求信号撤销
i <= 4'd0;
data_in <= data_in + 1'd1;
end
end
else i <= 4'd1;
end
4'd2:
begin
if((!SD_Busy) && (cnt >= 24'hfffff))//等待SDRAM控制模块不忙
begin
SD_RD_Req_r <= 1'd1;//发出读请求信号,同时发出读地址
RD_AB <= data_out;
BANK <= bank;
i <= 4'd3;
end
else
i <= 4'd2;
end
4'd3:
begin
if(SD_RD_Ack) //等待SDRAM控制模块发出读应答信号
begin
SD_RD_Req_r <= 1'd0; //读请求信号撤销
i <= 4'd4;
end
else
i <= 4'd3;
end
4'd4:
begin
if (q == 4'd7)
begin
q <= 4'd0;
i <= 4'd5;
end
else
begin
i <= 4'd4;
q <= q + 1'd1;
end
end
4'd5:
begin
if (SDRAM_Data_out == data_out[15:0])//判断数据是否正确
begin
if (data_out == 20'hfffff)
begin
if (bank == 2'd3)
begin
data_out <= 20'd0;
i <= 4'd7;
end
else
begin
data_out <= 20'd0;
bank <= bank + 1'd1;
i <= 4'd2;
end
end
else
begin
data_out <= data_out + 1'd1;
i <= 4'd2;
end
end
else i <= 4'd8;
end
4'd7: //结束
begin
if(counter == 28'h4000000)
begin
led1 <= 1'd0;
led2 <= 1'd1;
led3 <= 1'd1;
end
else if(counter == 28'h8000000)
begin
led1 <= 1'd1;
led2 <= 1'd0;
led3 <= 1'd1;
end
else if(counter == 28'hC000000)
begin
led1 <= 1'd1;
led2 <= 1'd1;
led3 <= 1'd0;
end
if(counter == 28'hC000000)
begin
counter <= 28'd0;
end
else
begin
counter <= counter + 1'd1;
end
end
4'd8: //报错
begin
if (p == 20'd1000000)
begin
p <= 20'd0;
i <= 4'd8;
end
else
begin
p <= p + 1'd1;
led1 <= 1'd0;
led3 <= 1'd0;
led2 <= 1'd0;
end
end
default: i <= 4'd0;
endcase
* 最后综合RTL视图如下,可以清楚看到各模块之间的联系和信号传递。
{{ :icore3:icore3_fpga_26_8.png?direct |}}
==== 五、 实验步骤 ====
- 将USB-Blaster与iCore3的JTGA调试口相连;
- 把iCore3通过Micro USB线与计算机连接,为iCore3供电;
- 按照实验原理编写程序及调试代码;
- 烧写FPGA程序到iCore3上;
- 观察实验现象;
==== 六、 实验现象 ====
观察iCore3开发板,可以看到FPGA_LED灯红绿蓝三种颜色依次循环闪烁,表示实验验证成功。