FPGA综合项目——SDRAM控制器
目录
- 1.整体框架
- 2.串口接收模块
- 3.接收模块测试仿真
- 4.串口发送模块
- 5.发送模块测试仿真
- 6.SDRAM基础学习
- 7.SDRAM顶层模块
- 8.SDRAM初始化模块设计与仿真测试
- 9.自动刷新模块设计与测试
- 10.写模块设计与测试
- 11.读模块设计与仿真测试
- 12.通信处理模块
- 13.顶层模块top
- 14.上板!
- 15.后记:资源使用情况
1.整体框架

本项目的目的,通过串口来对SDRAM进行读写。
首先是串口通信模块,常见的串行通信方式。
再者就是通信处理模块,具体的通信设置,发送什么命令是写?什么命令是读?发的什么数据?等等。
其次就是SDRAM模块,包括初始化、自动刷新、读、写等。(重点)
最后就是fifo的使用,因为SDRAM的读写是远远快于串口通讯的速度的,因此我们需要对写和读的数据进行缓存,不然就会丢失数据。
2.串口接收模块
串口通信我就不多说了,直接看时序图吧:

①当rx输入有下降沿,传输就开始了
②传输的速率看波特率,根据波特率计算每个数据发送所需要的时间(时间计算在代码注释中)
③连停止位一共传输9位数据,传完结束,完成一整次的传输
读完时序我们来思考一下我们需要怎么实现这个时序:
首先,下降沿开始传输——那么我们就要做一个监沿器来识别下降沿,这里还要额外考虑一个问题,那就是rx信号是由电脑发过来的,和FPGA是不一样的时钟,因此我们要先做异步处理,再对rx进行监沿。所以,这里需要2个D触发器进行异步处理,再加一个D触发器构成监沿器。
其次,根据波特率算出来的时间,用一个计数器计一个数据的时间,再用一个计数器来计数据个数。
接着,将输入给到输出,注意,这里应当用中间取值法,避免边沿的跳变。
最后,是代码:
module rx
(input clk,input rst_n,input rx,output reg [7:0]rx_data,output reg rx_over
);//参数
parameter _9600 = 5208; //9600波特率
parameter _8bit = 9; //加上起始位共9位
parameter MIDDLE = _9600/2 - 1; //中间取值
//内部信号
reg rx_ff0;
reg rx_ff1;
reg rx_ff2;
wire pose;
wire nege;
reg flag_rx;
reg [3:0] cnt_8bit;
reg bit_cnt_flag;
reg [12:0] cnt_9600;
//功能块
always@(posedge clk )beginrx_ff0 <= rx;rx_ff1 <= rx_ff0;rx_ff2 <= rx_ff1;
endassign pose = rx_ff2 == 0 && rx_ff1 == 1;
assign nege = rx_ff2 == 1 && rx_ff1 == 0;always@(posedge clk or negedge rst_n)beginif(!rst_n)flag_rx <= 0;else if(nege)flag_rx <= 1;else if(cnt_8bit == 0 && cnt_9600 == _9600-1 )flag_rx <= 0;
endalways@(posedge clk or negedge rst_n)beginif(!rst_n)cnt_9600 <= 0;else if(cnt_9600 == _9600-1)cnt_9600 <= 0;else if(flag_rx)cnt_9600 <= cnt_9600 + 1;elsecnt_9600 <= 0;
endalways@(posedge clk or negedge rst_n)beginif(!rst_n)bit_cnt_flag <= 0;else if(cnt_9600 == MIDDLE)bit_cnt_flag <= 1;elsebit_cnt_flag <= 0;
endalways@(posedge clk or negedge rst_n)beginif(!rst_n)cnt_8bit <= 0;else if(bit_cnt_flag && cnt_8bit == _8bit - 1 )cnt_8bit <= 0;else if(bit_cnt_flag)cnt_8bit <= cnt_8bit + 1;
endalways@(posedge clk or negedge rst_n)beginif(!rst_n)rx_over <= 0;else if(bit_cnt_flag && cnt_8bit == _8bit - 1)rx_over <= 1;else rx_over <= 0;
endalways@(posedge clk or negedge rst_n)beginif(!rst_n)rx_data <= 0;else if(bit_cnt_flag && cnt_8bit >= 1 )rx_data <= {rx_ff1,rx_data[7:1]};
endendmodule
3.接收模块测试仿真
测试模块主要的作用是编写测试的输入,以方便进行仿真。基本的测试编写可见测试文件编写以及modelsim仿真。
测试文件的功能在于,一位一位的给出rx信号,通过modelsim对源文件进行验证仿真;
①先用$ readmemh(’’ ./x.txt’’,mem) 命令将txt文本的数据以十六进制读到寄存器mem中
②再一位一位拆分输入的数据
③按照’’ 0 ,8个数据, 1’’ 这样的格式给rx赋值
④注意一个bit持续的时间
以下是测试代码:
`timescale 1 ns/1 ns
module tb_rx();//输入信号
reg clk ;
reg rst_n;
reg rx;//输出信号
wire rx_over;
wire[7:0] rx_data;//8位寄存器 4个
reg [7:0] mem_a[3:0];//参数
parameter CYCLE = 20;//待测试的模块例化rx u1(.clk (clk ), .rst_n (rst_n ), .rx (rx ), .rx_data (rx_data ), .rx_over (rx_over ) ); //生成50M时钟
initial beginclk = 0;forever#(CYCLE/2)clk=~clk;
end//产生复位信号
initial beginrst_n = 1;#2;rst_n = 0;#2rst_n = 1;
end//把文件里的数据写入mem_a存储器中
//此文件写好放在sim的文件夹中
initial $readmemh("./rx.txt",mem_a); initial beginrx = 0; #20rx = 1; #20byte(); //task
end//task1 , 将txt文件的4个数存到寄存器a中
task byte();integer i;for(i =0;i<4;i = i+1)beginbit(mem_a[i]); end
endtask//task2 , 将txt的每一个数据分为8位,一位一位取值
task bit(input [7:0] data_inter );integer i;for(i =0;i<10;i = i+1)begincase(i)0: rx <= 1'b0 ;1: rx <= data_inter[0];2: rx <= data_inter[1];3: rx <= data_inter[2];4: rx <= data_inter[3];5: rx <= data_inter[4];6: rx <= data_inter[5];7: rx <= data_inter[6];8: rx <= data_inter[7];default: rx <= 1; endcase#104160; //一个bit数据持续的时间 5208个时钟end
endtaskendmodule
仿真结果:


4.串口发送模块
发送的时序和rx相仿,只不过多了一个触发信号trigger,时序图如下:


①同样使用两个计数器,分别计数一个bit的时间和bit的个数
②使用一个寄存器存下输入进来的发送数据
③发送标志flag_tx的有效区域
④tx的发送 使用一个case语句完成,且在flag_tx下完成,初值为1
代码如下:
module tx
(input clk,input rst_n,input [7:0] tx_data,input trig,output reg tx
);//参数
parameter _9600 = 5208;
parameter _8bit = 10;
//内部信
reg [7:0] reg_tx_data;
reg flag_tx;
reg [12:0] cnt_9600;
reg [3:0] cnt_8bit;//将输入数据存入寄存器
always@(posedge clk or negedge rst_n)beginif(!rst_n)reg_tx_data <= 0;else if(trig && !flag_tx)reg_tx_data <= tx_data;end
//传输有效区域信号
always@(posedge clk or negedge rst_n)beginif(!rst_n)flag_tx <= 0;else if(trig && !flag_tx)flag_tx <= 1;else if(flag_tx && cnt_8bit == _8bit-1 && cnt_9600 == _9600-1)flag_tx <=0;
end
//一位数据传输时间计数
always@(posedge clk or negedge rst_n)beginif(!rst_n)cnt_9600 <= 0;else if(flag_tx)beginif(cnt_9600 == _9600-1)cnt_9600 <= 0;elsecnt_9600 <= cnt_9600 + 1;endend
//数据个数计数
always@(posedge clk or negedge rst_n)beginif(!rst_n)cnt_8bit <= 0;else if(flag_tx && cnt_9600 == _9600-1)beginif(cnt_8bit == _8bit-1)cnt_8bit <= 0;elsecnt_8bit <= cnt_8bit + 1;end
end
//tx
always@(posedge clk or negedge rst_n)beginif(!rst_n)tx <= 0;else if(flag_tx)case(cnt_8bit)0: tx <= 0;1: tx <= reg_tx_data[0];2: tx <= reg_tx_data[1];3: tx <= reg_tx_data[2];4: tx <= reg_tx_data[3];5: tx <= reg_tx_data[4];6: tx <= reg_tx_data[5];7: tx <= reg_tx_data[6];8: tx <= reg_tx_data[7];default:tx <= 1;endcaseelsetx <= 1;
end
endmodule
5.发送模块测试仿真
发送模块的仿真测试文件就比rx的简单很多了,关键在于延迟的计算,如果延迟不对,那么波特率就不是9600,那么数据就会乱套;代码如下:
`timescale 1 ns/1 nsmodule tb_tx();//输入信号
reg[7:0] tx_data ;
reg trig;
reg clk ;
reg rst_n;//输出信号
wire tx;//参数
parameter CYCLE = 20;//待测试的模块例化
tx u2
(.clk (clk ), .rst_n (rst_n ),.trig (trig ),.tx_data (tx_data ),.tx (tx )
);//生成本地时钟50M
initial beginclk = 0;forever#(CYCLE/2)clk=~clk;
end//产生复位信号
initial beginrst_n = 1;#2;rst_n = 0;#2rst_n = 1;
endinitial begintx_data <= 8'd0;trig <= 0;#20;tx_data <= 8'b01010101;trig <= 1'b1;#937440; //一共9个比特 ,因此是937440(5208*20*9)的时间trig <= 1'b0;#2;tx_data <= 8'b10101010;trig <= 1'b1;#937440;trig <= 1'b0;
endendmodule
仿真图如下:

