众所周知,SDRAM是FPGA平台的重要组成部分,任何大数据量的处理都需要用到SDRAM来缓存数据。SDRAM因为工作时钟通常为100MHZ以上,但数据采集端、数据输出端的工作时钟大多数情况下比SDRAM低,所以通常情况下要使用FIFO作为中间的转运媒介,把数据从采集端运输到SDRAM,再从SDRAM发送到输出端;但是为了循序渐进,故本文章只介绍SDRAM的内部时序和程序,后面再介绍SDRAM和FIFO的结合运用。
第一部分:SDRAM的介绍
1.SDRAM内部结构:
SDRAM芯片有13个A口(A0~A12)作为地址线,A0到A12为行地址,A0到A8是列地址线。
其中A10在预充电时,为1则表示对所有Bank进行预充电,为0则由BS0,BS1控制选择哪一个Bank进行预充电。
DQ(0~15)为数据的输入输出口。
本文章使用的SDRAM的型号为W9825G6KH-6:8192行 X 512列 X 16bit X 4Bank = 256Mbit
所以自刷新时需要在64ms内刷新8192行,那么则有:
64ms ➗ 8192 = 7.8125us
7.8125us X 100MHZ = 781.25 个时钟周期,向下取整数得781
T = 64ms - 8192 X = 0.0205ms
(在100M的时钟下可以打2050拍,远远大于全页(512)读写的所需要的时间,不用担心超时!)
命令由4个引脚构成:{cs,ras,cas,we} 如下:
数据门以及程序框架:
数电基础:三态门,有使能才开,默认可读
注:一定要把上面这张状态转换图烂熟于心,不然下面的代码你真的会懵!
我的代码可是出了名的节省资源,所以很多寄存器都是复用的!
其他内部结构我这里就不再赘述了,不了解的伙伴先自己研究一下SDRAM的数据手册哈!
数据手册:W9825G6KH-6数据手册
第二部分:程序部分
1.参数定义部分:
//SDRAM 16位 满速模式 DDR2 暂设100MHZ 包括:控制 命令 传输
//位宽
parameter DATA_W = 16 ;
parameter ADDR_W = 24 ;// 0~8 9~21 22~23
// 列 行 bank
//状态
parameter INIT_NOP = 4'b0001 ;//初始空状态
parameter PRECHARGE = 4'b0010 ;//预充电
parameter REFRESH = 4'b0011 ;//刷新64ms
parameter MODE = 4'b0100 ;//配置寄存器
parameter IDLE = 4'b0101 ;//空闲态
parameter ACTIVE = 4'b0110 ;//激活态
parameter WRITE = 4'b0111 ;//写
parameter READ = 4'b1000 ;//读
//命令参数
parameter NOP_CMD = 4'b0111 ;//空命令
parameter PRECHARGE_CMD = 4'b0010 ;//预充电命令
parameter REFRESH_CMD = 4'b0001 ;//刷新命令
parameter MODE_CMD = 4'b0000 ;//配置寄存器命令
parameter ACTIVE_CMD = 4'b0011 ;//激活命令
parameter WRITE_CMD = 4'b0100 ;//写命令
parameter READ_CMD = 4'b0101 ;//读命令
//物理引脚配置
always @(*)begin
{cs,ras,cas,we} = cmd;
end
ADDR_W = 24 的含义是:{9个列地址,13个行地址,2个bank选择位}
其他的命令请自行查阅数据手册:W9825G6KH-6数据手册
2.状态机定义:
//状态转换
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)
state_c <= INIT_NOP;
end
else begin
state_c <= state_n;
end
end
//
always @(*)begin
case(state_c)begin
INIT_NOP:begin
if(ini2pre_start)begin//初始空状态-预充电
state_n = PRECHARGE;
end
else begin
state_n = state_c;
end
end
PRECHARGE:begin
if(pre2ref_start)begin//预充电-刷新(64ms)
state_n = REFRESH;
end
else if(pre2idl_start)begin//预充电-空闲态
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
REFRESH :begin
if(ref2ref_start)begin//自动刷新(64ms)
state_n = REFRESH;
end
else if(ref2mod_start)begin//刷新-配置寄存器
state_n = MODE;
end
else if(ref2idl_start)begin//刷新-空闲态
state_n = IDLE;
else begin
state_n = state_c;
end
end
MODE :begin
if(mod2idl_start)begin//配置寄存器-空闲态
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
IDLE :begin
if(idl2ref_start)begin//空闲态-刷新
state_n = REFRESH;
end
else if(idl2act_start)begin//空闲态-激活态
state_n = ACTIVE;
end
else begin
state_n = state_c;
end
end
ACTIVE :begin
if(act2wrt_start)begin//激活态-写
state_n = WRITE;
end
else if(act2red_start)begin//激活态-读
state_n = READ;
end
else begin
state_n = state_c;
end
end
WRITE :begin
if(wrt2pre_start)begin//写-预充电
state_n = PRECHARGE;
end
else begin
state_n = state_c;
end
end
READ :begin
if(red2pre_start)begin//读-预充电
state_n = PRECHARGE;
else begin
state_n = state_c;
end
end
default :begin
state_c = INIT_NOP;
end
end
assign ini2pre_start = state_c == INIT_NOP && end_cnt;//初始空状态-预充电:SDRAM上电后要等待100us,计时计数器计时结束后跳转
assign pre2ref_start = state_c == PRECHARGE && end_cnt && init_flag;//第一次预充电完成后要刷新几次
//预充电-刷新(64ms):init_flag为寄存器配置完成的标志(初始为1,完成后为0)
assign pre2idl_start = state_c == PRECHARGE && end_cnt && init_flag == 0;//后面预充电完成之后就直接跳转到空闲态
assign ref2ref_start = state_c == REFRESH && end_cnt && resh_cnt == 0 && init_flag;//上电刷新两次(第一次)
assign ref2mod_start = state_c == REFRESH && end_cnt && resh_cnt == 1 && init_flag;//上电刷新两次完成后进入配置部分(第二次)
assign mod2idl_start = state_c == MODE && end_cnt;
assign idl2ref_start = state_c == IDLE && resh_flag == 0 && (rd_req || wr_req);//空闲>>自动刷新
assign ref2idl_start = state_c == REFRESH && end_cnt && init_flag == 0; //自动刷新>>空闲
assign idl2act_start = state_c == IDLE && (resh_flag || end_cnt1);//空闲跳转到激发态度
assign act2wrt_start = state_c == ACTIVE && end_cnt && rd_flag == 0;//激发跳转写
assign act2red_start = state_c == ACTIVE && end_cnt && rd_flag == 1;//激发跳转读
assign wrt2pre_start = state_c == WRITE && end_cnt;//写完后要充电
assign red2pre_start = state_c == READ && end_cnt;//读完后要充电
是不是突然就懵了!再回去看看那张状态转换图吧。
这里我就不讲解了,把状态图先熟悉先,后面有需要在再在这里补充!
2.状态机跳转定义:
//状态机跳转条件的定义
always @(posedge clk or negedge rst_n)begin//配置完成后init_flag拉低
if(rst_n == 1'b0)begin
init_flag <= 1;
end
else if(mod2idl_start)begin
init_flag <= 0;
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n == 0)begin
cnt1 <= 0;
end
else if(add_cnt1)begin
if(end_cnt1)begin
cnt1 <= 0;
end
end
else begin
cnt1 <= cnt1 + 1'b1;
end
end
assign add_cnt1 = init_flag == 1'b0;//配置寄存器完成才开始自动刷新
assign end_cnt1 = add_cnt1 && cnt1 = 1562 - 1;//全部刷新一次的时间
always @(posedge clk or negedge rst_n)begin//默认为低 刷新完成之后拉高标志信号
if(rst_n == 1'b0)begin
resh_flag <= 1'b0;
end
else if(end_cnt1)begin
resh_flag <= 1'b1;
end
else if(idl2ref_start)begin
resh_flag <= 1'b0;
end
end
always @(posedge clk or negedge rst_n)begin//上电刷新两次
if(rst_n == 1'b0)begin
resh_cnt <= 0;
end
else if(add_resh_cnt)begin
if(end_resh_cnt)begin
resh_cnt <= 0;
end
else begin
resh_cnt <= resh_cnt + 1'b1;
end
end
end
assign add_resh_cnt = init_flag && state_c == REFRESH && end_cnt;//刷新开始
assign end_resh_cnt = add_resh_cnt && resh_cnt = 2 - 1;//上电刷新两次
always @(posedge clk or negedge rst_n)begin//读写请求标志信号
if(rst_n == 1'b0)begin
rd_flag <= 1'b0;
end
else if(write_sel)begin//写过之后为0
rd_flag <= 1'b0;
else if(read_sel)begin //读过之后为1
rd_flag <= 1'b1;
end
end
end
always @(posedge clk or negedge rst_n)begin //写入和读取之前要有标志信号
if(rst_n == 1'b0)begin
rd_hty <= 1'b0;
end
else if(pre2idl_start)begin
rd_hty <= rd_flag;
end
end
assign write_sel = idl2act_start && ((rd_hty == 1'b0 && rd_req= 1'b0) || rd_hty) && wr_req;//
assign read_sel = idl2act_start && ((rd_hty && wr_req = 1'b0) || rd_hty == 1'b0) && rd_req;
这里的跳转还是需要思考一下的,比如:
当完成初始配置的时候才可以进入IDLE态;将rd_flag延一拍给rd_hty,给后面判断是否可以读写使用。
判断当前是否可以读写有一下两个原则:
1.写:上次响应了写且上次已经响应了读 ,当前有写请求且现在没有读请求
2.读:上次响应了读且现在没有写请求或者在上次已经响应了写 ,且当前有读请求
2.计数器定义:
计数器是灵魂!本人的计数定义规制:(目标数 - 1)
//时间长度定义
parameter T_100US = 10000 ;
parameter TRP = 3 ;
parameter TRC = 7 ;
parameter TMRD = 2 ;
parameter TRCD = 3 ;
parameter T_512 = 512 ;
always @(posedge clk or negedge rst_n)begin
if(rst_n == 0)begin
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)begin
cnt <= 0;
end
end
else begin
cnt <= cnt + 1'b1;
end
end
assign add_cnt = state_c != IDLE;
assign end_cnt = add_cnt && cnt = x - 1;//定时器复用
always @(*)begin//计数器的数值设定:100MHZ 10ns
if(state_c == INIT_NOP)begin
x = T_100US;//10 000 X 10ns = 100us
end
else if(state_c == PRECHARGE)begin
x = TRP;//6个周期
end
else if(state_c == REFRESH)begin
x = TRC;//2个周期
end
else if(state_c == MODE)begin
x = TMRD;//2个周期
end
else if(state_c == ACTIVE)begin
x = TRCD;//2个周期
end
else if(state_c == WRITE || state_c == READ)begin
x = T_512;//我这里自己设定写512个数据
end
else begin
x = 0;
end
end
看!这样子是不是就节省了一些资源,虽然不多。。。。。
代码中的T_512其实就算是全页读写了,当懂得SDRAM读写原理之后就可以在这里操作一下,使它变为突发长度(外界给数据的总个数和起始地址,就可以自动写入,无需写地址了)
2.数据的写入和读出:
有点长,先慢慢看!
//地址信息暂存器:
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
addr_i_ff0 <= 0;
end
else begin
addr_i_ff0 <= addr_i;
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
bank <= 2'b00;
end
else if(idl2act_start)begin
bank <= addr_i[23:22];
end
else if(act2wrt_start || act2red_start)begin
bank <= addr_i_ff0[23:22];
end
else begin
bank <= 2'b00;
end
//Bank定义
parameter MODE_VALUE = 13'b0000000110111;
parameter ALL_BANK = 13'b0001000000000;
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
addr_o <= 0;
end
else if(ref2mod_start)begin
addr_o <= MODE_VALUE;
end
else if(ini2pre_start || wrt2pre_start || red2pre_start)begin
addr_o <= ALL_BANK;
end
else if(idl2act_start)begin
addr_o <= addr_i[21:9];
end
else if(act2wrt_start || act2red_start)begin
addr_o <= addr_i_ff0[8:0];
end
else begin
addr_o <= 0;
end
always @(*)begin
rd_ack = read_sel;
end
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
read_flag_ff0 <= 1'b0;
read_flag_ff1 <= 1'b0;
read_flag_ff2 <= 1'b0;
end
else begin
read_flag_ff0 <= read_flag;
read_flag_ff1 <= read_flag_ff0;
read_flag_ff2 <= read_flag_ff1;
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
rdata <= 0;
end
else begin
rdata <= dq;
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
rdata_vld <= 0;
end
else if(read_flag_ff2)begin
rdata_vld <= 1;
end
else if(pre2idl_start && rd_flag)begin
rdata_vld <= 0;
end
end
always @(*)begin
wr_ack = write_sel;
end
//打3拍:CAS为3 ,且进入写态还要延期一个时钟,所以一共延4个时钟
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
wdata_ff0 <= 0;
wdata_ff1 <= 0;
wdata_ff2 <= 0;
wdata_ff3 <= 0;
end
else begin
wdata_ff0 <= wdata;
wdata_ff1 <= wdata_ff0;
wdata_ff2 <= wdata_ff1;
wdata_ff3 <= wdata_ff2;
end
end
assign dq_en = state_c == WRITE;
assign dq = dq_en ? wdata_ff3 : 16'hzzzz;
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
cmd = NOP_CMD;
end
else if(ini2pre_start || wrt2pre_start || red2pre_start)begin
cmd = PRECHARGE_CMD;
end
else if(pre2ref_start || ref2ref_start || idl2ref_start)begin
cmd = REFRESH_CMD;
end
else if(ref2mod_start)begin
cmd = MODE_CMD;
end
else if(act2wrt_start)begin
cmd = WRITE_CMD;
end
else if(act2red_start)begin
cmd = READ_CMD;
end
else if(idl2act_start)begin
cmd = ACTIVE_CMD;
end
else begin
cmd = NOP_CMD;
end
end
//有效使能cke 默认使能
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
cke = 1;
end
else begin
cke = 1;
end
end
//复位为高,其余为低
always @(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)begin
dqm <= 2'b00;
end
else if(dqm_en)begin
dqm <= 2'b11;
end
else begin
dqm <= 2'b00;
end
end
assign dqm_en = state_c == init_flag;
首先,既然有先后顺序,那必然少不了打拍子,这里打拍子的目的就是为了满足数据输出的时序要求(读使能后要延迟3个时钟周期);同理数据写入要延迟3个时钟周期(CAS),另外进入激发态那一个时钟周期,就一共4个时钟周期,所以wdata打四拍。
接着为命令部分,这里简述为:cmd={cs , ras , cas , we}分别对应不同的命令,给予不同电平即对应不同的命令。
最后,SDRAM 实际上是复杂的,这里只是简单展示其基本驱动原理,其他更高级的玩法需自行探索!