本节主要介绍了同步FIFO与异步FIFO的工作原理与硬件设计的关键,并列举了示例代码。
FIFO概述
FIFO(First In First Out),是一种先进先出的数据缓存器。其不可寻址,只能顺序写入、顺序读出,数据地址由内部读写地址自动加1完成(这里注意与RAM的区分:RAM支持外部给定地址读写数据),通常硬件上实现的是循环队列
FIFO的功能:FIFO是速率匹配中的一个缓冲环节,一般用于连接两个速率不同的数据通路
FIFO分类:
- 同步FIFO:
- 特点:数据写入FIFO的时钟和数据读出FIFO的时钟是同步的
- 作用:作为交互数据的缓冲,相当于一个buffer
- 异步FIFO:
- 特点:数据写入FIFO的时钟和数据读出FIFO的时钟是异步的
- 作用:实现数据在不同时钟域之间进行传递,或作为不同数据宽度的数据接口(比如写的数据是8个bit,读出的数据是16bit,或者写的是16个bit,读出的数据是8个bit)
- 同步FIFO:
FIFO深度与宽度:
- FIFO宽度:用fifo_data_size表示,也就是FIFO存储的每个数据宽度
- FIFO深度:用fifo_addr_size表示,也就是能存储多少个数据
FIFO中相关信号:
- 时钟、复位:clk,rst_n_i
- 读使能(读控制):rd_en_i
- 写使能(写控制):wr_en_i
- 满信号(满标志):full,当FIFO中的数据满(或接近满),不再能进行数据的写入
- 空信号(空标志):empty,当FIFO为空(或接近空),不再能进行数据的读出
同步FIFO
1.设计关键
“空”和“满”信号的判断:
首先来看一个正常循环FIFO的空满信号判断流程:
由上图可以总结出:
- 当读指针追上写指针时,FIFO为空
- 当写指针追上读指针时,FIFO为满
由于FIFO空和满状态时,读写指针都指向相同位置,那么如何较好的判断究竟是空还是满呢?答案是:将地址指针扩展1bit
- 在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3’b000-3’b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4’b0000-4’b1111。假设不看最高位的话,后面3位的表示区间仍然是3’b000-3’b111,也就意味着最高位可以拿来作为指示位。
- 由上图可以总结出:
- 读写指针相同,FIFO为空状态
- 读写指针最高位相反,其他位相同,FIFO为满状态
- 在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3’b000-3’b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4’b0000-4’b1111。假设不看最高位的话,后面3位的表示区间仍然是3’b000-3’b111,也就意味着最高位可以拿来作为指示位。
2.源代码
sync_fifo.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68module sync_fifo #(
parameter DEPTH = 8,
parameter WIDTH = 8
) (
input wire clk,
input wire rst_n,
input wire i_wen,
input wire [WIDTH-1:0] i_wdata,
input wire i_ren,
output reg [WIDTH-1:0] o_rdata,
output wire o_empty,
output wire o_full
);
parameter ADDR_WIDTH = $clog2(DEPTH);
reg [WIDTH-1:0] mem[DEPTH-1:0];
reg [ADDR_WIDTH:0] wptr, wptr_next;
reg [ADDR_WIDTH:0] rptr, rptr_next;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wptr <= {ADDR_WIDTH{1'b0}};
rptr <= {ADDR_WIDTH{1'b0}};
end else begin
wptr <= wptr_next;
rptr <= rptr_next;
end
end
always @(*) begin
wptr_next = wptr;
rptr_next = rptr;
if (i_wen && !o_full) begin
wptr_next = wptr + 1'b1;
end
if (i_ren && !o_empty) begin
rptr_next = rptr + 1'b1;
end
end
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < DEPTH; i = i + 1) begin
mem[i] <= 0;
end
end else if (i_wen && !o_full) begin
mem[wptr[ADDR_WIDTH-1:0]] <= i_wdata;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
o_rdata <= 0;
end else if (i_ren && !o_empty) begin
o_rdata <= mem[rptr[ADDR_WIDTH-1:0]];
end
end
assign o_empty = wptr == rptr;
assign o_full = wptr == {~rptr[ADDR_WIDTH], rptr[ADDR_WIDTH-1:0]};
endmodule
3.Testbench
sync_fifo_tb.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80//时间单位/精度
//------------<模块及端口声明>----------------------------------------
module sync_fifo_tb();
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
#(
.DEPTH (DATA_WIDTH), //FIFO位宽
.WIDTH (DATA_DEPTH) //FIFO深度
)
sync_fifo_ptr_inst(
.clk (clk ),
.rst_n (rst_n ),
.i_wdata (data_in ),
.i_ren (rd_en ),
.i_wen (wr_en ),
.o_rdata (data_out ),
.o_empty (empty ),
.o_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结果如下:
异步FIFO
1.设计关键
“空”和“满”信号的判断:
异步FIFO也可以用同步FIFO的方式去判断空满状态,但是!!!,异步FIFO存在跨时钟域的问题,它存在亚阈值问题,如果仍然使用二进制编码方式的话,会大大增加出现亚阈值状态的概率
首先,先解释为什么跨时钟域容易出现亚阈值问题(亚阈值问题就是数据不满足触发器的建立时间和保持时间,在时钟跳变沿时采集到了一个不稳定的状态,详情见Reference)
二进制的7(0111)跳转到8(1000),4位都会发生变化,所以发生亚稳态的概率就比较大,那么我们如何减少发生亚稳态的概率呢?答案是:用格雷码
格雷码是相邻数字之间只有1个bit的变化,那么就会大大减少亚稳态发生的概率
二进制与格雷码的转化:二进制码右移一位^二进制码=格雷码(^代表异或)
如何用格雷码判断空满:
读写指针相同,即认为空
当最高位和次高位不同,其余位相同认为是写满
跨时钟域的同步问题:因为读指针与写指针是受不同时钟控制的,所以它们之间的比较,需要先同步到一个时钟下(这里的同步都是指使用2个(或者3个,但此类情况不多)FF(触发器)来进行同步(俗称“打两拍”),我的理解是用这两拍的时间先锁存一个指针,然后再将这个指针与经过了2拍之后的另一个指针比较)
“写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
假设本来是这样的:
如果在2拍(写时钟下)时间内,写指针从0010写到0111,此时写指针和读指针(延时两拍后的,并非真实读指针)指向同一地址,FIFO为满
但其实在这段时间内,可能真实读指针已经读出了数据,所以此时存在“虚满”状态
“读空”的判断:需要将写指针同步到读时钟域,再与读指针判断
假设本来是这样的:
如果在2拍(读时钟下)时间内,读指针从0010读到0111再到1000,此时写指针和读指针(延时两拍后的,并非真实写指针)指向同一地址,FIFO为空
但其实在这段时间内,可能真实写指针已经写入了数据,所以此时存在“虚空”状态
2.源代码
async_fifo.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//异步FIFO
module async_fifo
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 //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已被读空
);
//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; //读地址指针,二进制
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d1; //读指针格雷码在写时钟域下同步1拍
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d2; //读指针格雷码在写时钟域下同步2拍
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d1; //写指针格雷码在读时钟域下同步1拍
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d2; //写指针格雷码在读时钟域下同步2拍
//wire define
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g; //写地址指针,格雷码
wire [$clog2(DATA_DEPTH) : 0] rd_ptr_g; //读地址指针,格雷码
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针,作为写ram的地址
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]; //写RAM地址等于写指针的低DATA_DEPTH位(去除最高位)
assign rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //读RAM地址等于读指针的低DATA_DEPTH位(去除最高位)
//写操作,更新写地址
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) 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)
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 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
//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
//当高位相反且其他位相等时,写指针超过读指针一圈,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]})? 1'b1 : 1'b0;
endmodule
3.Testbench
async_fifo_tb.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
module async_fifo_tb;
parameter DATA_WIDTH = 8 ; //FIFO位宽
parameter DATA_DEPTH = 8 ; //FIFO深度
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 ; //空标志,高电平表示当前FIFO已被写满
wire full ; //满标志,高电平表示当前FIFO已被读空
//------------<例化被测试模块>----------------------------------------
async_fifo
#(
.DATA_WIDTH (DATA_WIDTH), //FIFO位宽
.DATA_DEPTH (DATA_DEPTH) //FIFO深度
)
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; //初始时钟为0
wr_clk = 1'b0; //初始时钟为0
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 <= $random; //生成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次写操作,写入4个随机数据
repeat(4) begin
@(negedge wr_clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
end
end
//持续同时对FIFO读
@(negedge rd_clk)rd_en <= 1'b1;
//持续同时对FIFO写,写入数据为随机数据
forever begin
@(negedge wr_clk)begin
wr_en <= 1'b1;
data_in <= $random; //生成8位随机数
end
end
end
//------------<设置时钟>----------------------------------------------
always #10 rd_clk = ~rd_clk; //读时钟周期20ns
always #20 wr_clk = ~wr_clk; //写时钟周期40ns
endmodule结果如下:
整体结果:
细节展示:
Reference
- FIFO概述_哔哩哔哩_bilibili(FIFO的基本概念)
- 新新新手Icer练习(五):同步+异步FIFO的实现_哔哩哔哩_bilibili(同步与异步FIFO原理性的介绍)
- 同步FIFO的两种Verilog设计方法(计数器法、高位扩展法)_fifo同时读写计数器-CSDN博客(同步FIFO解释得很清楚)
- <FPGA>异步FIFO的Verilg实现方法_fpga fifo verilog-CSDN博客(异步FIFO也解释得比较清楚,但跨时钟域那可能会难以理解,得自己边画图或者和仿真结果理解)
- [FPGA设计的“打拍(寄存)”和“亚稳态” 到底是什么?_fpga打拍的作用-CSDN博客](https://blog.csdn.net/wuzhikaidetb/article/details/119619162#:~:text=单比特信号从慢速时钟域同步到快速时钟域需要使用打两拍的方式消除亚稳态。 第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为 70%~80%左右,第二级寄存,器可以稳定输出的概率为 99%左右,后面再多加寄存器的级数改善效果就不明显了,所以 数据进来后一般选择打两拍即可。)(亚稳态问题的解释)
- 关于异步FIFO设计,这7点你必须要搞清楚_异步fifo设计要素-CSDN博客