6.SDRAM基础学习
对SDRAM的基础学习,只要弄清楚我下面的几个问题,就基本上可以了。
①SDRAM是什么?
Synchronous Dynamic Random Access Memory,同步动态随机存储器,说白了,就是用来存数据用的外设(具体看此链接的6.2 DRAM 小结)。那么怎么存呢?看下一个问题。
②SDRAM有什么?
既然作为存东西的外设,那么必定有存放数据的结构,而这个就够就叫做bank,简单的说,一个bank就是一个存储阵列,利用行地址和列地址来进行排号标记,而一个SDRAM一般都有4个bank,它的容量就取决于bank的容量以及bank的数量。如图:

③SDRAM我们要懂什么?
首先,要明白他存储的原理,也就是为什么要刷新:存储电容会漏电,所以必须一定的间隔进行充电,这个过程就叫刷新。
其次,所用SDRAM的引脚以及数据手册要熟悉,引脚的作用,寄存器的配置都要从数据手册上获得信息。
最后就是各个功能的时序,也要从数据手册找出。以下以ISSI的SDRAM芯片为例讲解,这是此芯片的引脚:

我们进而查看命令所对应的引脚:
此表是命令列表,并详细说明了如何通过信号组合来形式命令。当然还要去读一些命令所要求的时序,由于手册太长,在这不可能一一列举,我们直接从项目出发,用到什么说什么,看下一节具体操作即可,此节只需了解以下SDRAM的基本原理。
7.SDRAM顶层模块
作为SDRAM的顶层模块,在写的时候是要一步一步更新的,在每个模块调试时,都会加入其顶层模块进行仿真;
8.SDRAM初始化模块设计与仿真测试
初始化SDRAM,我们要参照数据手册的初始化的时序图,如下:

图中的时间没有具体说明,因此我们还要在数据手册上查找相应的时间:


由此,我们时钟是20ns一个周期,所以trc取3个周期、trp取1个周期、tmrd取2个周期。那么总体的时序如下:
①由时序图可知,电源上电需要缓冲100us,我们这保守起见,给他200us延迟

②在200us过后,执行红色框框中的命令PRECARGE(bank充电),找到此命令对应的四个引脚的高低电平

③接着经过一个周期T,给到自动刷新命令

④接着4T后,再给一次自动刷新命令
⑤又4T后,进行模式寄存器命令完成初始化过程,而配置寄存器模式需要看相应的sdram地址,下图是地址对应的配置信息:

