SDRAM驱动篇之简易SDRAM控制器的verilog代码实现

作者: Kevin 分类: FPGA学习笔记 发布时间: 2016-01-17 17:48

在Kevin写的上一篇博文《SDRAM理论篇之基础知识及操作时序》中,已经把SDRAM工作的基本原理和SDRAM初始化、读、写及自动刷新操作的时序讲清楚了,在这一片博文中,Kevin来根据在上一篇博文中分析的思路来把写一个简单的SDRAM控制器。

我们在上一篇博文中提到了这样一个问题,SDRAM是每隔15us进行刷新一次,但是如果当SDRAM需要进行刷新时,而SDRAM正在写数据,这两个操作之间怎么进行协调呢?因为我们是肯定需要保证写的数据不能丢失,所以,我们可以考虑这样来做:如果刷新的时间到了,先让写操作把正在写的4个数据(突发长度为4)写完,然后再去进行刷新操作。而如果在执行读操作也遇到需要刷新的情况,我们也可以这样来做,先让数据读完,再去执行刷新操作。

大家看完可能会想,说是这么说,那代码怎么来写呢?似乎还是没什么思路。大家可以想象一下,我们写的SDRAM控制器是肯定包括初始化、读操作、写操作及自动刷新这些操作的,既然这样,我们就可以给每一个操作写上一个模块独立开来,这样也便于我们每个模块的调试,显然这种思路是正确的。那怎么让我们的各个模块工作起来呢,虽然都是独立的模块,但很显然这几个模块之间又是相互关联的。就拿上面刚才说的那个情况来讲,如果SDRAM需要刷新了,而SDRAM却正在执行写操作,那我们刷新模块与写模块之间怎么进行控制呢?这个问题解决了,读模块与刷新模块之间的这个问题也可以很轻松的解决。大家不妨可以自己先想一下。

主状态机与各模块间的连线

为了解决各个模块之间不方便控制的情况,我们引入一个新的机制 ——“仲裁”机制。“仲裁”用来干什么呢?在这里边,“仲裁”相当于我们这个SDRAM控制器的老大,对SDRAM的各个操作统一协调:读、写及自动刷新都由“仲裁”来控制。说到这里,显然我们可以再写一个“仲裁”模块,既然在仲裁模块中要控制这么多操作,那自然而然的肯定想到了利用状态机。那我们的状态机怎么来设计呢?请看下图:

sdram_state

只给一个状态机的图,Kevin还是觉得不够说明问题,再上一个模块之间的示意图:

state_module

在讲之前,Kevin 要给大家打一下预防针:在接下来讲的过程中,大家一定要搞清楚Kevin说的是模块之间连线的关系还是状态机之间跳转的关系哦。

在仲裁模块中,初始化操作完成之后便进入到了“ARBIT”仲裁状态,只有处于仲裁状态的时候,“仲裁老大”才能进行下命令。我们先来模拟一下,当状态机处于“WRITE”写状态时,如果SDRAM刷新的时间到了,刷新模块同时向写模块和仲裁模块发送刷新请求ref_req信号,当写模块接受到ref_req之后,写模块在写完当前4个数据(突发长度为4)之后,写模块的写结束标志flag_wr_end拉高,然后状态机进入“ARBIT”仲裁状态,处于仲裁状态之后,此时有刷新请求ref_req,然后状态机跳转到“AREF”状态并且仲裁模块发送ref_en刷新使能,然后呢,刷新模块将刷新请求信号ref_req拉低并给sdram发送刷新的命令。等刷新完毕之后,刷新模块给仲裁模块发送flag_ref_end刷新结束标志,状态机跳转到“ARBIT”仲裁状态。

注意了,当刷新完跳转到“ARBIT”仲裁状态之后,如果之前我们的全部数据仍然没有写完(Kevin指的是全部数据,并不是一个突发长度的4个数据哦),那么此时我们仍然要给仲裁模块写请求“wr_req”,然后仲裁模块经过一系列判断之后,如果符合写操作的时机,那就给写模块一个写使能信号“wr_en”,然后跳转到“WRITE”写状态并且写模块开始工作。

对于读模块与刷新操作之间的协调,相信大家应该也能想象得到了,Kevin在这里就不再啰嗦了。

