文章目录
前言
嗨,来啦,今天学习一个,比较难的知识点吧~
本文首发于公众号“FPGA学习者”,关注公众号获取更多精彩内容。
36.异步FIFO的设计
1️⃣先从同步FIFO说起
同步FIFO,简单地说就是写入FIFO的时钟和读出FIFO的时钟相同的FIFO,FIFO的一个最重要的地方就是空和满的判断;
对于同步FIFO来说,可以设置计数器,计数器最大值为FIFO深度,最小值为0,该计数器在写使能情况下加一,读使能情况下减一,分别代表着写入一个数据和读出一个数据;计数为0,说明读空,计数为FIFO深度,则说明写满。分别产生相应的空信号和满信号;
至少思路是这么个思路,同步FIFO可参考文章:FPGA基础知识极简教程(3)从FIFO设计讲起之同步FIFO篇
关于该同步FIFO设计中,还有一个小知识点,那就是关于系统函数$clog2。
$clog2是个什么函数呢?
$clog2是Verilog–2005标准新增的一个系统函数,功能就是对输入整数实现以2为底取对数,其结果向上取整(如5.5取6)。有一点需要说明的是,目前Vivado2017以上的版本都是支持这个系统函数的(Quartus II不清楚 )。
【注】更多关于$clog2相关知识点详见CSDN博主「孤独的单刀」文章,链接:Verilog设计中如何匹配变量的位宽?
2️⃣异步FIFO介绍
异步FIFO,有两个时钟域,读FIFO时钟和写FIFO的时钟不相同;异步FIFO时钟可以用来很好的解决跨时钟域的问题,异步FIFO有哪些参数呢?如下:
FIFO | First Input First Output,即先入先出队列,本质是RAM |
---|---|
FIFO_WIDTH | FIFO的位宽,即FIFO中每个地址对应的数据的位宽 |
FIFO_DEPTH | FIFO的深度,即FIFO中能存入多少个(位宽为FIFO_WIDTH的)数据 |
full | FIFO发出的满信号,当FIFO满了之后,将full拉高 |
empty | FIFO发出的空信号,当FIFO空了之后,将empty拉高 |
wr_clk | 写时钟,写操作所遵循的时钟信号 |
rd_clk | 读时钟,读操作所遵循的时钟信号 |
wr_en | 主机发送给FIFO的写使能,一般受制于full信号,若full信号为高,主机会拉低写使能信号,防止新数据覆盖原来的数据 |
rd_en | 主机发送给FIFO的读使能,一般受制于empty信号,若empty信号为高,主机会拉低读使能信号,防止从FIFO中读出不确定的数据。 |
去百度异步FIFO,通常都会找到这么一张异步FIFO的结构图片,如下:
第一次看到这个图,不免有些迷茫,不着急,我们先不看这个图,先从头,慢慢来:
首先,这是一个FIFO的整体框图,无论同步FIFO还是异步FIFO,都适用下图:
其次,从之前那个图,大致能看出,异步FIFO的结构,主要由5大模块组成
①双端口RAM,或者说FIFO存储体,主要用于数据的存储。
②FIFO的写入控制器;
③FIFO的读出控制器;
④读指针同步器,使用写时钟域的时钟对读指针打两拍,同步到写时钟域;
⑤写指针同步器,使用读时钟域的时钟对写指针打两拍,同步到读时钟域;
如此,便可得到一张简化版的结构图:
[图源:百度@行走的BUG永动机]
各个模块的功能在于控制异步FIFO:
①在有写使能且未写满的情况下,在写时钟域进行写操作;
②在有读使能且未读空的情况下,在读时钟域进行读操作;
3️⃣空满判断
在同步FIFO中,怎么判断空满呢?
①使用计数器,对FIFO中的数据进行计数;这是上一期同步FIFO的方法。
②通过对比读写指针,比如下面这张图:
读写指针(即读写地址)相等时,FIFO有可能空(复位初始状态),也有可能满(比如第五行的情形)。那该怎么判断呢?
一个通常采用的做法是:
地址扩展一位对读写指针进行计数;
读空信号:复位的时候,读指针和写指针相等,读空信号有效;当读指针赶上写指针的时候,写指针等于读指针意味着最后一个数据被读完,此时读空信号有效。
写满信号:当写指针比读指针多一圈时,写指针等于读指针意味着写满了,此时写满信号有效。
(这个多一圈,即读指针信号比写指针信号多走了一个FIFO_DEPTH),具体怎么理解呢?假设FIFO_DEPTH = 8;则有效的地址信号为3位,读写指针都扩展1位到4位。
写指针(4位) | 读指针(4位) | 空满判断 |
---|---|---|
xxxx | xxxx(和写指针相同) | 空标志 |
1xxx | 0xxx(除最高位外相同) | 满标志 |
0xxx | 1xxx(除最高位外相同) | 不可能出现这种情况 |
为什么不会出现第三种情况呢,因为读指针最多和写指针相等,读指针不可能比写指针多走一圈,读不可能比写快。但是写可以比读多转一圈,此时也就写满了。
总结起来就是
读写指针相同时认为是读空
当最高位不同,其余位相同时认为是写满
该方法适用于二进制数之间的空满比较判断。
在异步FIFO中,怎么判断空满呢?
异步FIFO中,读指针和写指针都都分别在读时钟和写时钟下计数,这是不能直接进行比较的,想要进行比较,那就需要将指针同步到另一个时钟域。具体来说就是:
读指针在写时钟下,同步到写时钟域
写指针在读时钟下,同步到读时钟域
怎么进行方便简单的同步呢?
这就想到了曾经的异步信号打两拍来降低亚稳态概率的操作,但是这是单bit异步信号同步操作;要是能把多位的读写指针信号,转化成单bit变化的信号,不就可以通过打两拍的方式进行同步了嘛?
那,格雷码派上用场了。
读写指针可以使用格雷码进行计数,格雷码一个最大的特点就是计数加一时,所有位数只变其中一位,这就可以打两拍进行同步了。
怎么对格雷码进行比大小呢?如下图:
二进制 | 格雷码 | 二进制 | 格雷码 |
---|---|---|---|
0000 | 0000 | 1000 | 1100 |
0001 | 0001 | 1001 | 1101 |
0010 | 0011 | 1010 | 1111 |
0011 | 0010 | 1011 | 1110 |
0100 | 0110 | 1100 | 1010 |
0101 | 0111 | 1101 | 1011 |
0110 | 0101 | 1110 | 1001 |
0111 | 0100 | 1111 | 1000 |
这就看清楚了,当二进制两个数相等时,毫无疑问,格雷码也是相同的,当二进制最高位不同(即,“多了一圈”后),格雷码的最高位和次高位不相同,其余低位都相同。
加上之前判断空满的想法仍然适用,总结起来就是:
读写指针所有位都相同时认为是读空
当最高位和次高位不同,其余位相同认为是写满
4️⃣跨时钟域问题
上面提到了同步和异步FIFO中判断空满状态的方法,在异步FIFO中,读指针属于读时钟域,写指针属于写时钟域,肯定是不能直接进行对比指针的,即使都转化为格雷码也不能直接对比。
解决方法是:两级寄存器同步+格雷码,似乎上面说过了,但是在这里再总结一下:
写指针转化为格雷码,经两级触发器同步到读时钟域,与读指针的格雷码相比较,判断是否产生读空信号;
读指针转化为格雷码,经两级触发器同步到写时钟域,与写指针的格雷码相比较,判断是否产生写满信号;
现在再来看这个图,4和5这两个模块应该很清楚了。
或者说,我又找到一个图,看着也比较清楚:
[图源:B站@FPGA小学生]
设计的时候读写指针用了至少两级寄存器同步,同步会消耗至少两个时钟周期,势必会使得判断空或满有所延迟,这会不会导致设计出错呢?
总结来说异步逻辑转到同步逻辑不可避免需要额外的时钟开销,这会导致满空趋于保守,但是保守并不等于错误,这么写会稍微有性能损失,但是不会出错。
5️⃣关于格雷码的转换
①二进制转格雷码
转换规则是:1)最高位保留不变;2)格雷码其余位为二进制码对应位与其前一位的异或。
用代码表述就是
二进制码B[n:0],格雷码G[n:0];
G[n] = B[n];//最高位不变
G[n-1 : 0] = B[n-1 : 0] ^ B[n : 1];
当然了,还有一种更简单的方法:移位法。
将二进制数右移一位后,与原二进制数相异或便可得到格雷码。
assign gray_value = binary_value ^ (binary_value>>1);
②格雷码转二进制码
转换规则是:1)最高位保留不变;2)其余位格雷码与二进制对应位的前一位相异或,得到二进制的对应位。
代码表述就是:
格雷码G[n:0],二进制码B[n:0];
B[n] = G[n];
B[i - 1] = G[i - 1] ^ B[i];(1<= i <= n)
转换图形如下:
用Verilog代码实现如下:
module gray2bin #(
parameter N = 4
)(
input [N-1:0] gray,
output [N-1:0] bin
);
assign bin[N-1] = gray[N-1];
genvar i;
generate
for(i = N-2; i >= 0; i = i - 1) begin: gray_2_bin
assign bin[i] = bin[i + 1] ^ gray[i];
end
endgenerate
endmodule
不过,异步FIFO中并没有用到格雷码转二进制码。
6️⃣代码实现异步FIFO
异步FIFO代码
module asy_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 8
)(
input [WIDTH - 1 : 0] wr_data,
input wr_clk,
input wr_rstn,
input wr_en,
input rd_clk,
input rd_rstn,
input rd_en,
output fifo_full,
output fifo_empty,
output [WIDTH - 1 : 0] rd_data
);
//定义读写指针
reg [$clog2(DEPTH) : 0] wr_ptr, rd_ptr;
//定义一个宽度为WIDTH,深度为DEPTH的fifo
reg [WIDTH - 1 : 0] fifo [DEPTH - 1 : 0];
//定义读数据
reg [WIDTH - 1 : 0] rd_data;
//写操作
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
wr_ptr <= 0;
else if(wr_en && !fifo_full) begin
fifo[wr_ptr] <= wr_data;
wr_ptr <= wr_ptr + 1;
end
else
wr_ptr <= wr_ptr;
end
//读操作
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
rd_ptr <= 0;
rd_data <= 0;
end
else if(rd_en && !fifo_empty) begin
rd_data <= fifo[rd_ptr];
rd_ptr <= rd_ptr + 1;
end
else
rd_ptr <= rd_ptr;
end
//定义读写指针格雷码
wire [$clog2(DEPTH) : 0] wr_ptr_g;
wire [$clog2(DEPTH) : 0] rd_ptr_g;
//读写指针转换成格雷码
assign wr_ptr_g = wr_ptr ^ (wr_ptr >>> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >>> 1);
//定义打拍延迟格雷码
reg [$clog2(DEPTH) : 0] wr_ptr_gr, wr_ptr_grr;
reg [$clog2(DEPTH) : 0] rd_ptr_gr, rd_ptr_grr;
//写指针同步到读时钟域
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
wr_ptr_gr <= 0;
wr_ptr_grr <= 0;
end
else begin
wr_ptr_gr <= wr_ptr_g;
wr_ptr_grr <= wr_ptr_gr;
end
end
//读指针同步到写时钟域
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn) begin
rd_ptr_gr <= 0;
rd_ptr_grr <= 0;
end
else begin
rd_ptr_gr <= rd_ptr_g;
rd_ptr_grr <= rd_ptr_gr;
end
end
//声明空满信号数据类型
reg fifo_full;
reg fifo_empty;
//写满判断
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
fifo_full <= 0;
else if((wr_ptr_g[$clog2(DEPTH)] != rd_ptr_grr[$clog2(DEPTH)]) && (wr_ptr_g[$clog2(DEPTH) - 1] != rd_ptr_grr[$clog2(DEPTH) - 1]) && (wr_ptr_g[$clog2(DEPTH) - 2 : 0] == rd_ptr_grr[$clog2(DEPTH) - 2 : 0]))
fifo_full <= 1;
else
fifo_full <= 0;
end
//读空判断
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn)
fifo_empty <= 0;
else if(wr_ptr_grr[$clog2(DEPTH) : 0] == rd_ptr_g[$clog2(DEPTH) : 0])
fifo_empty <= 1;
else
fifo_empty <= 0;
end
endmodule
测试文件
module asy_fifo_tb;
parameter width = 8;
parameter depth = 8;
reg wr_clk, wr_en, wr_rstn;
reg rd_clk, rd_en, rd_rstn;
reg [width - 1 : 0] wr_data;
wire fifo_full, fifo_empty;
wire [width - 1 : 0] rd_data;
//实例化
asy_fifo myfifo (
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.wr_rstn(wr_rstn),
.rd_rstn(rd_rstn),
.wr_en(wr_en),
.rd_en(rd_en),
.wr_data(wr_data),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.fifo_full(fifo_full)
);
//时钟
initial begin
rd_clk = 0;
forever #25 rd_clk = ~rd_clk;
end
initial begin
wr_clk = 0;
forever #30 wr_clk = ~wr_clk;
end
//赋值
initial begin
wr_en = 0;
rd_en = 0;
wr_rstn = 1;
rd_rstn = 1;
#10;
wr_rstn = 0;
rd_rstn = 0;
#20;
wr_rstn = 1;
rd_rstn = 1;
@(negedge wr_clk)
wr_data = {$random}%30;
wr_en = 1;
repeat(7) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end
@(negedge wr_clk)
wr_en = 0;
@(negedge rd_clk)
rd_en = 1;
repeat(7) begin
@(negedge rd_clk);
end
@(negedge rd_clk)
rd_en = 0;
#150;
@(negedge wr_clk)
wr_en = 1;
wr_data = {$random}%30;
repeat(15) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end
@(negedge wr_clk)
wr_en = 0;
#50;
$finish;
end
endmodule
仿真截图:
整体仿真图如上图所示,我们来慢慢的分析一下;
另一边满信号拉低和空信号拉高也是同样的分析方法;
大致上,异步FIFO的设计思路以及基本功能算是实现了。
7️⃣几点思考
①其实异步FIFO的设计有比较多具体的实现形式,空满判断时可以使用组合逻辑,那么仿真结果又和上图不一样了。
②出现空或满状态后也可以强行拉低外部写入或读取使能,从FIFO自身就限制不让数据写入,当然,这个功能可以在FIFO之外用逻辑电路实现。
③此次仿真文件中,假设rd_clk比wr_clk快,通过打两拍同步的方式,慢时钟域同步到快时钟域和快时钟域同步到慢时钟域处理方式是不同的,后面有机会再详细说明。
④如果跨时钟域同步时,同步过来的是一个亚稳态的数据,那么FIFO还能否正常工作呢?
这个问题大概就是,同步过程中格雷码出现了错误情况,因为格雷码每次变化一位,那么传输过程中就算出错最多也是一位数据出错,此时相当于指针信号没有发生变化。
如果是写指针同步失效,用这个错误的写指针在读时钟域进行空判断最多是让空标志在FIFO不是真正空的时候产生,而不会产生空读的情况。
在读指针同步失效时也是一样的道理。
格雷码保证的是同步后的读写地址即使在出错的情况下依然能够保证FIFO功能的正确性。
注意: 上述1bit数据不一致的情况只限于相邻两次跳变之间,假若超过两个周期,情况就不一定了。因此,地址总线的bus skew一定不能超过一个周期,否则可能出现gray码多位数据跳变的情况。
⑤不同位宽的异步FIFO应该怎么设计呢?又该需要考虑哪些注意事项呢?
⑥关于异步FIFO的设计深度相关问题,后续有机会再做讨论。
8️⃣写在后面
当然了,我们学习异步FIFO的设计,主要目的是为了掌握其工作机制和原理,而并不是真的在实际工程中用自己设计的异步FIFO,毕竟各大厂商都有自己的FIFO IP核可以调用,他们的设计远比我们设计出来的更稳定、功能更丰富。
还有若干关于跨时钟域、亚稳态方面的问题后续再逐步讨论。
往期精彩
笔试|面试|FPGA知识点大全系列(1)
笔试|面试|FPGA知识点大全系列(2)
笔试|面试|FPGA知识点大全系列(3)
笔试|面试|FPGA知识点大全系列(4)
笔试|面试|FPGA知识点大全系列(5)
笔试|面试|FPGA知识点大全系列(6)