module SCFIFO1
//定义FIFO位宽和深度
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 , //FIFO深度
parameter full_almost = 'd14 , //将满的位置
parameter empty_almost = 'd2 //将空的位置
)
(
input [DATA_WIDTH-1:0] data,
input clk,
input rst_n,
input wrreq,
input rdreq,
output empty, //空标志,高电平表示当前FIFO读空
output reg almost_empty,
output full , //满标志,高电平表示当前FIFO写满
output reg almost_full,
output [$clog2(DATA_DEPTH) :0]usedw, //$clog2是以2为底取对数
output reg [DATA_WIDTH-1:0] q
);
reg [$clog2(DATA_DEPTH) :0] fifo_cnt; //fifo计数器,对fifo内的数据进行计数
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0]; //用二维数组实现RAM
reg [$clog2(DATA_DEPTH) - 1 : 0] wr_addr; //写地址
reg [$clog2(DATA_DEPTH) - 1 : 0] rd_addr; //读地址
//写操作:计算写地址
always @ (posedge clk or negedge rst_n)
if(!rst_n)
wr_addr <= 0;
else if(wrreq && !full)begin
wr_addr <= wr_addr + 1'b1;
fifo_buffer[wr_addr]<=data;
end
//读操作:计算读地址
always @ (posedge clk or negedge rst_n)
if(!rst_n)
rd_addr <= 0;
else if(rdreq && !empty)begin //读使能且未读空
rd_addr <= rd_addr + 1'b1;
q <=fifo_buffer[rd_addr];
end
//usedw,计数器来计算fifo中的数据个数
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
fifo_cnt <= 0;
else begin
case({wrreq,rdreq}) //拼接读写使能信号进行判断
2'b00:fifo_cnt <= fifo_cnt; //不读不写
2'b01: //只读不写
if(fifo_cnt != 0) //fifo没有被读空
fifo_cnt <= fifo_cnt - 1'b1; //fifo个数-1
2'b10: //只写不读
if(fifo_cnt != DATA_DEPTH) //fifo没有被写满
fifo_cnt <= fifo_cnt + 1'b1; //fifo个数+1
2'b11:fifo_cnt <= fifo_cnt; //读写同时
default:;
endcase
end
end
assign usedw = fifo_cnt;
//依据计数器状态更新指示信号
//还可设置将满和将空的位置,来判断,从而得到almost_full和almost_emputy信号
assign full = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0; //空信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0; //满信号
//将满
always @ (posedge clk or negedge rst_n)
if (!rst_n)
almost_full <= 0;
else if(wrreq && fifo_cnt == full_almost-1) //计数0开始,因此-1
almost_full <= 1;
else
almost_full <= 0;
//将空
always @ (posedge clk or negedge rst_n)
if (!rst_n)
almost_empty <= 0;
else if(rdreq && fifo_cnt == empty_almost+1)//计数0开始,因此+1
almost_empty <= 1;
else
almost_empty <= 0;
endmodule
`timescale 1ns/1ps
module fifo_tb(); //对同步fifo仿真
parameter DATA_WIDTH = 'd8 ; //FIFO位宽
parameter DATA_DEPTH = 'd16 ; //FIFO深度
reg [DATA_WIDTH-1:0] data;
reg clk;
reg rst_n;
reg wrreq;
reg rdreq;
wire empty;
wire full;
wire almost_full;
wire almost_empty;
wire [DATA_WIDTH-1:0] q;
wire [$clog2(DATA_DEPTH) :0]usedw;
SCFIFO1
#(
.DATA_WIDTH (DATA_WIDTH),
.DATA_DEPTH (DATA_DEPTH)
)
u1(
.data(data),
.clk(clk),
.rst_n(rst_n),
.wrreq(wrreq),
.rdreq(rdreq),
.almost_empty(almost_empty),
.almost_full(almost_full),
.empty(empty),
.full(full),
.q(q),
.usedw(usedw)
);
//产生激励
initial begin
clk = 0;
rst_n = 0;
data = 0;
rdreq = 0;
wrreq = 0;
#10;
//将FIFO写满
repeat(16)begin
rst_n = 1;
wrreq = 1;
#20;
data = data + 1;
end
wrreq = 0;
repeat(16)begin
rdreq = 1;
#20;
data = data + 1;
end
rdreq = 0;
#500;
$stop;
end
always #10 clk = ~clk;
endmodule
先wrreq=1,写入16个数据,将fifo写满,后rdreq = 1 ,将16个数据读出。
波形如下:
红色框中是定义的四个参数,FIFO位宽,深度以及将满和将空的位置。
分析写相关信号:
当wrreq = 1开始写数据,因此usedw开始+1,表示已用fifo的深度,当使用了14个深度的时候,是将满的位置,因此almost_full=1。同时当usedw=16,说明用了16深度,fifo写满,full=1。
分析读相关信号:
当rdreq = 1读数据,因此usedw开始从16不断-1,当usedw=2,表示将空的位置,因此almost_emoty=1。同时当usedw=0,说明fifo读空,empty=1。
可看到最终q的输出数据是0-15。实现了数据的先入先出。
第一节介绍了同步fifo的设计方法,但同步fifo读写采用同一时钟,而异步fifo读写时钟不同,下面对其进行分析。参考
异步fifo设计的重点也是写满和读空的判断,参考大佬的博文,得到了如下的判断方式:
“写满”的判断: 将读指针同步到写时钟域,再与写指针判断
.
“读空”的判断:将写指针同步到读时钟域,再与读指针判断
如下是假读空:
左图:读指针没有超过写指针,说明没有读空。
右图:写指针同步到读时钟域需要T时间,经过T时间后,虽然在读时钟域中读写指针相同(读空),但左图才是实际的情况,因此这种情况是假读空。
如下是假写满:
左图:写指针没有超过读指针一圈,说明没有写满
右图:读指针同步到写时钟域需要T时间,经过T时间后,虽然在写时钟域中写指针超过了读指针一圈(写满),但左图才是实际的情况,因此这种情况是假写满。
1、降低亚稳态
跨时钟域中可能出现亚稳态,最终导致fifo功能错误,由于格雷码每次只有一位发生变化,因此我们引入格雷码来降低亚稳态现象的出现。
2、格雷码判断空满
将格雷码进行同步,同步后的格雷码转换成二进制后比较。
1、分别构造读、写时钟域下的读、写指针,指针位数需拓展一位(最高位作为指示位,判断写指针是否超过读指针一圈)。
分别将读、写指针从二进制码转换成格雷码
二进制码转格雷码:
assign wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
2、将格雷码形式的读指针同步到写时钟域,将格雷码形式的写指针同步到读时钟域
//将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n)begin
rd_ptr_g_d1 <= 0; //寄存1拍
rd_ptr_g_d2 <= 0; //寄存2拍
end
else begin
rd_ptr_g_d1 <= rd_ptr_g; //寄存1拍
rd_ptr_g_d2 <= rd_ptr_g_d1; //寄存2拍
end
end
//将写指针的格雷码同步到读时钟域,来判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n)begin
wr_ptr_g_d1 <= 0; //寄存1拍
wr_ptr_g_d2 <= 0; //寄存2拍
end
else begin
wr_ptr_g_d1 <= wr_ptr_g; //寄存1拍
wr_ptr_g_d2 <= wr_ptr_g_d1; //寄存2拍
end
end
3、在写时钟域判断“写满”:格雷码形式的读写指针高2位相反,其余位相等——写满
在读时钟域判断“读空”,格雷码形式的读写指针高2位相等,其余位也相等——读空
//写满
assign full = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;
//读空
assign empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
大佬写的都很清晰,可以学习一下,完整代码。
//异步FIFO
module DCFIFO1
#(
parameter DATA_WIDTH = 'd8, //FIFO位宽
parameter DATA_DEPTH = 'd8 //FIFO深度
)
(
//写有关信号
input wr_clk , //写时钟
input wr_rst_n , //低电平有效的写复位信号
input wr_en , //写使能信号,高电平有效
input [DATA_WIDTH-1:0] data_in , //写入的数据
//读有关信号
input rd_clk , //读时钟
input rd_rst_n , //低电平有效的读复位信号
input rd_en , //读使能信号,高电平有效
output reg [DATA_WIDTH-1:0] data_out , //输出的数据
//空满标志
output empty , //空标志,高电平表示当前FIFO已被写满
output full //满标志,高电平表示当前FIFO已被读空
);
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0]; //深度为8,位宽为8的fifo
//定义读写指针,且指针位数拓展一位,用来作为空满指示位
reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写指针,二进制(5位)
reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读指针,二进制
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g; //写指针,格雷码
wire [$clog2(DATA_DEPTH) : 0] rd_ptr_g; //读指针,格雷码
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d1; //写指针格雷码在读时钟域下打1拍
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d2; //写指针格雷码在读时钟域下打2拍
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d1; //读指针格雷码在写时钟域下打1拍
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d2; //读指针格雷码在写时钟域下打2拍
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写指针,作为写ram的地址(4位)
wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读指针,作为读ram的地址
//地址指针从二进制转换成格雷码
assign wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
//读写RAM地址赋值
assign wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //拓展一位的写指针,去除最高位
assign rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //拓展一位的读指针,去除最高位
//将写指针的格雷码同步到读时钟域,与格雷码读指针判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n)begin
wr_ptr_g_d1 <= 0; //打1拍
wr_ptr_g_d2 <= 0; //打2拍
end
else begin
wr_ptr_g_d1 <= wr_ptr_g; //打1拍
wr_ptr_g_d2 <= wr_ptr_g_d1; //打2拍
end
end
//写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n)
wr_ptr <= 0;
else if (!full && wr_en)begin //写使能有效且非满
wr_ptr <= wr_ptr + 1'd1;
fifo_buffer[wr_ptr_true] <= data_in;
end
end
//将读指针的格雷码同步到写时钟域,与格雷码写指针判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n)
if(!wr_rst_n)begin
rd_ptr_g_d1 <= 0;
rd_ptr_g_d2 <= 0;
end
else begin
rd_ptr_g_d1 <= rd_ptr_g;
rd_ptr_g_d2 <= rd_ptr_g_d1;
end
//读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n)
if(!rd_rst_n)
rd_ptr <= 'd0;
else if(rd_en && !empty) begin //读使能有效且非空
rd_ptr <= rd_ptr + 1'd1;
data_out <= fifo_buffer[rd_ptr_true];
end
//当高2位相等,其余位也相等,读空
//同步后的格雷码写指针 == 格雷码读指针,FIFO被读空
assign empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
//当高2位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满
//格雷码写指针 == 同步后的读指针格雷码高两位取反,再拼接上余下位
assign full = (wr_ptr_g == {~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1]),rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]});
endmodule
tb文件:
`timescale 1ns/1ns //时间单位/精度
module fifo_tb();
parameter DATA_WIDTH = 8 ; //位宽
parameter DATA_DEPTH = 8 ; //深度
reg wr_clk ;
reg wr_rst_n ;
reg wr_en ;
reg [DATA_WIDTH-1:0] data_in ;
reg rd_clk ;
reg rd_rst_n ;
reg rd_en ;
wire [DATA_WIDTH-1:0] data_out ;
wire empty ; //空标志,高电平表示被写满
wire full ; //满标志,高电平表示被读空
DCFIFO1
#(
.DATA_WIDTH (DATA_WIDTH),
.DATA_DEPTH (DATA_DEPTH)
)
async_fifo_inst(
.wr_clk (wr_clk ),
.wr_rst_n (wr_rst_n ),
.wr_en (wr_en ),
.data_in (data_in ),
.rd_clk (rd_clk ),
.rd_rst_n (rd_rst_n ),
.rd_en (rd_en ),
.data_out (data_out ),
.empty (empty ),
.full (full )
);
//初始化
initial begin
rd_clk = 1'b0;
wr_clk = 1'b0;
wr_rst_n <= 1'b0;
rd_rst_n <= 1'b0;
wr_en <= 1'b0;
rd_en <= 1'b0;
data_in <= 'd0;
#5
wr_rst_n <= 1'b1;
rd_rst_n <= 1'b1;
//写8次,让FIFO写满
repeat(8) begin
@(negedge wr_clk)begin
wr_en <= 1'b1;
data_in <= data_in +1 ; //8个数
end
end
//拉低写使能
@(negedge wr_clk) wr_en <= 1'b0;
//读8次,将FIFO读空
repeat(8) begin
@(negedge rd_clk) rd_en <= 1'd1;
end
//拉低读使能
@(negedge rd_clk) rd_en <= 1'd0;
//仅写4个数据,此时不读
repeat(4) begin
@(negedge wr_clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
end
end
//将读使能拉高
@(negedge rd_clk) rd_en <= 1'b1;
//同时对FIFO写数据,此时实现同时读写
forever begin
@(negedge wr_clk)begin
wr_en <= 1'b1;
data_in <= data_in +1 ;
end
end
end
always #10 rd_clk = ~rd_clk; //读时钟周期20ns
always #20 wr_clk = ~wr_clk; //写时钟周期40ns
endmodule
波形: