浅谈Verilog HDL ------ SDRAM

众所周知,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 \frac{781}{100MHZ} = 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 实际上是复杂的,这里只是简单展示其基本驱动原理,其他更高级的玩法需自行探索!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值