SDRAM控制器设计(9)用读写FIFO优化及仿真验证
在视频图像的处理系统中,经常使用 SDRAM 作为视频图像数据的缓存。而视频图像数据流一般都是顺序产生的,同时在输出时,也只需要顺序输出即可。
对于这种连续的数据流缓存,只用上面设计的 SDRAM 控制器模块存在一些问题的,上面也有提到过,就是会在某些特殊的时刻,有些读或写会被忽略掉,而且数据的写或读不能连续对数据流进行缓存,只能间歇式的读或写 SDRAM 数据。这样是不利于连续数据流存储的,会出现数据存储或读取的遗漏问题。下面将针对这个问题对上面设计的 SDRAM 控制器模块进行优化。优化的思路很简单,就是在数据写入前和数据读出后各增加一个 FIFO 模块对数据进行暂时的缓存。这样就可以避免上面特殊时刻读或写被忽略掉而造成的数据遗漏问题,因为FIFO 同样具有数据存储作用,可以对这些数据进行短暂存储,避免了遗漏丢失。
这里的两个 FIFO 模块均是由 Quartus II 软件生成的 IP 核,读写数据位宽设置的为 16,深度设置的为 256,并且数据读出设置为 showahead 模式。图为增加 FIFO 模块后的 SDRAM 控制器的设计框图。
sdram_control 模块的写使能 Wr 和读使能 Rd 是分别通过判断写FIFO 模块和读 FIFO 模块中所存储的数据量来决定的。在写 FIFO 模块中,当存放的数据量大于一次突发写长度数据量时就将写使能 Wr 拉高;在读 FIFO 模块中,当存放的数据量小于整个 FIFO 能存放数据量的一半时就读使能 Rd 拉高。
这样就能解决上述在连续数据流读取时,数据读取存在遗漏的问题。
优化后的顶层模块代码
module sdram_control_top(
Clk,
Rst_n,
Sd_clk,
Wr_data,
Wr_en,
Wr_addr,
Wr_max_addr,
Wr_load,
Wr_clk,
Wr_full,
Wr_use,
Rd_data,
Rd_en,
Rd_addr,
Rd_max_addr,
Rd_load,
Rd_clk,
Rd_empty,
Rd_use,
Sa,
Ba,
Cs_n,
Cke,
Ras_n,
Cas_n,
We_n,
Dq,
Dqm
);
`include "../src/Sdram_Params.h"
input Clk; //系统时钟
input Rst_n; //复位信号,低电平有效
input Sd_clk; //SDRAM 时钟信号
input [`DSIZE-1:0] Wr_data; //待写入数据
input Wr_en; //写数据使能信号
input [23:0] Wr_addr; //写数据起始地址
input [23:0] Wr_max_addr; //写数据最大地址(SC_BL 的整数倍)
input Wr_load; //写 FIFO 清零信号
input Wr_clk; //写 FIFO 数据时钟信号
output Wr_full; //写 FIFO 数据满信号
output[7:0] Wr_use; //写 FIFO 数据可用数据量
output[`DSIZE-1:0] Rd_data; //读出的数据
input Rd_en; //读数据使能信号
input [23:0] Rd_addr; //读数据起始地址
input [23:0] Rd_max_addr; //读数据最大地址(SC_BL 的整数倍)
input Rd_load; //读 FIFO 清零信号
input Rd_clk; //读 FIFO 数据时钟信号
output Rd_empty; //读 FIFO 数据空信号
output[7:0] Rd_use; //读 FIFO 数据可用数据量
output[`ASIZE-1:0] Sa; //SDRAM 地址总线
output[`BSIZE-1:0] Ba; //SDRAMBank 地址
output Cs_n; //SDRAM 片选信号
output Cke; //SDRAM 时钟使能
output Ras_n; //SDRAM 行地址选
output Cas_n; //SDRAM 列地址选
output We_n; //SDRAM 写使能
inout [`DSIZE-1:0] Dq; //SDRAM 数据总线
output[`DSIZE/8-1:0]Dqm; //SDRAM 数据掩码
reg sd_wr; //写 SDRAM 使能信号
reg sd_rd; //读 SDRAM 使能信号
reg [`ASIZE-1:0] sd_caddr; //写 SDRAM 时列地址
reg [`ASIZE-1:0] sd_raddr; //写 SDRAM 时行地址
reg [`BSIZE-1:0] sd_baddr; //写 SDRAM 时 Bank 地址
wire [`DSIZE-1:0] sd_wr_data; //待写入 SDRAM 数据
wire [`DSIZE-1:0] sd_rd_data; //读出 SDRAM 的数据
wire sd_rdata_vaild; //读 SDRAM 时数据有效区
wire sd_wdata_vaild; //写 SDRAM 时数据有效区
wire sd_wdata_done; //一次写突发完成标识位
wire sd_rdata_done; //一次读突发完成标识位
wire [7:0] fifo_rduse; //写 FIFO 模块可读数据量
wire [7:0] fifo_wruse; //读 FIFO 模块已写数据量
reg [23:0] wr_sdram_addr; //写 SDRAM 的地址
reg [23:0] rd_sdram_addr; //读 SDRAM 的地址
wire sd_wr_req; //请求写数据到 SDRAM
wire sd_rd_req; //请求向 SDRAM 读数据
//SDRAM 控制器模块例化
sdram_control sdram_control(
.Clk(Clk),
.Rst_n(Rst_n),
.Wr(sd_wr),
.Rd(sd_rd),
.Caddr(sd_caddr),
.Raddr(sd_raddr),
.Baddr(sd_baddr),
.Wr_data(sd_wr_data),
.Rd_data(sd_rd_data),
.Rd_data_vaild(sd_rdata_vaild),
.Wr_data_vaild(sd_wdata_vaild),
.Wdata_done(sd_wdata_done),
.Rdata_done(sd_rdata_done),
.Sa(Sa),
.Ba(Ba),
.Cs_n(Cs_n),
.Cke(Cke),
.Ras_n(Ras_n),
.Cas_n(Cas_n),
.We_n(We_n),
.Dq(Dq),
.Dqm(Dqm)
);
//写 FIFO 模块例化
fifo_wr sd_wr_fifo(
.aclr(Wr_load),
.data(Wr_data),
.rdclk(Clk),
.rdreq(sd_wdata_vaild),
.wrclk(Wr_clk),
.wrreq(Wr_en),
.q(sd_wr_data),
.rdempty(),
.rdusedw(fifo_rduse),
.wrfull(Wr_full),
.wrusedw(Wr_use)
);
//读 FIFO 模块例化
fifo_rd sd_rd_fifo(
.aclr(Rd_load),
.data(sd_rd_data),
.rdclk(Rd_clk),
.rdreq(Rd_en),
.wrclk(Sd_clk),
.wrreq(sd_rdata_vaild),
.q(Rd_data),
.rdempty(Rd_empty),
.rdusedw(Rd_use),
.wrfull(),
.wrusedw(fifo_wruse)
);
//写 SDRAM 数据的地址,数据写完一次增加一次突发长度
aaa@qq.com(posedge Clk or negedge Rst_n)
begin
if(!Rst_n)
wr_sdram_addr <= Wr_addr;
else if(Wr_load == 1’b1)
wr_sdram_addr <= Wr_addr;
else if(sd_wdata_done)begin
if(wr_sdram_addr == Wr_max_addr-SC_BL)
wr_sdram_addr <= Wr_addr;
else
wr_sdram_addr <= wr_sdram_addr + SC_BL;
end
else
wr_sdram_addr <= wr_sdram_addr;
end
//读 SDRAM 数据的地址,数据读完一次增加一次突发长度
aaa@qq.com(posedge Clk or negedge Rst_n)
begin
if(!Rst_n)
rd_sdram_addr <= Rd_addr;
else if(Rd_load == 1'b1)
rd_sdram_addr <= Rd_addr;
else if(sd_rdata_done)begin
if(rd_sdram_addr == Rd_max_addr-SC_BL)
rd_sdram_addr <= Rd_addr;
else
rd_sdram_addr <= rd_sdram_addr + SC_BL;
end
else
rd_sdram_addr <= rd_sdram_addr;
end
//写 SDRAM 请求信号
assign sd_wr_req = (fifo_rduse >= SC_BL)?1'b1:1'b0;
//读 SDRAM 请求信号
assign sd_rd_req = (!Rd_load)&&(fifo_wruse[7]==1'b0)?1'b1:1'b0;
//写 SDRAM 使能信号
aaa@qq.com(posedge Clk or negedge Rst_n)
begin
if(!Rst_n)
sd_wr <= 1'b0;
else if(sd_wr_req)
sd_wr <= 1'b1;
else
sd_wr <= 1'b0;
end
//读 SDRAM 使能信号
aaa@qq.com(posedge Clk or negedge Rst_n)
begin
if(!Rst_n)
sd_rd <= 1'b0;
else if(sd_rd_req)
sd_rd <= 1'b1;
else
sd_rd <= 1'b0;
end
//SDRAM 的列地址
aaa@qq.com(*)
begin
if(!Rst_n)
sd_caddr <= 9'd0;
else if(sd_wr_req)
sd_caddr <= wr_sdram_addr[8:0];
else if(sd_rd_req)
sd_caddr <= rd_sdram_addr[8:0];
else
sd_caddr <= sd_caddr;
end
//SDRAM 的行地址
aaa@qq.com(*)
begin
if(!Rst_n)
sd_raddr <= 13'd0;
else if(sd_wr_req)
sd_raddr <= wr_sdram_addr[21:9];
else if(sd_rd_req)
sd_raddr <= rd_sdram_addr[21:9];
else
sd_raddr <= sd_raddr;
end
//SDRAM 的 bank 地址
aaa@qq.com(*)
begin
if(!Rst_n)
sd_baddr <= 2'd0;
else if(sd_wr_req)
sd_baddr <= wr_sdram_addr[23:22];
else if(sd_rd_req)
sd_baddr <= rd_sdram_addr[23:22];
else
sd_baddr <= sd_baddr;
end
endmodule
优化后的SDRAM仿真验证
优化后的 SDRAM 控制器设计完成后,接下来就是对该控制器进行仿真验证,这里同样采用镁光官网提供的 SDRAM 模型进行仿真,仿真过程主要是向SDRAM 写入 1000 个数据,在写的过程以及写停止写后的一段时间去读 SDRAM的数据,编写的 testbench 文件代码如下
`timescale 1ns/1ns
`define CLK100_PERIOD 10
`define WCLK_PERIOD 40
`define RCLK_PERIOD 40
module sdram_control_top_tb;
`include "../src/Sdram_Params.h"
reg Clk;
reg Rst_n;
reg [`DSIZE-1:0] Wr_data;
reg Wr_en;
reg Wr_load;
reg Wr_clk;
wire[`DSIZE-1:0] Rd_data;
reg Rd_en;
reg Rd_load;
reg Rd_clk;
wire[`ASIZE-1:0] Sa;
wire[`BSIZE-1:0] Ba;
wire Cs_n;
wire Cke;
wire Ras_n;
wire Cas_n;
wire We_n;
wire[`DSIZE-1:0] Dq;
wire[`DSIZE/8-1:0]Dqm;
wire sdram_clk;
//SDRAM 时钟信号
assign sdram_clk = ~Clk;
sdram_control_top sdram_control_top(
.Clk(Clk),
.Rst_n(Rst_n),
.Sd_clk(sdram_clk),
.Wr_data(Wr_data),
.Wr_en(Wr_en),
.Wr_addr(0),
.Wr_max_addr(1000),
.Wr_load(Wr_load),
.Wr_clk(Wr_clk),
.Wr_full(),
.Wr_use(),
.Rd_data(Rd_data),
.Rd_en(Rd_en),
.Rd_addr(0),
.Rd_max_addr(1000),
.Rd_load(Rd_load),
.Rd_clk(Rd_clk),
.Rd_empty(),
.Rd_use(),
.Sa(Sa),
.Ba(Ba),
.Cs_n(Cs_n),
.Cke(Cke),
.Ras_n(Ras_n),
.Cas_n(Cas_n),
.We_n(We_n),
.Dq(Dq),
.Dqm(Dqm)
);
//SDRAM 模型例化
sdr sdram_model(
.Dq(Dq),
.Addr(Sa),
.Ba(Ba),
.Clk(sdram_clk),
.Cke(Cke),
.Cs_n(Cs_n),
.Ras_n(Ras_n),
.Cas_n(Cas_n),
.We_n(We_n),
.Dqm(Dqm)
);
//SDRAM 控制器时钟
initial Clk = 1'b1;
always #(`CLK100_PERIOD/2) Clk = ~Clk;
//写数据到 SDRAM 时钟
initial Wr_clk = 1'b1;
always #(`WCLK_PERIOD/2) Wr_clk = ~Wr_clk;
//读数据到 SDRAM 时钟
initial Rd_clk = 1'b1;
always #(`RCLK_PERIOD/2) Rd_clk = ~Rd_clk;
initial
begin
Rst_n = 0;
Wr_load = 1;
Rd_load = 1;
Wr_data = 0;
Wr_en = 0;
Rd_en = 0;
#(`CLK100_PERIOD*200+1)
Rst_n = 1;
Wr_load = 0;
Rd_load = 0;
@(posedge sdram_control_top.sdram_control.init_done)
#2000;
//读写数据
Wr_en = 1;
Rd_en = 1;
repeat(1000)
begin
#(`RCLK_PERIOD);
Wr_data = Wr_data + 1;
end
#(`CLK100_PERIOD*2)
Wr_en = 1'b0; //关闭写使能
#5000;
Rd_en = 1'b0; //关闭读使能
#5000;
$stop;
end
endmodule
写数据过程中,数据按照我们设计的方式在进行写。
在读过程中,读者应该能发现,在读过程的前半部分,读出的数据是不连续的,并且有些是不定态。
这个也是在我们设计的预期之内的效果,之所以出现这个,原因是我们sdram_control 模块的读使能信号是根据读 FIFO 模块中已经存储的数据量来产生的,产生的条件是只要已经存储的数据量低这个 FIFO 模块所能存储数据量的一半时,就产生读使能信号,这样就造成前期在 SDRAM 中没有数据时(即不确定数),读 FIFO 模块也会向 sdram_control 模块发出读使能信号读取数据,出现前面的一些未知的数据。当读 SDRAM 的地址重新回到读起始地址时,读数据输出就正常了。
这个问题是可以解决的,使用者可以根据具体的使用情景对读数据使能进行控制并配合读 FIFO 模块中的清零信号 aclr 去避开这个问题。
下一部分 串口传图帧缓存设计应用中,设计时为避免这个问题,先等待SDRAM 中有一定数量的数据后再去进行读取,并且在读取数据显示在 TFT 屏时,在每一帧数据起始时都将读 FIFO 进行一次清空,保证待显示的图片是与 TFT屏显示是同步的。