而我们设置的sdram_addr为0000_0011_0010
代码及注释如下:
module sdram_initial
(input clk,input rst_n,output reg [3:0] cmd_reg, //用来寄存命令的信号output reg [11:0] sdram_addr, //时序图中的A0-A11output wire init_end //初始化结束信号
);//参数,包括延迟200us、4个指令对应的引脚高低电平
localparam DELAY_200US = 10_000;
localparam NOP = 4'b0111;
localparam PRECHARGE = 4'b0010;
localparam AUTOREFLASH = 4'b0001;
localparam MODESET = 4'b0000;//内部信号
reg [13:0] cnt0;
reg [3:0] cnt1;
wire flag_200us;//200us计数
assign flag_200us = (cnt0 >= DELAY_200US-1)? 1'b1:1'b0;always @(posedge clk or negedge rst_n)beginif(!rst_n)begincnt0 <= 0;endelse if(!flag_200us)begincnt0 <= cnt0 + 1;end
end//周期个数计数
assign init_end = (cnt1>='d10)?1:0;always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt1 <= 0;endelse if(flag_200us && !init_end)beginif(cnt1==11-1)cnt1 <= 0;elsecnt1 <= cnt1 + 1;end
end//命令操作
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begincmd_reg <= NOP;endelse if(flag_200us)begincase(cnt1)0: cmd_reg <= PRECHARGE;1: cmd_reg <= AUTOREFLASH;5: cmd_reg <= AUTOREFLASH;9: cmd_reg <= MODESET;default: cmd_reg <= NOP;endcaseend
end//sdram_addr
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginsdram_addr <= 0;endelse if(flag_200us)begincase(cnt1)9: sdram_addr <= 12'b0000_0011_0010;default: sdram_addr <= 12'b0100_0000_0000;endcaseend
endendmodule
再完成顶层模块的编写:
module sdram_top
(input clk,input rst_n,output wire sdram_clk,output wire sdram_cke,output wire sdram_cs,output wire sdram_cas,output wire sdram_ras,output wire sdram_we,output wire [1 :0 ] sdram_bank,output wire [11:0 ] sdram_addr,output wire [1 :0 ] sdram_dqm,inout [15:0 ] sdram_dq
);//参数
//状态机采用独热码定义
localparam IDLE = 5'b0_0001;
localparam HANDLE = 5'b0_0010;
localparam AUTOREFRESH = 5'b0_0100;
localparam WRITE = 5'b0_1000;
localparam READ = 5'b1_0000;localparam CMD_NOP = 4'b0111 ;//内部信号
wire init_end;
wire [3:0] init_cmd;
wire [11:0] init_addr;//例化各模块
sdram_initial u1_init
(.clk (clk),.rst_n (rst_n),.cmd_reg (init_cmd),.sdram_addr (init_addr), .init_end (init_end)
);assign sdram_cke = 1'b1;
assign sdram_clk = ~clk;
assign sdram_dqm = 2'b00;
assign sdram_addr = init_addr;assign {sdram_cs,sdram_ras,sdram_cas,sdram_we} = init_cmd;endmodule
至此,初始化完成、顶层完成后,接下来就是对初始化模块的测试了,我们对顶层模块做测试,每加一个模块,我们就测试一次。测试我们需要用到一个插件:

完成之后呢,编写测试文件:
`timescale 1 ns/1 nsmodule sdram_top_tb();//输入信号
reg clk ;
reg rst_n;//输出信号
wire sdram_clk ;
wire sdram_cke ;
wire sdram_cs ;
wire sdram_cas ;
wire sdram_ras ;
wire sdram_we ;
wire [1:0] sdram_bank ;
wire [11:0] sdram_addr ;
wire [1:0] sdram_dqm ;
wire [15:0] sdram_dq ;//参数
parameter CYCLE = 20;
parameter RST_TIME = 3 ;//待测试的模块例化
sdram_top u1_sdram_top
(.clk (clk ), .rst_n (rst_n ),.sdram_clk (sdram_clk ),.sdram_cke (sdram_cke ),.sdram_cs (sdram_cs ),.sdram_cas (sdram_cas ),.sdram_ras (sdram_ras ),.sdram_we (sdram_we ),.sdram_bank (sdram_bank ),.sdram_addr (sdram_addr ),.sdram_dqm (sdram_dqm ), .sdram_dq (sdram_dq ));
//插件例化,相当于是一个sdram硬件
sdram_model_plus u2_plus
( .Dq (sdram_dq), .Addr (sdram_addr), .Ba (sdram_bank), .Clk (sdram_clk), .Cke (sdram_cke), .Cs_n (sdram_cs), .Ras_n (sdram_ras), .Cas_n (sdram_cas), .We_n (sdram_we), .Dqm (sdram_dqm),.Debug (1'b1)
);//重定义具体sdram的参数,因为插件的和实际的并不一样
defparam u2_plus.addr_bits = 12;
defparam u2_plus.data_bits = 16;
defparam u2_plus.col_bits = 9;
defparam u2_plus.mem_sizes = 2*1024*1024; //2M 一个bank// 生成本地时钟50M
initial beginclk = 0;forever#(CYCLE/2)clk=~clk;
end//产生复位信号
initial beginrst_n = 1;#2;rst_n = 0;#(CYCLE*RST_TIME);rst_n = 1;
endendmodule
仿真结果:充电范围所有bank、自动刷新一次、自动刷新两次、模式设置为输出延迟3T、突发长度4、连续突发类型;

9.自动刷新模块设计与测试
那么刷新的频率怎么算呢?首先数据手册上写这4096/64ms,意思就是64ms内对4096行进行刷新,也就是64000/4096 约为15us一次刷新。
4096行是怎么来的呢?我们的行地址不是有12位嘛,所以2^12为4096行。
自动刷新的操作:每15us发送请求进行刷新操作
同样的,自动刷新模块照样需要我们取查数据手册的自动刷新时序图,如下所示:

同样的,trc取3个时钟周期、trp取1个时钟周期,时序图中的顺序是:
①在初始化完成后,进行自动刷新(刷新完成信号)。先预充电,地址为A10为1,选中全部bank
②过一个时钟周期,进行自动刷新
③图上是进行两次自动刷新,实际上一次就行
④刷新模块的作用:每15us对全部bank(A10置1)进行一次刷新
⑤从这里开始引入了仲裁模块(直接写在顶层模块中了HANDLE状态),其他执行模块与仲裁模块之间有三线连接,分别是请求、使能、结束标志;仲裁模块通过自身判断再给出使能信号,给出使能信号后状态跳转到其他执行状态,完成执行任务后再跳回仲裁状态

④刷新模块代码如下:
module autorefresh
(input clk,input rst_n,input init_end,input ref_en,output wire ref_req,output wire ref_end,output reg [3:0] autorefresh_cmd,output wire [11:0] sdram_addr);//计数参数
localparam DELAY_15US = 750;
//命令参数
localparam NOP = 4'b0111;
localparam PRECHARGE = 4'b0010;
localparam AUTOREFLASH = 4'b0001;
localparam MODESET = 4'b0000;//内部信号
reg [9:0] cnt0;
reg [3:0] cnt1;
reg flag_ref; //刷新进行中标志//功能
//15us计数器
always @(posedge clk or negedge rst_n)beginif(!rst_n)cnt0 <= 0;else if(init_end)beginif(cnt0== DELAY_15US-1)cnt0 <= 0;elsecnt0 <= cnt0 + 1;end
end//刷新请求,每15us一次
assign ref_req = (cnt0 >= DELAY_15US-1)? 1'b1 : 1'b0;//flag_ref
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_ref <= 0;endelse if(ref_en) beginflag_ref <= 1;endelse if(ref_end) beginflag_ref <=0;end
end//时钟T个数计数器
always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt1 <= 0;endelse if(flag_ref)beginif(cnt1== 4-1)cnt1 <= 0;elsecnt1 <= cnt1 + 1;end
end//给出命令
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginautorefresh_cmd <= NOP;endelse case(cnt1)1: autorefresh_cmd <= PRECHARGE;2: autorefresh_cmd <= AUTOREFLASH;default: autorefresh_cmd <= NOP;endcase
end//sdram_addr,A10置1选中所有bank
assign sdram_addr = 12'b0100_0000_0000;//ref_end
assign ref_end = (cnt1>='d3)? 1 : 0;endmodule
接着就是更新加入仲裁机制后的顶层模块:
module sdram_top
(input clk,input rst_n,output wire sdram_clk,output wire sdram_cke,output wire sdram_cs,output wire sdram_cas,output wire sdram_ras,output wire sdram_we,output wire [1 :0 ] sdram_bank,output wire [11:0 ] sdram_addr,output wire [1 :0 ] sdram_dqm,inout [15:0 ] sdram_dq
);//状态机采用独热码定义
localparam IDLE = 5'b0_0001;
localparam HANDLE = 5'b0_0010;
localparam AUTOREFRESH = 5'b0_0100;
localparam WRITE = 5'b0_1000;
localparam READ = 5'b1_0000;
//空命令
localparam CMD_NOP = 4'b0111 ;//内部信号
//初始化
wire init_end;
wire [3:0] init_cmd;
wire [11:0] init_addr;
//状态机
reg [4:0] state;
//自动刷新
wire ref_req;
reg ref_en;
wire ref_end;
wire [3:0] ref_cmd;
wire [11:0] ref_addr;//例化模块
sdram_initial u1_init
(.clk (clk),.rst_n (rst_n),.cmd_reg (init_cmd),.sdram_addr (init_addr), .init_end (init_end)
);autorefresh u2_autorefresh
(.clk (clk),.rst_n (rst_n),.init_end (init_end),.ref_en (ref_en),.ref_req (ref_req),.ref_end (ref_end),.autorefresh_cmd (ref_cmd),.sdram_addr (ref_addr) );//功能
//仲裁模块状态机
always@(posedge clk or negedge rst_n)beginif(!rst_n)state <= IDLE;else case(state)IDLE:if(init_end)state <= HANDLE;elsestate <= IDLE; HANDLE: if(ref_en)state <= AUTOREFRESH;else state <= HANDLE;AUTOREFRESH: if(ref_end)state <= HANDLE;elsestate <= AUTOREFRESH;default: state <=IDLE;endcaseend//刷新使能
always@(posedge clk or negedge rst_n)beginif(!rst_n)ref_en <= 0;else if(ref_req && state == HANDLE)ref_en <= 1;elseref_en <= 0;
endassign sdram_cke = 1'b1;
assign sdram_clk = ~clk;
assign sdram_dqm = 2'b00;
assign sdram_addr = (state == IDLE)?init_addr:ref_addr;
assign {sdram_cs,sdram_ras,sdram_cas,sdram_we} = (state == IDLE)? init_cmd:ref_cmd;endmodule
下面进行仿真测试,测试文件不用修改;
仿真结果如下:

10.写模块设计与测试
写模块的设计主要考虑三个方面:第一,写模块自身的时序。第二,写模块与top模块状态机的连接(特别是从写状态跳出到仲裁状态)。第三,和自动刷新模块的连接。
①首先,从SDRAM本身的时序来说,如下第一个图,写有两种模式,一种是WRITE,另一种是WRITEA,前者可以连续写更加高效,所以我们选择用WRITE。(粗线为自动跳转)因为WRITEA会自动跳转到充电状态,所以效率不高。

那么写时序从数据手册读出时序图如下:先激活,再写,突发长度为4。我们写模块的功能就是写两行(测试用),同时要兼顾自动刷新!

写模块的状态机如下:

② 写模块与top模块的连接,通过一个flag_wr_end信号,使得top的状态机从写状态跳到处理(仲裁)状态,来完成自动刷新的动作,这里需要对刷新模块的刷新使能进行修改,由原来的组合逻辑改为时序逻辑产生,修改后的刷新模块如下:
module autorefresh
(input clk,input rst_n,input init_end,input ref_en,output reg ref_req,output wire ref_end,output reg [3:0] autorefresh_cmd,output wire [11:0] sdram_addr);//计数参数
localparam DELAY_15US = 750;
//命令参数
localparam NOP = 4'b0111;
localparam PRECHARGE = 4'b0010;
localparam AUTOREFLASH = 4'b0001;
localparam MODESET = 4'b0000;//内部信号
reg [9:0] cnt0;
reg [3:0] cnt1;
reg flag_ref; //刷新进行中标志//功能
//15us计数器
always @(posedge clk or negedge rst_n)beginif(!rst_n)cnt0 <= 0;else if(init_end)beginif(cnt0== DELAY_15US-1)cnt0 <= 0;elsecnt0 <= cnt0 + 1;end
end//刷新请求,每15us一次 ,直到刷新使能来之前,都为1
always @(posedge clk or negedge rst_n) beginif(rst_n == 1'b0)ref_req <= 1'b0;else if(ref_en == 1'b1)ref_req <= 1'b0;else if(cnt0 >= DELAY_15US-1)ref_req <= 1'b1;
end
//flag_ref
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_ref <= 0;endelse if(ref_en) beginflag_ref <= 1;endelse if(ref_end) beginflag_ref <=0;end
end//时钟T个数计数器
always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt1 <= 0;endelse if(flag_ref)beginif(cnt1== 4-1)cnt1 <= 0;elsecnt1 <= cnt1 + 1;end
end//给出命令
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginautorefresh_cmd <= NOP;endelse case(cnt1)1: autorefresh_cmd <= PRECHARGE;2: autorefresh_cmd <= AUTOREFLASH;default: autorefresh_cmd <= NOP;endcase
end//sdram_addr,A10置1选中所有bank
assign sdram_addr = 12'b0100_0000_0000;//ref_end
assign ref_end = (cnt1>='d3)? 1 : 0;endmodule
③写模块与刷新模块的连接,通过刷新请求和刷新结束标志连接,当刷新请求来了,写模块完成当前的突发后停止,进入REQ状态,并使top的状态跳转至处理状态,以便进行刷新。
④写模块代码如下,有详细注释
module sdram_write
(input clk, //输入,50M时钟input rst_n, //输入,异步复位input wr_en, //输入,写使能input ref_req, //输入,刷新请求,用来中断写input wr_trigger, //输入,写触发信号input ref_end, //输入,刷新结束,可以继续写output reg wr_req, //输出,写请求output reg flag_wr_end,//输出,top状态机跳出写状态信号 output reg [3:0] wr_cmd, //输出,写命令output wire [1:0] bank_addr, //输出,bank地址output reg [11:0] wr_addr, //输出,写地址output reg [15:0] wr_data //输出,写数据,此代码在本模块内产生);//状态参数
localparam IDLE = 5'b0_0001;
localparam REQ = 5'b0_0010;
localparam ACTIVE = 5'b0_0100;
localparam WRITE = 5'b0_1000;
localparam PRE = 5'b1_0000;
//命令参数
localparam CMD_NOP = 4'b0111;
localparam CMD_PRECHARGE = 4'b0010;
localparam CMD_AUTOREFLASH = 4'b0001;
localparam CMD_ACTIVE = 4'b0011;
localparam CMD_WRITE = 4'b0100;//内部信号
reg flag_wr; //写区域
reg [4:0] state_wr; //写时序状态
reg flag_ac_end; //激活状态计数,计数完成跳转下一状态
reg flag_pre_end; //充电状态计数,计数完成跳转下一状态
reg change_row; //换行,一行写满(512个数据),要换行,再激活新的一行
reg wr_data_end; //数据写完信号
reg [1:0] burst_cnt; //突发计数器,一次写入多少数据的控制
reg [1:0] burst_cnt_ff; //打一拍
reg [3:0] active_cnt; //激活计数器
reg [3:0] pre_cnt; //充电计数器
reg [6:0] col_cnt; //数据计数器,满512换行,归零
reg [11:0] row_addr; //行地址
wire [8:0] col_addr; //列地址//功能
//状态机
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginstate_wr <= IDLE;endelse begincase(state_wr)IDLE:if(wr_trigger)state_wr <= REQ;elsestate_wr <= IDLE;REQ:if(wr_en)state_wr <= ACTIVE;elsestate_wr <= REQ;ACTIVE:if(flag_ac_end)state_wr <= WRITE;elsestate_wr <= ACTIVE;WRITE:if(wr_data_end)state_wr <= PRE;else if(ref_req && burst_cnt_ff == 3 && flag_wr )state_wr <= PRE;else if(change_row && flag_wr)state_wr <= PRE;PRE:if(ref_req && flag_wr)state_wr <= REQ;else if(flag_pre_end && flag_wr)state_wr <= ACTIVE;else if(wr_data_end)state_wr <= IDLE;default:state_wr <= IDLE;endcaseend
end//burst_cnt_ff 打一拍
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginburst_cnt_ff <= 0;endelse burst_cnt_ff <= burst_cnt;
end//数据输出
always @(*)begincase(burst_cnt_ff)0: wr_data = 'd1;1: wr_data = 'd2;2: wr_data = 'd3;3: wr_data = 'd4;endcase
end//命令
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginwr_cmd <= CMD_NOP;endelse case(state_wr)ACTIVE:if(active_cnt == 'd0)wr_cmd <= CMD_ACTIVE;elsewr_cmd <= CMD_NOP;WRITE:if(burst_cnt == 'd0)wr_cmd <= CMD_WRITE;elsewr_cmd <= CMD_NOP;PRE:if(pre_cnt == 'd0)wr_cmd <= CMD_PRECHARGE;elsewr_cmd <= CMD_NOP;default:wr_cmd <= CMD_NOP;endcase
end//写地址
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginwr_addr <= 0;endelse case(state_wr)ACTIVE:if(active_cnt == 'd0)wr_addr <= row_addr;WRITE:wr_addr <= {3'b000,col_addr};PRE:if(pre_cnt == 'd0)wr_addr <= {12'b0100_0000_0000};default:wr_addr <= 0; endcase
end//bank地址
assign bank_addr = 2'b00;//flag_wr
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_wr <= 0;endelse if(wr_trigger && !flag_wr)flag_wr <= 1;else if(wr_data_end)flag_wr <= 0;
end//act计数器,计数激活所需要的时间
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginactive_cnt <= 0;endelse if(state_wr == ACTIVE)active_cnt <= active_cnt + 1;elseactive_cnt <= 0;
end//刷新计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginpre_cnt <= 0;endelse if(state_wr == PRE) beginpre_cnt <= pre_cnt + 1;endelsepre_cnt <= 0;
end//col计数器,列计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begincol_cnt <= 0;endelse if(col_addr == 'd511) col_cnt <= 0;else if(burst_cnt =='d3)col_cnt <= col_cnt + 1;
end//产生flag_ac_end信号
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_ac_end <= 0;endelse if(active_cnt >= 'd3) beginflag_ac_end <= 1;endelseflag_ac_end <= 0;
end
//产生flag_pre_end信号
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_pre_end <= 0;endelse if(pre_cnt >= 'd3) flag_pre_end <= 1;elseflag_pre_end <= 0;
end//产生flag_wr_end信号(写一次结束)
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_wr_end <= 0;endelse if(state_wr == PRE && ref_req ) flag_wr_end <= 1;else if(state_wr == PRE && wr_data_end)flag_wr_end <= 1;elseflag_wr_end <= 0;
end//写所有结束标志
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginwr_data_end <= 0;endelse if(row_addr == 'd1 && col_addr == 'd511 ) beginwr_data_end <= 1;endelsewr_data_end <= 0;
end//突发长度计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginburst_cnt <= 0;endelse if(state_wr == WRITE) beginburst_cnt <= burst_cnt +1;endelseburst_cnt <= 0;
end//col_addr 列地址计算
assign col_addr = {col_cnt,burst_cnt};//row_addr 行地址
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrow_addr <= 0;endelse if(change_row)row_addr <= row_addr + 1;
end//换行请求
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginchange_row <= 0;endelse if(col_addr == 'd510 )change_row <= 1;elsechange_row <= 0;
end//写请求
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginwr_req <= 0;endelse if(state_wr == REQ && ref_end)wr_req <= 1;elsewr_req <= 0;
endendmodule
在top模块中加入写模块的例化,在测试文件中写好wr_trigger的波形,便继续仿真:
module sdram_top
(input clk,input rst_n,input wr_trigger,output wire sdram_clk,output wire sdram_cke,output wire sdram_cs,output wire sdram_cas,output wire sdram_ras,output wire sdram_we,output wire [1 :0 ] sdram_bank,output reg [11:0 ] sdram_addr,output wire [1 :0 ] sdram_dqm,inout [15:0 ] sdram_dq
);//状态机采用独热码定义
localparam IDLE = 5'b0_0001;
localparam HANDLE = 5'b0_0010;
localparam AUTOREFRESH = 5'b0_0100;
localparam WRITE = 5'b0_1000;
localparam READ = 5'b1_0000;
//空命令
localparam CMD_NOP = 4'b0111 ;//内部信号 //初始化
wire init_end;
wire [3:0] init_cmd;
wire [11:0] init_addr;
//状态机
reg [4:0] state;
//自动刷新
wire ref_req;
reg ref_en;
wire ref_end;
wire [3:0] ref_cmd;
wire [11:0] ref_addr;
//写模块
reg wr_en;
wire wr_req;
wire [3:0] wr_cmd;
wire [11:0] wr_addr;
wire [1:0] wr_bank_addr;
wire flag_wr_end;
wire [15:0] wr_data;//命令赋值信号
reg [3:0] sdram_cmd;
assign {sdram_cs,sdram_ras,sdram_cas,sdram_we} = sdram_cmd;//例化模块
sdram_initial u1_init
(.clk (clk),.rst_n (rst_n),.cmd_reg (init_cmd),.sdram_addr (init_addr), .init_end (init_end)
);autorefresh u2_autorefresh
(.clk (clk),.rst_n (rst_n),.init_end (init_end),.ref_en (ref_en),.ref_req (ref_req),.ref_end (ref_end),.autorefresh_cmd (ref_cmd),.sdram_addr (ref_addr) );sdram_write u3
(.clk (clk), .rst_n (rst_n), .wr_en (wr_en), .ref_req (ref_req), .wr_trigger (wr_trigger), .ref_end (ref_end), .wr_req (wr_req), .flag_wr_end (flag_wr_end),.wr_cmd (wr_cmd), .bank_addr (wr_bank_addr), .wr_addr (wr_addr), .wr_data (wr_data));//功能
//仲裁模块状态机
always@(posedge clk or negedge rst_n)beginif(!rst_n)state <= IDLE;else case(state)IDLE:if(init_end)state <= HANDLE;elsestate <= IDLE; HANDLE: if(ref_en)state <= AUTOREFRESH;else if(wr_en)state <= WRITE;else state <= HANDLE;AUTOREFRESH: if(ref_end)state <= HANDLE;elsestate <= AUTOREFRESH;WRITE: if(flag_wr_end)state <= HANDLE;elsestate <= WRITE;default: state <=IDLE;endcaseendalways@(*)begincase(state)IDLE:beginsdram_cmd <= init_cmd;sdram_addr <= init_addr;endAUTOREFRESH:beginsdram_cmd <= ref_cmd;sdram_addr <= ref_addr;endWRITE:beginsdram_cmd <= wr_cmd;sdram_addr <= wr_addr;enddefault:beginsdram_cmd <= CMD_NOP;sdram_addr <= 0;endendcaseend//刷新使能
always@(posedge clk or negedge rst_n)beginif(!rst_n)ref_en <= 0;else if(ref_req && state == HANDLE)ref_en <= 1;elseref_en <= 0;
end
//写使能
always@(posedge clk or negedge rst_n)beginif(!rst_n)wr_en <= 0;else if(wr_req && state == HANDLE && !ref_req)wr_en <= 1;elsewr_en <= 0;
endassign sdram_cke = 1'b1;
assign sdram_clk = ~clk;
assign sdram_dqm = 2'b00;
assign sdram_dq = (state == WRITE)?wr_data:{16{1'bz}};
assign sdram_bank = (state == WRITE)?wr_bank_addr:0;endmodule
至此,写模块设计完成,在整个设计过程中呢,博主写错了一个状态机的名字,一个小小的问题找了一晚上的错,最终解决,也以此告诫大家一定义要细心!仿真结果如下:





11.读模块设计与仿真测试
我们完成了写模块的设计后,读模块就简单的要命了。打开写好的写模块,将所有的写命令全部改成读命令即可。接着补充top模块中的代码,将读模块例化,补全内部信号。
而读模块与写模块不一样的地方在于时序有一个小差别:读命令后两个时钟周期才将数据读到,也就是会有2个时钟周期的延迟;
还有一个就是读和写的优先级,这里优先写(先判断写在到读)

读模块代码如下:
module sdram_read
(input clk, //输入,50M时钟input rst_n, //输入,异步复位input rd_en, //输入,读使能input ref_req, //输入,刷新请求,用来中断读input rd_trigger, //输入,读触发信号input ref_end, //输入,刷新结束,可以继续读output reg rd_req, //输出,读请求output reg flag_rd_end,//输出,top状态机跳出读状态信号 output reg [3:0] rd_cmd, //输出,读命令output wire [1:0] bank_addr, //输出,bank地址output reg [11:0] rd_addr //输出,读地址);//状态参数
localparam IDLE = 5'b0_0001;
localparam REQ = 5'b0_0010;
localparam ACTIVE = 5'b0_0100;
localparam READ = 5'b0_1000;
localparam PRE = 5'b1_0000;
//命令参数
localparam CMD_NOP = 4'b0111;
localparam CMD_PRECHARGE = 4'b0010;
localparam CMD_AUTOREFLASH = 4'b0001;
localparam CMD_ACTIVE = 4'b0011;
localparam CMD_READ = 4'b0101;//内部信号
reg flag_rd; //读区域
reg [4:0] state_rd; //读时序状态
reg flag_ac_end; //激活状态计数,计数完成跳转下一状态
reg flag_pre_end; //充电状态计数,计数完成跳转下一状态
reg change_row; //读完要换行,再激活新的一行
reg rd_data_end; //数据写完信号
reg [1:0] burst_cnt; //突发计数器,一次读多少数据的控制
reg [1:0] burst_cnt_ff; //打一拍
reg [3:0] active_cnt; //激活计数器
reg [3:0] pre_cnt; //充电计数器
reg [6:0] col_cnt; //数据计数器,满512换行,归零
reg [11:0] row_addr; //行地址
wire [8:0] col_addr; //列地址//功能
//状态机
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginstate_rd <= IDLE;endelse begincase(state_rd)IDLE:if(rd_trigger)state_rd <= REQ;elsestate_rd <= IDLE;REQ:if(rd_en)state_rd <= ACTIVE;elsestate_rd <= REQ;ACTIVE:if(flag_ac_end)state_rd <= READ;elsestate_rd <= ACTIVE;READ:if(rd_data_end)state_rd <= PRE;else if(ref_req && burst_cnt_ff == 'd3 && flag_rd )state_rd <= PRE;else if(change_row && flag_rd)state_rd <= PRE;PRE:if(ref_req && flag_rd)state_rd <= REQ;else if(flag_pre_end && flag_rd)state_rd <= ACTIVE;else if(rd_data_end)state_rd <= IDLE;default:state_rd <= IDLE;endcaseend
end//burst_cnt_ff 打一拍
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginburst_cnt_ff <= 0;endelse beginburst_cnt_ff <= burst_cnt;end
end//命令
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrd_cmd <= CMD_NOP;endelse case(state_rd)ACTIVE:if(active_cnt == 'd0)rd_cmd <= CMD_ACTIVE;elserd_cmd <= CMD_NOP;READ:if(burst_cnt == 'd0)rd_cmd <= CMD_READ;elserd_cmd <= CMD_NOP;PRE:if(pre_cnt == 'd0)rd_cmd <= CMD_PRECHARGE;elserd_cmd <= CMD_NOP;default:rd_cmd <= CMD_NOP;endcase
end//读地址
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrd_addr <= 0;endelse case(state_rd)ACTIVE:if(active_cnt == 'd0)rd_addr <= row_addr;READ:rd_addr <= {3'b000,col_addr};PRE:if(pre_cnt == 'd0)rd_addr <= {12'b0100_0000_0000};default:rd_addr <= 0; endcase
end//bank地址
assign bank_addr = 2'b00;//flag_rd
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_rd <= 0;endelse if(rd_trigger && !flag_rd)flag_rd <= 1;else if(rd_data_end)flag_rd <= 0;
end//act计数器,计数激活所需要的时间
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginactive_cnt <= 0;endelse if(state_rd == ACTIVE)active_cnt <= active_cnt + 1;elseactive_cnt <= 0;
end//刷新计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginpre_cnt <= 0;endelse if(state_rd == PRE) beginpre_cnt <= pre_cnt + 1;endelsepre_cnt <= 0;
end//col计数器,列计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begincol_cnt <= 0;endelse if(col_addr == 'd511) col_cnt <= 0;else if(burst_cnt =='d3)col_cnt <= col_cnt + 1;
end//产生flag_ac_end信号
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_ac_end <= 0;endelse if(active_cnt >= 'd3) beginflag_ac_end <= 1;endelseflag_ac_end <= 0;
end//产生flag_pre_end信号
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_pre_end <= 0;endelse if(pre_cnt >= 'd3) beginflag_pre_end <= 1;endelseflag_pre_end <= 0;
end//产生flag_rd_end信号
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginflag_rd_end <= 0;endelse if(state_rd == PRE && ref_req ) flag_rd_end <= 1;else if(state_rd == PRE && rd_data_end)flag_rd_end <= 1;elseflag_rd_end <= 0;
end//突发长度计数
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginburst_cnt <= 0;endelse if(state_rd == READ) beginburst_cnt <= burst_cnt +1;endelseburst_cnt <= 0;
end//读结束标志
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrd_data_end <= 0;endelse if(row_addr == 'd1 && col_addr == 'd511 ) beginrd_data_end <= 1;endelserd_data_end <= 0;
end//col_addr
assign col_addr = {col_cnt,burst_cnt};//row_addr
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)row_addr <= 0;else if(change_row)row_addr <= row_addr + 1;
end//换行请求
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginchange_row <= 0;endelse if(col_addr == 'd510 )change_row <= 1;elsechange_row <= 0;
end//读请求
always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrd_req <= 0;endelse if(state_rd == REQ && ref_end)rd_req <= 1;elserd_req <= 0;
endendmodule
为了方便测试,在测试文件中多引入了读触发信号,写触发在205000ns,而读触发在325000ns。
`timescale 1 ns/1 nsmodule sdram_top_tb();//输入信号
reg clk ;
reg rst_n;
reg wr_trigger;
reg rd_trigger; //输出信号
wire sdram_clk ;
wire sdram_cke ;
wire sdram_cs ;
wire sdram_cas ;
wire sdram_ras ;
wire sdram_we ;
wire [1:0] sdram_bank ;
wire [11:0] sdram_addr ;
wire [1:0] sdram_dqm ;
wire [15:0] sdram_dq ;//参数
parameter CYCLE = 20;
parameter RST_TIME = 3 ;//待测试的模块例化
sdram_top u1_sdram_top
(.clk (clk ), .rst_n (rst_n ),.wr_trigger (wr_trigger ),.rd_trigger (rd_trigger ),.sdram_clk (sdram_clk ),.sdram_cke (sdram_cke ),.sdram_cs (sdram_cs ),.sdram_cas (sdram_cas ),.sdram_ras (sdram_ras ),.sdram_we (sdram_we ),.sdram_bank (sdram_bank ),.sdram_addr (sdram_addr ),.sdram_dqm (sdram_dqm ), .sdram_dq (sdram_dq ));
//插件例化,相当于是一个sdram硬件
sdram_model_plus u2_plus
( .Dq (sdram_dq), .Addr (sdram_addr), .Ba (sdram_bank), .Clk (sdram_clk), .Cke (sdram_cke), .Cs_n (sdram_cs), .Ras_n (sdram_ras), .Cas_n (sdram_cas), .We_n (sdram_we), .Dqm (sdram_dqm),.Debug (1'b1)
);//重定义具体sdram的参数,因为插件的和实际的并不一样
defparam u2_plus.addr_bits = 12;
defparam u2_plus.data_bits = 16;
defparam u2_plus.col_bits = 9;
defparam u2_plus.mem_sizes = 2*1024*1024; //2M 一个bank// 生成本地时钟50M
initial beginclk = 0;forever#(CYCLE/2)clk=~clk;
end//产生复位信号
initial beginrst_n = 1;#2;rst_n = 0;#(CYCLE*RST_TIME);rst_n = 1;
endinitial beginwr_trigger <= 0;#205000wr_trigger <= 1;#20wr_trigger <= 0;
endinitial beginrd_trigger <= 0;#325000rd_trigger <= 1;#20rd_trigger <= 0;
endendmodule
仿真结果如图:

初始化后,由于要等此次刷新结束后才开始写,所以205000ns时并不能立即写,要延后到下一个刷新结束,激活第0行开始写数据8411。

写满一行时还没到15us一次的刷新,因此进行激活第1行操作,继续写。

230000ns又要刷新了,跳出写,进行刷新命令。

写完后,中间无命令的一段时间每15us进行一次刷新。

320000us后开始读操作,数据8411,读数正常。

读完一行开始换行,需要激活下一行。因为上述的延迟2周期的数据,出现上图情况,也是正常情况。

读数据过程中遇到刷新命令,中断读而进行刷新,完成刷新后继续读。

数据读完后没有其他命令,则进行每15us的自动刷新操作。至此仿真结束,没有问题。整个SDRAM的功能模块也写完,接下来编写完成整个控制器系统。
12.通信处理模块

在写完SDRAM模块后呢,其他的代码就简单很多啦,先写一个串口通信的通信处理模块,其作用是将rx模块发送过来的8位数据写到wfifo中,并给出读写的触发信号到sdram模块。代码及注释如下:
module ctrl_rxtx(input clk,input rst_n,input flag_uart,input [7:0] uart_data,output wire wr_trigger,output wire rd_trigger,output wire wfifo_wr_en,output wire [7:0] wfifo_wr_data);//参数localparam RX_NUM_END = 'd4;//内部信号reg [2:0] rx_num; //接收数据的个数,此数据是有rx模块给出的8位数据,一共接收五个reg [7:0] cmd_reg;//数据寄存器//功能//接收数据计数always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)beginrx_num <= 0;endelse if(flag_uart && rx_num == 'd0 && uart_data == 8'haa)rx_num <= 0;else if(flag_uart && rx_num == RX_NUM_END)rx_num <= 0;else if(flag_uart)rx_num <= rx_num + 1;end//数据寄存,命令也是从数据这出来的always @(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begincmd_reg <= 8'h00;endelse if(rx_num == 'd0 && flag_uart)cmd_reg <= uart_data;end//接受到第四个数据且为命令指示数据55时,给出写触发信号assign wr_trigger = (rx_num == RX_NUM_END && cmd_reg == 8'h55)?flag_uart:0;//没有接收新数据且为命令指示数据aa时,给出读触发信号assign rd_trigger = (rx_num == 'd0 && uart_data == 8'haa)?flag_uart:0;//写到wfifo的使能,有数据过来时,就写到wfifo中assign wfifo_wr_en = (rx_num >= 'd1)?flag_uart:0;//写入wfifo的数据assign wfifo_wr_data = uart_data;endmodule
13.顶层模块top
按照第一节的图,在每个模块之间多加了关于读写fifo的接口,在top模块中连线如下:

首先我们要生成一个fifo的ip,调用其FIFO的.v文件,具体的创建参照FPGA实例03——FIFO的IP核创建及16位输入转8位输出
具体的fifo IP文件如下,我们只需要例化出一个写fifo和一个读fifo即可,此fifo为16深度,数据8位,同步fifo(读写都是相同的时钟)
//synopsys translate_off
`timescale 1 ps / 1 ps
// synopsys translate_on
module fifo_ip (clock,data,rdreq,wrreq,empty,q);input clock;input [7:0] data;input rdreq;input wrreq;output empty;output [7:0] q;wire sub_wire0;wire [7:0] sub_wire1;wire empty = sub_wire0;wire [7:0] q = sub_wire1[7:0];scfifo scfifo_component (.clock (clock),.data (data),.rdreq (rdreq),.wrreq (wrreq),.empty (sub_wire0),.q (sub_wire1),.aclr (),.almost_empty (),.almost_full (),.full (),.sclr (),.usedw ());defparamscfifo_component.add_ram_output_register = "OFF",scfifo_component.intended_device_family = "Cyclone IV E",scfifo_component.lpm_numwords = 16,scfifo_component.lpm_showahead = "OFF",scfifo_component.lpm_type = "scfifo",scfifo_component.lpm_width = 8,scfifo_component.lpm_widthu = 4,scfifo_component.overflow_checking = "ON",scfifo_component.underflow_checking = "ON",scfifo_component.use_eab = "ON";endmodule
接着我们需要在sdram的读和写模块中加入fifo的使能和数据接口:
①由写模块给写使能到写fifo中,使得缓存在写fifo中的数据像sdram的写模块传输,注意数据要与写突发对齐,不然数据必定出错
②由读模块给读使能到读fifo中,使得读fifo读取sdram的数据,注意数据要与读突发对齐,不然数据必定出错
③在各个模块直接接入写fifo以及读fifo的接口(有点繁杂)
④将sdram模块本来的写2行操作改为写4个数据
⑤分析整个控制器的数据流:

最后的顶层代码如下:
module top(input clk,input rst_n,input rx,output tx,output wire sdram_clk,output wire sdram_cke,output wire sdram_cs,output wire sdram_cas,output wire sdram_ras,output wire sdram_we,output wire [1 :0 ] sdram_bank,output wire [11:0 ] sdram_addr,output wire [1 :0 ] sdram_dqm,inout [15:0 ] sdram_dq);//内部信号,中间连接点wire [7:0] data; //rx出来的8位数据,先给到通信模块,再给到wfifo,在给到SDRAM模块,再到rfifo,最后输出wire tx_trigger;wire sdram_wr_trigger;wire sdram_rd_trigger;wire wfifo_wr_en;wire [7:0] wfifo_wr_data;wire wfifo_rd_en;wire [7:0] wfifo_rd_data;wire [7:0] rfifo_wr_data;wire rfifo_wr_en;wire [7:0] rfifo_rd_data;wire rfifo_rd_en;wire rfifo_empty;uart_rx u1_rx(.clk (clk), .rst_n (rst_n),.rx_uart (rx),.data (data),.rx_over (tx_trigger));ctrl_rxtx u2_control(.clk (clk),.rst_n (rst_n),.flag_uart (tx_trigger),.uart_data (data),.wr_trigger (sdram_wr_trigger),.rd_trigger (sdram_rd_trigger),.wfifo_wr_en (wfifo_wr_en),.wfifo_wr_data (wfifo_wr_data));ip_fifo u3_wfifo(.clock (clk),.data (wfifo_wr_data),.rdreq (wfifo_rd_en),.wrreq (wfifo_wr_en),.empty ( ),.q (wfifo_rd_data));ip_fifo u4_rfifo(.clock (clk),.data (rfifo_wr_data),.rdreq (rfifo_rd_en),.wrreq (rfifo_wr_en),.empty (rfifo_empty),.q (rfifo_rd_data));uart_tx u5_tx(.clk (clk),.rst_n (rst_n),.tx (tx), .rfifo_empty (rfifo_empty),.rfifo_rd_data (rfifo_rd_data), .rfifo_rd_en (rfifo_rd_en));sdram_top u6_sdram(.clk (clk),.rst_n (rst_n),.wr_trigger (sdram_wr_trigger),.rd_trigger (sdram_rd_trigger),.wfifo_rd_data (wfifo_rd_data),.wfifo_rd_en (wfifo_rd_en ),.rfifo_wr_en (rfifo_wr_en ),.rfifo_wr_data (rfifo_wr_data),.sdram_clk (sdram_clk ),.sdram_cke (sdram_cke ),.sdram_cs (sdram_cs ),.sdram_cas (sdram_cas ),.sdram_ras (sdram_ras ),.sdram_we (sdram_we ),.sdram_bank (sdram_bank ),.sdram_addr (sdram_addr ),.sdram_dqm (sdram_dqm ),.sdram_dq (sdram_dq )); endmodule
14.上板!
根据我们的fpga板子的原理图,对应设置好管脚,最后进行上版实验,当然我这里是最终的结果,并不是一次就实现了功能,其中修修改改了好多细节的地方,所以说时序的设计要想的非常的全面,不然就得慢慢调试,这个过程太苦了!下面是最终的功能:发送55表示写入,再写入4个数据,aa表示读出

板级调试结束!
至此,整个项目结束。
15.后记:资源使用情况

使用的基本逻辑单元为333个,其中组合逻辑用了319个,时序逻辑用了191个,也就是说,存在既有组合逻辑也有时序逻辑的LE;
我们再来看一个表

纯组合逻辑:142个基本逻辑单元
纯时序逻辑:14个基本逻辑单元
组合时序混合:177个基本逻辑单元
由于过长的组合逻辑(多个LUT串联)会有较大的延时,而时序逻辑能够把较长的组合逻辑链分割成较短的组合逻辑链,有效地缩短关键路径和次关键路径的长度,进而提高该FPGA设计的整体时序性能,所以组合逻辑与时序逻辑的使用比例(组/时)可以作为评价一个FPGA设计时序性能的辅助参数
这里的的组/时 = 319 / 191 = 1.67
















