1、什么是FIFO
FIFO 是一种先进先出的数据缓存器,在逻辑设计里面用的非常多,FIFO 设计可以说是逻辑设计人员必须掌握的常识性设计。FIFO 一般用在隔离两边读写带宽不一致,或者位宽不一样的地方。 在 FPGA 设计,使用 FIFO 一般有两个方法,第一个方法是直接调用官方的 FIFO IP,另外一个方法是自己设计 FIFO 控制逻辑。当然我们学会设计FIFO,并不一定是真的需要自己造轮子,只是说作为从业人员我们要了解相关的设计方法,毕竟自己造的轮子不一定能跑不是。
FIFO 包括同步 FIFO 和异步 FIFO 两种,同步 FIFO 有一个时钟信号,读和写逻辑全部使用这一个时钟 信号,异步 FIFO 有两个时钟信号,读和写逻辑用的各种的读写时钟。 FIFO 与普通存储器 RAM 的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写 入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。 FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。
FIFO 的常见参数:
- FIFO 的宽度:即 FIFO 一次读写操作的数据位
- FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)
- 满标志:FIFO 已满或将要满时由 FIFO 的状态送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出(overflow)
- 空标志:FIFO 已空或将要空时由 FIFO 的状态送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出(underflow)
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据(同步FIFO中与写时钟一致)
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据(同步FIFO中与读时钟一致)
2、同步FIFO的设计方法
FIFO 读写指针(读写指针就是读写地址)的工作原理:
- 写指针:总是指向下一个将要被写入的单元,复位时,指向第 1 个单元(编号为 0)
- 读指针:总是指向当前要被读出的数据,复位时,指向第 1 个单元(编号为 0) FIFO 的“空”/“满”检测
FIFO 设计的关键:产生可靠的 FIFO 读写指针和生成 FIFO“空”/“满”状态标志。 当读写指针相等时,表明 FIFO 为空,这种情况发生在复位操作时,或者当读指针读出 FIFO 中最后一 个字后,追赶上了写指针时,如下图所示:
当读写指针再次相等时,表明 FIFO 为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around) 又追上了读指针,如下图:
可见读写指针可以在读写使能有效时,每时钟周期+1,而如何产生可靠的“空”/“满”信号则成了同步FIFO设计的重点。下面有两种解决方法:
2.1、计数器法
构建一个计数器,该计数器(fifo_cnt)用于指示当前 FIFO 中数据的个数:
- 复位时,该计数器为0,FIFO中的数据个数为0
- 当读写使能信号均有效时,说明又读又写,计数器不变,FIFO中的数据个数无变化
- 当写使能有效且 full=0,则 fifo_cnt +1;表示写操作且 FIFO 未满时候,FIFO 中的数据个数增加了 1
- 当读使能有效且 empty=0,则 fifo_cnt -1;表示读操作且 FIFO 未空时候,FIFO 中的数据个数减少了 1
- fifo_cnt =0 的时候,表示 FIFO 空,需要设置 empty=1;fifo_cnt = fifo的深度 的时候,表示 FIFO 现在已经满,需要设置 full=1
2.1.1、Verilog代码
解决好如上所述的几个问题后,可以很容易的贴出Verilog代码:
//计数器法实现同步FIFO
module sync_fifo_cnt
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 //FIFO深度
)
(
input clk , //系统时钟
input rst_n , //低电平有效的复位信号
input [DATA_WIDTH-1:0] data_in , //写入的数据
input rd_en , //读使能信号,高电平有效
input wr_en , //写使能信号,高电平有效
output reg [DATA_WIDTH-1:0] data_out, //输出的数据
output empty , //空标志,高电平表示当前FIFO已被写满
output full , //满标志,高电平表示当前FIFO已被读空
output reg [$clog2(DATA_DEPTH) : 0] fifo_cnt //$clog2是以2为底取对数
);
//reg define
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) begin
if (!rst_n)
rd_addr <= 0;
else if (!empty && rd_en)begin //读使能有效且非空
rd_addr <= rd_addr + 1'd1;
data_out <= fifo_buffer[rd_addr];
end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_addr <= 0;
else if (!full && wr_en)begin //写使能有效且非满
wr_addr <= wr_addr + 1'd1;
fifo_buffer[wr_addr]<=data_in;
end
end
//更新计数器
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
fifo_cnt <= 0;
else begin
case({wr_en,rd_en}) //拼接读写使能信号进行判断
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 full = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0; //满信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0; //空信号
endmodule
2.1.2、Testbench及仿真结果
接下来编写脚本对源码进行测试:
- 例化1个深度为8,位宽为8的同步FIFO
- 先对FIFO进行写操作,直到其写满,写入的数据为随机数据
- 然后对FIFO进行读操作,直到其读空
- 然后对FIFO写入4个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns //时间单位/精度
//------------<模块及端口声明>----------------------------------------
module tb_sync_fifo_cnt();
parameter DATA_WIDTH = 8 ; //FIFO位宽
parameter DATA_DEPTH = 8 ; //FIFO深度
reg clk ;
reg rst_n ;
reg [DATA_WIDTH-1:0] data_in ;
reg rd_en ;
reg wr_en ;
wire [DATA_WIDTH-1:0] data_out;
wire empty ;
wire full ;
wire [$clog2(DATA_DEPTH) : 0] fifo_cnt;
//------------<例化被测试模块>----------------------------------------
sync_fifo_cnt
#(
.DATA_WIDTH (DATA_WIDTH), //FIFO位宽
.DATA_DEPTH (DATA_DEPTH) //FIFO深度
)
sync_fifo_cnt_inst(
.clk (clk ),
.rst_n (rst_n ),
.data_in (data_in ),
.rd_en (rd_en ),
.wr_en (wr_en ),
.data_out (data_out ),
.empty (empty ),
.full (full ),
.fifo_cnt (fifo_cnt )
);
//------------<设置初始测试条件>----------------------------------------
initial begin
clk = 1'b0; //初始时钟为0
rst_n <= 1'b0; //初始复位
data_in <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
//重复8次写操作,让FIFO写满
repeat(8) begin
@(negedge clk)begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
end
end
//重复8次读操作,让FIFO读空
repeat(8) begin
@(negedge clk)begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
//重复4次写操作,写入4个随机数据
repeat(4) begin
@(negedge clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
rd_en <= 1'b0;
end
end
//持续同时对FIFO读写,写入数据为随机数据
forever begin
@(negedge clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
rd_en <= 1'b1;
end
end
end
//------------<设置时钟>----------------------------------------------
always #10 clk = ~clk; //系统时钟周期20ns
endmodule
使用modelsim进行仿真,仿真结果如下:
可以看到,仿真结果与预期效果一致(详见图注)。
2.2、高位扩展法
举例在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3'b000-3'b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4'b0000-4'b1111。假设不看最高位的话,后面3位的表示区间仍然是3'b000-3'b111,也就意味着最高位可以拿来作为指示位。
- 当最高位不同,且其他位相同,则表示读指针或者写指针多跑了一圈,而显然不会让读指针多跑一圈(多跑一圈读啥?),所以可能出现的情况只能是写指针多跑了一圈,与就意味着FIFO被写满了
- 当最高位相同,且其他位相同,则表示读指针追到了写指针或者写指针追到了读指针,而显然不会让写指针追读指针(这种情况只能是写指针超过读指针一圈),所以可能出现的情况只能是读指针追到了写指针,也就意味着FIFO被读空了
2.2.1、Verilog代码
解决好如上所述的几个问题后,可以很容易的贴出Verilog代码:
module sync_fifo_ptr
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 //FIFO深度
)
(
input clk , //系统时钟
input rst_n , //低电平有效的复位信号
input [DATA_WIDTH-1:0] data_in , //写入的数据
input rd_en , //读使能信号,高电平有效
input wr_en , //写使能信号,高电平有效
output reg [DATA_WIDTH-1:0] data_out, //输出的数据
output empty , //空标志,高电平表示当前FIFO已被写满
output full //满标志,高电平表示当前FIFO已被读空
);
//reg define
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];
reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写地址指针,位宽多一位
reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读地址指针,位宽多一位
//wire define
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针
wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针
wire wr_ptr_msb; //写地址指针地址最高位
wire rd_ptr_msb; //读地址指针地址最高位
assign {wr_ptr_msb,wr_ptr_true} = wr_ptr; //将最高位与其他位拼接
assign {rd_ptr_msb,rd_ptr_true} = rd_ptr; //将最高位与其他位拼接
//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)
rd_ptr <= 'd0;
else if (rd_en && !empty)begin //读使能有效且非空
data_out <= fifo_buffer[rd_ptr_true];
rd_ptr <= rd_ptr + 1'd1;
end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!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
//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
//当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
assign full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
endmodule
2.2.2、Testbench及仿真结果
接下来编写脚本对源码进行测试(与2.1.2一致):
- 例化1个深度为8,位宽为8的同步FIFO
- 先对FIFO进行写操作,直到其写满,写入的数据为随机数据
- 然后对FIFO进行读操作,直到其读空
- 然后对FIFO写入4个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns //时间单位/精度
//------------<模块及端口声明>----------------------------------------
module tb_sync_fifo_ptr();
parameter DATA_WIDTH = 8 ; //FIFO位宽
parameter DATA_DEPTH = 8 ; //FIFO深度
reg clk ;
reg rst_n ;
reg [DATA_WIDTH-1:0] data_in ;
reg rd_en ;
reg wr_en ;
wire [DATA_WIDTH-1:0] data_out;
wire empty ;
wire full ;
//------------<例化被测试模块>----------------------------------------
sync_fifo_ptr
#(
.DATA_WIDTH (DATA_WIDTH), //FIFO位宽
.DATA_DEPTH (DATA_DEPTH) //FIFO深度
)
sync_fifo_ptr_inst(
.clk (clk ),
.rst_n (rst_n ),
.data_in (data_in ),
.rd_en (rd_en ),
.wr_en (wr_en ),
.data_out (data_out ),
.empty (empty ),
.full (full )
);
//------------<设置初始测试条件>----------------------------------------
initial begin
clk = 1'b0; //初始时钟为0
rst_n <= 1'b0; //初始复位
data_in <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
//重复8次写操作,让FIFO写满
repeat(8) begin
@(negedge clk)begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
end
end
//重复8次读操作,让FIFO读空
repeat(8) begin
@(negedge clk)begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
//重复4次写操作,写入4个随机数据
repeat(4) begin
@(negedge clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
rd_en <= 1'b0;
end
end
//持续同时对FIFO读写,写入数据为随机数据
forever begin
@(negedge clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
rd_en <= 1'b1;
end
end
end
//------------<设置时钟>----------------------------------------------
always #10 clk = ~clk; //系统时钟周期20ns
endmodule
使用modelsim进行仿真,仿真结果如下:
可以看到,仿真结果与预期效果一致(详见图注)。
3、其他
- 了解了同步FIFO的设计方法后,再进行异步FIFO的设计就比较简单了,主要就是解决好异步FIFO中的跨时钟域比较读写指针的问题。下一篇文章再对异步FIFO的设计进行分析验证
- 创作不易,希望各位大佬多多三连支持!一家之言,如有错误还请指正!