同步FIFO
1.序言
(1)什么是FIFO
FIFO(First in,First out),先入先出缓冲器,对数据起缓冲作用,常用在数据的跨时钟域传输。
(2)FIFO分类
FIFO由数据存取电路和外围的控制电路构成,通过外围控制电路将需要写入的数据写入存储器件,并在需要时将数据读出。
写入数据操作和读出数据操作分别在写数据时钟和读数据时钟的控制下进行。当这两个时钟为同一个时,我们称之为同步FIFO;当这两个时钟不是同一个时,我们称之为异步FIFO。(异步fifo涉及到跨时钟域数据传输问题,设计更为复杂)
2.同步FIFO
(1)接口说明(同步fifo同用一个时钟和复位)
端口名 | I/O | 描述 |
---|---|---|
端口名 | I/O | 描述 |
clk | input | 系统时钟,读写操作共用 |
rst_n | input | 系统复位,读写操作共用 |
wdata | input | 写入的数据 |
winc | input | 写入数据使能信号 |
wfull | output | 存储空间写满反馈信号 |
rdata | output | 读出的数据 |
rinc | input | 读出数据使能信号 |
wempty | output | 存储空间读空反馈信号 |
(2)代码实现
A)序言
FIFO由数据存取电路和外围的控制电路构成。数据存取电路由存储空间配置和读写操作电路实现,外围控制电路由读写使能信号产生,读写操作地址产生和写满/读空信号产生两部分实现。(本文采用高位扩展法实现写满/读空信号产生,还有一种计数器的方法本文没有说明)
B)存储空间配置
作用:根据需要存取数据的位宽和深度分配一个存储空间。用来存储写入的数据。
注意:为了适应不同的数据位宽和存储深度,我们通常将位宽和深度参数化,使其可配置。即在调用此fifo函数时,通过传参实现不同数据位宽和存储深度的fifo。需要注意的是,fifo存储深度通常给的参数是10进制,就需要根据10进制数计算所需要的二进制存储空间,使用了系统函数**$clog2**,比如当存储深度为16时,我们所需要的数据地址位宽为log216-1,所以定义数据位宽:
input [$clog2(DEPTH)-1:0] waddr;
实现:
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
C)读写操作电路
作用:当读写使能信号有效时,根据当前需要读出或写入数据的地址,进行读写数据操作。
实现:
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
D)读写使能信号产生
作用:提供给读写操作电路读写数据操作有效信号(wenc,renc),
写入数据操作时:数据能够写入需要满足两个条件,第一个是写入数据有效(即写入数据使能信号winc为高);第二个是当前存储空间还有空位置,即存储空间没有被写满(full为低)。
读出数据操作时,数据能够读出需要满足两个条件:第一个是读出数据使能信号rinc为高;第二个是当前存储空间还有没有被读出的数据,即存储空间没有被读空(empty为低)。
实现:
//读写操作有效信号
wire wenc;
wire renc;
assign wenc = (winc && (!wfull))?1'b1:1'b0;
assign renc = (rinc && (!rempty))?1'b1:1'b0;
E)读写操作地址
作用:提供读写操作数据的读写地址;
注意:a)当读写操作有效信号有效,就会按照当前地址读写数据,说明地址是提前准备好的,所以读写一个数据的同时,需要准备好下一时刻的数据地址;
时序图:(以写数据操作为例)
b)为了方便产生写满信号和读空信号,将读/写数据地址扩展了一位(高位扩展法)构成地址指针,高位扩展法在下一步详细说明;
c)所以实际的数据地址应该为地址指针除去最高位。
实现:
//读写操作地址指针
reg [$clog2(DEPTH):0] waddr_ptr;
reg [$clog2(DEPTH):0] raddr_ptr;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
waddr_ptr <= 1'b0;
else if(wenc)
waddr_ptr <= waddr_ptr +1'b1;
else
waddr_ptr <= waddr_ptr;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
raddr_ptr <= 1'b0;
else if(renc)
raddr_ptr <= raddr_ptr +1'b1;
else
raddr_ptr <= raddr_ptr;
end
//实际的地址为地址指针除去最高位;
assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];
F)写满/读空信号产生
作用:
指示当前存储空间的状态,是否被写满或读空;
说明:#############关键知识点###############
实际的读写数据地址位宽为[WIDTH-1:0],但是上一步我们给的数据地址指针(raddr_ptr,waddr_ptr)位宽为[WIDTH:0],拓展了一位(后面称为拓展位),这一位就是为了方便产生写满和读空信号。如下图所示,数据进行写入和读出的数据地址从小到大递增,当操作到最后一个地址后,又会再一次跳到0000。当读和写操作地址指针指向同一个存储单元,其实有两种可能性,第一种是所有被写入的数据均已经被读出去了,即读空状态;第二种是写数据操作已经进行了一周,即写数据地址已经超过读数据地址一周了,即写满状态。所以地址指针通过增加拓展位,来区分读空和写满状态。
读空写满信号判断依据:
读空信号:读写地址指针完全一致;
写满信号:读写地址指针,只有拓展位不同。
代码:
//读空写满信号产生模块
assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[[$clog2(DEPTH)-1:0]])))?1'b1:1'b0;
3.同步完整代码
(1)完整代码(前面说明的代码也是基于此的)
`timescale 1ns/1ns
/**********************************RAM************************************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
//存储空间配置
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
/**********************************SFIFO************************************/
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
// output reg wfull ,
// output reg rempty ,
output wfull ,
output rempty ,
output wire [WIDTH-1:0] rdata
);
//读写操作有效信号
wire wenc;
wire renc;
assign wenc = (winc && (!wfull))?1'b1:1'b0;
assign renc = (rinc && (!rempty))?1'b1:1'b0;
//读写操作地址指针
reg [$clog2(DEPTH):0] waddr_ptr;
reg [$clog2(DEPTH):0] raddr_ptr;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
waddr_ptr <= 1'b0;
else if(wenc)
waddr_ptr <= waddr_ptr +1'b1;
else
waddr_ptr <= waddr_ptr;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
raddr_ptr <= 1'b0;
else if(renc)
raddr_ptr <= raddr_ptr +1'b1;
else
raddr_ptr <= raddr_ptr;
end
// //读空写满信号产生模块
// always@(posedge clk or negedge rst_n)begin
// if(!rst_n)
// rempty <= 1'b0;
// else if(waddr_ptr == raddr_ptr)
// rempty <= 1'b1;
// else
// rempty <= 1'b0;
// end
// always@(posedge clk or negedge rst_n)begin
// if(!rst_n)
// wfull <= 1'b0;
// else if((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))
// wfull <= 1'b1;
// else
// wfull <= 1'b0;
// end
assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))?1'b1:1'b0;
//实际的数据地址
wire [3:0] waddr;
wire [3:0] raddr;
assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];
//模块例化
dual_port_RAM #(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
)
u_dual_port_RAM
(
.wclk (clk ),
.wenc (wenc ),
.waddr (waddr ),
.wdata (wdata ),
.rclk (clk ),
.renc (renc ),
.raddr (raddr ),
.rdata (rdata)
);
endmodule
(2)牛客网提交通过的代码(为提交通过做了修改)
说明:
牛客网的提交通过的代码存在bug,即存在和实际情况不符合的地方:
(1)牛客网的读空和写满信号是reg类型,且相较于正常情况延迟了一拍,当读写地址指一致的时候(读空),读出使能信号先到,会误读一位数据;
(2)牛客网的读空和写满信号是reg类型,且读空信号复位为0才能通过,但是实际情况是系统复位清空,存储空间没有数据,所以实际读空信号应该复位为1。
牛客网完整代码:
`timescale 1ns/1ns
/**********************************RAM************************************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
//存储空间配置
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
/**********************************SFIFO************************************/
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output reg wfull ,
output reg rempty ,
// output wfull ,
// output rempty ,
output wire [WIDTH-1:0] rdata
);
//读写操作有效信号
wire wenc;
wire renc;
assign wenc = (winc && (!wfull))?1'b1:1'b0;
assign renc = (rinc && (!rempty))?1'b1:1'b0;
//读写操作地址指针
reg [4:0] waddr_ptr;
reg [4:0] raddr_ptr;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
waddr_ptr <= 1'b0;
else if(wenc)
waddr_ptr <= waddr_ptr +1'b1;
else
waddr_ptr <= waddr_ptr;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
raddr_ptr <= 1'b0;
else if(renc)
raddr_ptr <= raddr_ptr +1'b1;
else
raddr_ptr <= raddr_ptr;
end
//读空写满信号产生模块
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rempty <= 1'b0;
else if(waddr_ptr == raddr_ptr)
rempty <= 1'b1;
else
rempty <= 1'b0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
wfull <= 1'b0;
else if((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))
wfull <= 1'b1;
else
wfull <= 1'b0;
end
// assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
// assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))?1'b1:1'b0;
//实际的数据地址
wire [3:0] waddr;
wire [3:0] raddr;
assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];
//模块例化
dual_port_RAM #(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
)
u_dual_port_RAM
(
.wclk (clk ),
.wenc (wenc ),
.waddr (waddr ),
.wdata (wdata ),
.rclk (clk ),
.renc (renc ),
.raddr (raddr ),
.rdata (rdata)
);
endmodule