仲裁模块(顶层模块)代码介绍

下面先看一下在我们的仲裁模块(顶层模块)中状态机的定义:

//state
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			state	<=	IDLE;
		else case(state)
			IDLE:
				if(key[0] == 1'b1)
					state	<=	INIT;
				else
					state	<=	IDLE;
			INIT:
				if(flag_init_end == 1'b1)	//初始化结束标志
					state	<=	ARBIT;
				else
					state	<=	INIT;
			ARBIT:
				if(ref_req == 1'b1)	//刷新请求到来且已经写完
					state	<=	AREF;
				else if(ref_req == 1'b0 && rd_en == 1'b1)	//默认读操作优先于写操作
					state	<=	READ;			
				else if(ref_req == 1'b0 && wr_en == 1'b1)	//无刷新请求且写请求到来
					state	<=	WRITE;
				else
					state	<=	ARBIT;
			AREF:
				if(flag_ref_end == 1'b1)
					state	<=	ARBIT;
				else
					state	<=	AREF;
			WRITE:
				if(flag_wr_end == 1'b1)
					state	<=	ARBIT;
				else
					state	<=	WRITE;
			READ:
				if(flag_rd_end == 1'b1)
					state	<=	ARBIT;
				else
					state	<=	READ;
			default:
				state	<=	IDLE;			
		endcase	

下面简单的介绍一下状态机代码:

key[0]作为我们初始化的一个使能信号,如果是实际下板子的时候,我们还需要给按键加一个按键消抖模块。当按键0按下之后,代表我们的SDRAM的初始化使能信号来了,所以状态机从“IDLE”跳转到了“INIT”状态。在初始化状态,如果我们的初始化模块传来了初始化结束标志“flag_init_end”,那状态机跳转到“ARBIT”仲裁状态,在仲裁状态中,第一个“if”是判断刷新请求的,这也就说明了我们刷新的优先级最高。之后,如果处于仲裁状态,来了读使能信号或者写使能信号并且没有刷新请求,那状态机就跳转到对应的状态。如果处于读或写的状态,当读结束标志或者写结束标志来临的时候(这里的写结束标志和读结束标志都是指突发读或突发写的结束标志),那么就会跳转到仲裁状态。

初始化模块代码简单介绍

下面再简单的看下初始化模块中的代码:

/***********************************************
*	Module	Name	:	sdram_init
*	Engineer	:	Kevin
*	Function	:	sdram初始化模块
*	Date		:	2016.01.10
*	Blog	Website	:	dengkanwen.com
*	Version		:	v1.0
***********************************************/
module	sdram_init(
		input	wire		sclk,		//系统时钟为50M,即T=20ns
		input	wire		s_rst_n,
		
		output	reg	[3:0]	cmd_reg,	//sdram命令寄存器
		output	reg	[11:0]	sdram_addr,	//地址线
		output	reg	[1:0]	sdram_bank,	//bank地址
		output	reg		flag_init_end	//sdram初始化结束标志	
		);
		
	parameter	CMD_END		=	4'd11,		//初始化结束时的命令计数器的值
			CNT_200US	=	14'd1_0000,	
			NOP		=	4'b0111,	//空操作命令
			PRECHARGE	=	4'b0010,	//预充电命令
			AUTO_REF	=	4'b0001,	//自刷新命令
			MRSET		=	4'b0000;	//模式寄存器设置命令

	reg	[13:0]	cnt_200us;		//200us计数器
	reg		flag_200us;		//200us结束标志(200us结束后,一直拉高)
	reg	[3:0]	cnt_cmd;		//命令计数器,便于控制在某个时候发送特定指令
	reg		flag_init;		//初始化标志:初始化结束后,该标志拉低
//flag_init 
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_init	<=	1'b1;
		else if(cnt_cmd == CMD_END)
			flag_init	<=	1'b0;	
//cnt_200us	
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cnt_200us	<=	14'd0;
		else if(cnt_200us == CNT_200US)
			cnt_200us	<=	14'd0;
		else if(flag_200us == 1'b0)
			cnt_200us	<=	cnt_200us + 1'b1;
//flag_200us
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_200us	<=	1'b0;
		else if(cnt_200us == CNT_200US)
			flag_200us	<=	1'b1;
//cnt_cmd
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cnt_cmd	<=	4'd0;
		else if(flag_200us == 1'b1 && flag_init == 1'b1)
			cnt_cmd	<=	cnt_cmd + 1'b1;
//flag_init_end
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_init_end	<=	1'b0;
		else if(cnt_cmd == CMD_END)
			flag_init_end	<=	1'b1;
		else
			flag_init_end	<=	1'b0;
//cmd_reg
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cmd_reg	<=	NOP;
		else if(cnt_200us == CNT_200US)
			cmd_reg	<=	PRECHARGE;
		else if(flag_200us)
			case(cnt_cmd)
				4'd0:
					cmd_reg	<=	PRECHARGE;	//预充电命令
				4'd6:
					cmd_reg	<=	AUTO_REF;
				4'd10:
					cmd_reg	<=	MRSET;		//模式寄存器设置
				default:
					cmd_reg	<=	NOP;
			endcase
//sdram_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_addr	<=	12'd0;
		else case(cnt_cmd)
			4'd0:
				sdram_addr	<=	12'b0100_0000_0000;	//预充电时,A10拉高,对所有Bank操作
			4'd10:
				sdram_addr	<=	12'b0000_0011_0010;	//模式寄存器设置时的指令:CAS=2,Burst Length=4;
			default:
				sdram_addr	<=	12'd0;
		endcase
//sdram_bank
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_bank	<=	2'd0;	//这里仅仅只是初始化,在模式寄存器设置时才会用到且其值为全零,故不赋值

//sdram_clk
	assign	sdram_clk	=	~sclk;
			
endmodule

下面我们来结合代码回顾下初始化过程:

首先,我们需要有200us的稳定期,所以我们便有了一个200us的计数器cnt_200us,而这个计数器是根据flag_200us的低电平来工作的。大家可以看到,falg_200us在200us计时之后一直拉高。在200us计满,即flag_200us拉高之后,我们就需要先给一个“NOP”命令,然后给两次“Precharge”命令,同时选中ALL Banks。

SDRAM写模块介绍

下面咱们先不对SDRAM的初始化进行仿真,等把全部的模块讲完再来仿真。接下来再继续说写操作:首先在我们的仲裁模块,初始化完成之后,就已经跳转到“ARBIT”仲裁状态了,然后,我们在testbench中模拟一个外部的写请求信号。咱们先看下写模块的代码:

 

module	sdram_write(
		input	wire		sclk,
		input	wire		s_rst_n,
		input	wire		key_wr,
		input	wire		wr_en,		//来自仲裁模块的写使能
		input	wire		ref_req,	//来自刷新模块的刷新请求
		input	wire	[5:0]	state,		//顶层模块的状态
		
		output	reg	[15:0]	sdram_dq,	//sdram输入/输出端口
		//output	reg	[3:0]	sdram_dqm,	//输入/输出掩码
		output	reg	[11:0]	sdram_addr,	//sdram地址线
		output	reg	[1:0]	sdram_bank,	//sdram的bank地址线
		output	reg	[3:0]	sdram_cmd,	//sdram的命令寄存器
		output	reg		wr_req,		//写请求(不在写状态时向仲裁进行写请求)
		output	reg		flag_wr_end	//写结束标志(有刷新请求来时,向仲裁输出写结束)
		);
		
	parameter	NOP	=	4'b0111,	//NOP命令
			ACT	=	4'b0011,	//ACT命令
			WR	=	4'b0100,	//写命令(需要将A10拉高)
			PRE	=	4'b0010,	//precharge命令
			CMD_END	=	4'd8,
			COL_END	=	9'd508,		//最后四个列地址的第一个地址
			ROW_END	=	12'd4095,	//行地址结束
			AREF	=	6'b10_0000,	//自动刷新状态
			WRITE	=	6'b00_1000;	//状态机的写状态
			
	reg		flag_act;			//需要发送ACT的标志			
	reg	[3:0]	cmd_cnt;			//命令计数器
	reg	[11:0]	row_addr;			//行地址
	reg	[11:0]	row_addr_reg;			//行地址寄存器
	reg	[8:0]	col_addr;			//列地址
	reg		flag_pre;			//在sdram内部为写状态时需要给precharge命令的标志

//flag_pre
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_pre	<=	1'b0;
		else if(col_addr == 9'd0 && flag_wr_end == 1'b1)
			flag_pre	<=	1'b1;
		else if(flag_wr_end == 1'b1)
			flag_pre	<=	1'b0;
	
//flag_act
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_act	<=	1'b0;
		else if(flag_wr_end)
			flag_act	<=	1'b0;
		else if(ref_req == 1'b1 && state == AREF)
			flag_act	<=	1'b1;
//wr_req
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			wr_req	<=	1'b0;
		else if(wr_en == 1'b1)
			wr_req	<=	1'b0;
		else if(state != WRITE && key_wr == 1'b1)
			wr_req	<=	1'b1;
//flag_wr_end
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_wr_end	<=	1'b0;
		else if(cmd_cnt == CMD_END)
			flag_wr_end	<=	1'b1;
		else
			flag_wr_end	<=	1'b0;
//cmd_cnt
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cmd_cnt	<=	4'd0;
		else if(state == WRITE)
			cmd_cnt	<=	cmd_cnt + 1'b1;
		else 
			cmd_cnt	<=	4'd0;
		
//sdram_cmd
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_cmd	<=	4'd0;
		else case(cmd_cnt)
			3'd1:
				if(flag_pre == 1'b1)
					sdram_cmd	<=	PRE;
				else
					sdram_cmd	<=	NOP;
			3'd2:
				if(flag_act == 1'b1 || col_addr == 9'd0)
					sdram_cmd	<=	ACT;
				else
					sdram_cmd	<=	NOP;
			3'd3: 		
				sdram_cmd	<=	WR;

			default:
				sdram_cmd	<=	NOP;		
		endcase
//sdram_dq
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_dq	<=	16'd0;
		else case(cmd_cnt)
			3'd3:
				sdram_dq	<=	16'h0012;
			3'd4:
				sdram_dq	<=	16'h1203;
			3'd5:
				sdram_dq	<=	16'h562f;
			3'd6:
				sdram_dq	<=	16'hfe12;
			default:
				sdram_dq	<=	16'd0;
		endcase
/* //sdram_dq_m
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_dqm	<=	4'd0; */
//row_addr_reg
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			row_addr_reg	<=	12'd0;
		else if(row_addr_reg == ROW_END && col_addr == COL_END && cmd_cnt == CMD_END)
			row_addr_reg	<=	12'd0;
		else if(col_addr == COL_END && flag_wr_end == 1'b1)
			row_addr_reg	<=	row_addr_reg + 1'b1;
		
//row_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			row_addr	<=	12'd0;
		else case(cmd_cnt)
		//因为下边的命令是通过行、列地址分开再给addr赋值,所以需要提前一个周期赋值,以保证在命令到来时能读到正确的地址
			3'd2:
				row_addr	<=	12'b0000_0000_0000;	//在写命令时,不允许auto-precharge	
			default:
				row_addr	<=	row_addr_reg;
		endcase
//col_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			col_addr	<=	9'd0;
		else if(col_addr == COL_END && cmd_cnt == CMD_END)
			col_addr	<=	9'd0;
		else if(cmd_cnt == CMD_END)
			col_addr	<=	col_addr + 3'd4;
//sdram_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_addr	<=	12'd0;
		else case(cmd_cnt)
			3'd2:
				sdram_addr	<=	row_addr;
			3'd3:
				sdram_addr	<=	col_addr;
			
			default:
				sdram_addr	<=	row_addr;
		endcase
//sdram_bank
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_bank	<=	2'b00;
endmodule

在我们的模块端口列表中,用key_wr来接收写请求信号,这个写请求信号,是在没有写完之前一直拉高的,在写完了全部数据之后才拉低的。当然这个代码的话,还是按照Kevin在上一篇博文中的理论来的,大家只要好好理解理论就可以很轻松的明白为什么代码要这样写了。

在写模块中,Kevin是让SDRAM循环着写16’h0012,16’h1203,16’h562f,16’hfe12这四个数据。

另外一点,在我们的这个写模块中,Kevin是在每写完4个数据,也就是突发结束后,有一个写完标志,从而使状态机跳转到仲裁状态,然后如果数据没写完,由于写请求是拉高的,所以如果此时没有刷新请求,那状态机还是会跳转到写状态继续写的。

SDRAM读操作模块

首先,咱们依然先上代码:

module	sdram_read(
		input	wire		sclk,
		input	wire		s_rst_n,
		input	wire		rd_en,
		input	wire	[5:0]	state,
		input	wire		ref_req,		//自动刷新请求
		input	wire		key_rd,			//来自外部的读请求信号
		input	wire	[15:0]	rd_dq,		//sdram的数据端口
		
		output	reg	[3:0]	sdram_cmd,
		output	reg	[11:0]	sdram_addr,
		output	reg	[1:0]	sdram_bank,
		output	reg		rd_req,			//读请求
		output	reg		flag_rd_end		//突发读结束标志
		);

	parameter	NOP	=	4'b0111,
			PRE	=	4'b0010,
			ACT	=	4'b0011,
			RD	=	4'b0101,		//SDRAM的读命令(给读命令时需要给A10拉低)
			CMD_END	=	4'd12,			//
			COL_END	=	9'd508,			//最后四个列地址的第一个地址
			ROW_END	=	12'd4095,		//行地址结束
			AREF	=	6'b10_0000,		//自动刷新状态
			READ	=	6'b01_0000;		//状态机的读状态
		
	reg	[11:0]	row_addr;
	reg	[8:0]	col_addr;
	reg	[3:0]	cmd_cnt;
	reg		flag_act;				//发送ACT命令标志(单独设立标志,便于跑高速)

//flag_act
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_act	<=	1'b0;
		else if(flag_rd_end == 1'b1 && ref_req == 1'b1)
			flag_act	<=	1'b1;
		else if(flag_rd_end == 1'b1)
			flag_act	<=	1'b0;
//rd_req
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			rd_req	<=	1'b0;
		else if(rd_en == 1'b1)
			rd_req	<=	1'b0;
		else if(key_rd == 1'b1 && state != READ)
			rd_req	<=	1'b1;
//cmd_cnt
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cmd_cnt	<=	4'd0;
		else if(state == READ)
			cmd_cnt	<=	cmd_cnt + 1'b1;
		else
			cmd_cnt	<=	4'd0;
//flag_rd_end
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_rd_end	<=	1'b0;
		else if(cmd_cnt == CMD_END)
			flag_rd_end	<=	1'b1;
		else
			flag_rd_end	<=	1'b0;
//row_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			row_addr	<=	12'd0;
		else if(row_addr == ROW_END && col_addr == COL_END && flag_rd_end == 1'b1)
			row_addr	<=	12'd0;
		else if(col_addr == COL_END && flag_rd_end == 1'b1)
			row_addr	<=	row_addr + 1'b1;
//col_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			col_addr	<=	9'd0;
		else if(col_addr == COL_END && flag_rd_end == 1'b1)
			col_addr	<=	9'd0;
		else if(flag_rd_end == 1'b1)
			col_addr	<=	col_addr + 3'd4;
//cmd_cnt
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cmd_cnt	<=	4'd0;
		else if(state == READ)
			cmd_cnt	<=	cmd_cnt + 1'b1;
		else
			cmd_cnt	<=	4'd0;
//sdram_cmd
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_cmd	<=	NOP;
		else case(cmd_cnt)
			4'd2:
				if(col_addr == 9'd0)
					sdram_cmd	<=	PRE;
				else
					sdram_cmd	<=	NOP;
			4'd3:	
				if(flag_act == 1'b1 || col_addr == 9'd0)
					sdram_cmd	<=	ACT;
				else
					sdram_cmd	<=	NOP;
			4'd4:
				sdram_cmd	<=	RD;
			default:
				sdram_cmd	<=	NOP;
		endcase
//sdram_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_addr	<=	12'd0;
		else case(cmd_cnt)
			4'd4:
				sdram_addr	<=	{3'd0, col_addr};
			default:
				sdram_addr	<=	row_addr;
		endcase
//sdram_bank
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_bank	<=	2'd0;
		

endmodule

其实读模块和写模块是极其相似的,所以在这里,Kevin就不做赘述,大家慢慢消化吧。

SDRAM自动刷新模块

自动刷新模块算是比较简单的,等15us的时间过了,就向仲裁模块发刷新请求,然后在刷新完成之后,产生刷新结束标志:

/*****************************************************************
*	Module	Name	:	auto_refresh
*	Enegineer	:	Kevin
*	Function	:	sdram自动刷新
*	Blog	Website	:	http://dengkanwen.com
*	Comment		:	在这个模块中并没有bank地址的输出线,需要在顶层模块中设置
******************************************************************/
module	auto_refresh(
		input	wire		sclk,
		input	wire		s_rst_n,
		input	wire		ref_en,
		input	wire		flag_init_end,	//初始化结束标志(初始化结束后,启动自刷新标志)
		
		output	reg	[11:0]	sdram_addr,
		output	reg	[1:0]	sdram_bank,
		output	reg		ref_req,
		output	reg	[3:0]	cmd_reg,
		output	reg		flag_ref_end
		);

	parameter	BANK	=	12'd0100_0000_0000,	//自动刷新是对所有bank刷新
			CMD_END	=	4'd10,
			CNT_END	=	10'd749,	//15us计时结束
			NOP	=	4'b0111,	//
			PRE	=	4'b0010,	//precharge命令
			AREF	=	4'b0001;	//auto-refresh命令
			
			
	reg	[9:0]	cnt_15ms;	//15ms计数器
	reg		flag_ref;	//处于自刷新阶段标志
	reg		flag_start;	//自动刷新启动标志
	reg	[3:0]	cnt_cmd;	//指令计数器
//flag_start
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_start	<=	1'b0;
		else if(flag_init_end == 1'b1)
			flag_start	<=	1'b1;
//cnt_15ms
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cnt_15ms	<=	10'd0;
		else if(cnt_15ms == CNT_END)
			cnt_15ms	<=	10'd0;
		else if(flag_start == 1'b1)
			cnt_15ms	<=	cnt_15ms + 1'b1;
//flag_ref
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_ref	<=	1'b0;
		else if(cnt_cmd == CMD_END)
			flag_ref	<=	1'b0;
		else if(ref_en == 1'b1)
			flag_ref	<=	1'b1;
//cnt_cmd
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cnt_cmd	<=	4'd0;
		else if(flag_ref == 1'b1)
			cnt_cmd	<=	cnt_cmd + 1'b1;
		else
			cnt_cmd	<=	4'd0;
//flag_ref_end
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_ref_end	<=	1'b0;
		else if(cnt_cmd == CMD_END)
			flag_ref_end	<=	1'b1;
		else
			flag_ref_end	<=	1'b0;
//cmd_reg
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			cmd_reg	<=	NOP;
		else case(cnt_cmd)
			3'd0:
				if(flag_ref == 1'b1)
					cmd_reg	<=	PRE;
				else
					cmd_reg	<=	NOP;
			3'd1:
				cmd_reg	<=	AREF;
			3'd5:
				cmd_reg	<=	AREF;
			default:
				cmd_reg	<=	NOP;
		endcase
//sdram_addr
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_addr	<=	12'd0;
		else case(cnt_cmd)
			4'd0:
				sdram_addr	<=	BANK;	//bank进行刷新时指定allbank or signle bank
			default:
				sdram_addr	<=	12'd0;
		endcase
//sdram_bank
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			sdram_bank	<=	2'd0;		//刷新指定的bank
//ref_req
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			ref_req	<=	1'b0;
		else if(ref_en == 1'b1)
			ref_req	<=	1'b0;
		else if(cnt_15ms == CNT_END)
			ref_req	<=	1'b1;
//flag_ref_end
	always	@(posedge sclk or negedge s_rst_n)
		if(s_rst_n == 1'b0)
			flag_ref_end	<=	1'b0;
		else if(cnt_cmd == CMD_END)
			flag_ref_end	<=	1'b1;
		else
			flag_ref_end	<=	1'b0;

endmodule

至此,咱们的整个设计就已经讲完了,至于模块之间怎么连线,Kevin就不再硬性灌输了,留给大家自己完成吧。当然Kevin还是想提醒大家,因为SDRAM的数据总线是双向的,所以需要弄个三态门,在向SDRAM写数据的时候,模块定义的数据总线应为输出型,在接收数据时,需要定义成高阻态。

SDRAM仿真

设计讲完了,咱们来说下仿真,在我们仿真的时候,我们需要用到SDRAM的仿真模型(关于仿真模型,Kevin已经上传到“福利/文档手册”这个栏目下了)。

因为我们需要有一个200us的稳定期,所以我们可以先让Modelsim跑个200us,可以直接在命令窗口输入”run 200us”;200us的稳定器过了之后,接下来应该就是咱们的初始话了,所以我们在让modelsim跑600ns,下面是仿真的结果:

sdram_c

sdram_tb_wave

在上边的仿真中,我们已经知道SDRAM已经初始化成功了,设置的潜伏期为3,突发长度为4

下面我们再运行一段时间,就运行1us吧,往SDRAM中写数据:

sdram_wave

这里的写的数据,就是咱们在写模块中设置的那4个数,只是之前的是用16进制定义的,这里显示的是10进制。

然后我们再看一下读数据:sdram_read

大家可以看下,我们读出来的数据和写进来的数据是不是一样的呢?

20条评论
  • pengjl

    2016年3月16日 下午9:08

    😈 厉害,醍醐灌顶啊!
    我想要实现大约4Gbps的带宽,想使用32bit接口的SDRAM,使用全页突发的方式来做,不知道是否可行。
    我想突发读和刷新相遇时,就主动放弃这次突发读算了。不知道有没更好的办法?

    1. Kevin

      2016年4月29日 下午6:11

      不知道你的系统时钟是多大,不过还是不太建议采用页突发。

  • duoduo

    2016年4月7日 上午11:38

    你好,请问SDRAM的仿真模块可以提供一下给我么,谢谢,我也是新手刚学,向你学习

    1. Kevin

      2016年4月12日 下午7:58

      已经发送到你的QQ邮箱了

  • 小熊

    2016年7月19日 上午11:20

    你好,请问SDRAM的仿真模块可以提供一下给我么,谢谢,我也是新手刚学,向你学习,还有我申请加入qq群了,请回复一下~~~

    1. Kevin

      2016年7月24日 上午12:13

      好的,如果您有啥需要帮助的,也可以直接加我QQ私聊的哈

      1. 0932

        2016年8月10日 下午4:26

        我需要你的ARBIT模块的完整代码,不知博主是否愿意提供一下?

      2. 0932

        2016年8月10日 下午6:41

        请问您QQ多少啊?

  • 0932

    2016年8月10日 下午3:48

    博主有完整代码吗?你的仲裁模块好像只给了给转换流程

  • 高旗

    2016年10月29日 下午12:08

    能提供一下仿真模块代码吗?谢谢您

    1. Kevin

      2016年11月28日 下午10:41

      SDRAM仿真模型可以加入【开源骚客】QQ交流群:312109973进行下载的哈

  • jiangxiang

    2017年1月5日 上午11:24

    文章写得很好,我用的是32bit的SDRAM,刚接触SDAM,这几天一直在看您写的程序,SDRAM的仿真模块可以提供一下吗,非常感谢,能加一下您的QQ吗,想向您请教一下,我的QQ是邮箱账号。

    1. Kevin

      2017年1月13日 下午9:29

      Kevin的QQ号:1024726016

  • huster in SZ

    2017年3月1日 上午9:17

    初始化、读写和刷新都会各自产生一组控制命令和相应地址,请问这个该由哪一组送给外部SDRAM时,也是由仲裁模块内部来判断的??? 仲裁部分能不能提供更完整的代码,谢谢!!! Q:381251085, 网易:wentaocsking@163.com

    1. Kevin

      2017年3月4日 下午10:23

      您可以观看我们录制的SDRAM视频教程,或者加入我们的FPGA讨论群哈

  • 走不尽的路

    2017年12月2日 上午10:07

    初始化模块的73行预充电命令写错了

    1. Kevin

      2017年12月18日 下午9:52

      感谢支持,现已修改

  • Anglo

    2019年11月7日 下午5:59

    您好!方便给一份完整代码吗?非常感谢!

  • Anglo

    2019年11月7日 下午6:00

    还想要仿真model~~~~ 🙄

    1. Kevin

      2019年11月9日 上午10:06

      加我微信吧:opensoc888,我发给你

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注