Verilog 编写同步/异步 FIFO
同步FIFO
Verilog代码:
module Sync_fifo(
input clk_i,
(* direct_reset = "true" *) input rst_i,//同步复位
input [7:0] wr_data_i,
input wr_en_i,
input rd_en_i,
output reg [7:0] rd_data_o,
output reg fifo_full_o,
output reg fifo_empty_o
);
integer i;
reg [7:0] data_store[15:0];//reg数组存储
reg [3:0] rd_ptr,wr_ptr;//读写指针
always @(posedge clk_i)begin:write_data
if(rst_i)begin
wr_ptr <= 4'b0;
for(i=0;i<=15;i=i+1)
data_store[i] <= 8'b0;
end
else if(fifo_full_o)//fifo为满时,不进行写操作,写指针不变化
wr_ptr <= wr_ptr;
else if(wr_en_i)begin
data_store[wr_ptr] <= wr_data_i;//写数据
wr_ptr <= wr_ptr + 4'd1;
end
else begin
wr_ptr <= wr_ptr;
end
end
always @(posedge clk_i)begin:read_data
if(rst_i)begin
rd_ptr <= 4'b0;
rd_data_o <= 8'b0;
end
else if(fifo_empty_o)begin//fifo为空时,输出信号置0
rd_ptr <= rd_ptr;
rd_data_o <= 8'b0;
end
else if(rd_en_i)begin
rd_ptr <= rd_ptr + 4'd1;
rd_data_o <= data_store[rd_ptr];//读数据
end
else begin
rd_ptr <= rd_ptr;
rd_data_o <= 8'b0;
end
end
always @(posedge clk_i)begin:empty_gene
if(rst_i)
fifo_empty_o <= 1'b1;
else if(wr_en_i & rd_en_i)//既读也写,empty保持不变
fifo_empty_o <= fifo_empty_o;
else if(wr_en_i)//只写不读,empty拉低
fifo_empty_o <= 1'b0;
else if(rd_en_i)//只读不写,在读写指针相等时full拉高
fifo_empty_o <= (wr_ptr == rd_ptr+4'd1)? 1'b1:fifo_empty_o;
end
always @(posedge clk_i)begin:full_gene
if(rst_i)
fifo_full_o <= 1'b0;
else if(wr_en_i & rd_en_i)//既读也写,full保持不变
fifo_full_o <= fifo_full_o;
else if(rd_en_i)//只读不写,full拉低
fifo_full_o <= 1'b0;
else if(wr_en_i)//只写不读,在读写指针相等时full拉高
fifo_full_o <= (rd_ptr == wr_ptr+4'd1)? 1'b1:fifo_full_o;
end
endmodule
异步FIFO
参照Xilinx fifo-generator修改端口,在同步FIFO的代码基础上进行修改(当然是大改了)
读写时钟不同带来的几个问题:
1. 复位问题
1.1 同步还是异步?
在CSDN上看了一些异步FIFO的代码,大部分都采用了异步复位,这种方式对于数据路径影响小,但是会产生撤销复位信号时的亚稳态问题,同时会引入毛刺,除非使用复位同步器(异步复位,同步释放)和毛刺过滤器(过滤1ns之内的毛刺)。
`timescale 1ns / 1ps
module arstn_sync(//Async rstn signal synchronizer & burrs filter
input clk_i,
input arstn_i,
(* max_fanout = 50 *) output reg arstn_sync_o //reduce fanout of single register
);
wire #(1) arstn_dly = arstn_i;//filter burrs within 1ns
wire arstn = arstn_i | arstn_dly;
reg arstn_sync0;
always @(posedge clk_i or negedge arstn)begin
if(~arstn)begin:async_reset
arstn_sync0 <= 1'b0;
arstn_sync_o <= 1'b0;
end
else begin:sync_release
arstn_sync0 <= 1'b1;
arstn_sync_o <= arstn_sync0;
end
end
endmodule
笔者日常使用的都是同步复位,时序问题少,时钟边沿可以过滤毛刺,但是综合工具无法区分复位信号和数据信号,可以使用对应的综合命令[1]将复位信号接入触发器的复位引脚,如同步FIFO代码中的
(* direct_reset = "true" *) input rst_i,
本文设计的异步FIFO采用同步复位。
1.2 同步复位与那个时钟同步?
FIFO的本质是存储器,读操作对存储的数据不会产生影响,而写操作会。换句话说,对于FIFO,写是被动的,读是主动的,因此(外部输入的)复位信号需要和写操作时钟保持同步。
在 Xilinx IP 核 fifo-generator 的手册(pg057)中也提到 UltraScale 架构的 FPGA 只能使用同步复位引脚,复位信号必须与写时钟同步[2]。
UltraScale architecture-based built-in FIFO supports only the synchronous reset (srst). The reset must always be synchronous to write clock (clk/wr_clk). (page.136)
2. 空/满信号产生问题
2.1 亚稳态
在同步FIFO的设计中,full和empty信号的产生都需要比较读指针和写指针,而在异步条件下,两个指针分属不同的时钟域,直接进行比较的话,数据变化与时钟跳变沿过于接近会违背触发器的建立(Setup)或者保持(Hold)时间,产生亚稳态,使电路进入不稳定的状态,产生错误的输出,除非两个时钟能够保持一定的频率和相位关系,让数据变化与时钟跳变沿的时间间隔足够大。
跨时钟域传输的数据可以视为异步输入,可以使用多级同步器减少亚稳态的发生概率。
module multi_synchronizer(
input clk_i,
input async_i,
output sync_o
);
parameter STEP_NUM = 2;//multi-step synchronizer
(* shreg_extract = "no" *) reg [STEP_NUM-1:0] sync_data;//synthesis cmd: no shift register
assign sync_o = sync_data[STEP_NUM-1];
integer i;
always @(posedge clk_i)begin
sync_data[0] <= async_i;
for(i=1;i<STEP_NUM;i=i+1)
sync_data[i]<= sync_data[i-1];
end
endmodule
在大多数多时钟域的设计中,使用两级同步电路就足以避免亚稳态的出现了[3]。因此,异步FIFO使用两级同步器将读写指针及读写使能信号传输至另一时钟域下。
两级同步器会给指针数据带来两个时钟周期的延迟,由full和empty产生的逻辑来看,两个信号都可以按时拉高,而拉低会延后两个时钟周期,延迟的时间内FIFO本可以工作但被阻止了,存在一定的浪费,但是不会导致读出或者写入发生错误。
2.2 二进制指针跳变
多级同步器并不能完全避免亚稳态的出现,而使用二进制指针,寄存器在向高位进位时,会有较多的位产生跳变,进入亚稳态时将会产生大量错误,因此在读写时钟域之间传递指针数据需要减少相邻两个指针间的跳变位数,理论上最低的跳变位数为1。
格雷码完美地符合了这一要求,介绍和二进制数转换方法可见参考资料[4]。
二进制码 | 格雷码 | 十进制数 |
---|---|---|
000 | 000 | 0 |
001 | 001 | 1 |
010 | 011 | 2 |
011 | 010 | 3 |
100 | 110 | 4 |
101 | 111 | 5 |
110 | 101 | 6 |
111 | 100 | 7 |
n位二进制码 b i n i bin_i bini 转换为n位格雷码 g r a y i gray_i grayi 的公式及对应的Verilog代码[3]如下:
b i n n − 1 = g r a y n − 1 b i n i = g r a y i ⊕ b i n i + 1 bin_{n-1} = gray_{n-1}\\bin_i = gray_i \oplus bin_{i+1} binn−1=grayn−1bini=grayi⊕bini+1
module gray2bin #(parameter WIDTH = 4)(
input [WIDTH-1:0] gray_i,
output reg [WIDTH-1:0] bin_o
);
integer i;
always @(gray_i)
for(i=0;i<WIDTH;i=i+1)
bin_o[i] <= ^(gray_i>>i);
endmodule
n位格雷码
g
r
a
y
i
gray_i
grayi 转换为n位二进制码
b
i
n
i
bin_i
bini 的公式及对应的Verilog代码如下:
g
r
a
y
n
−
1
=
b
i
n
n
−
1
g
r
a
y
i
=
b
i
n
i
⊕
b
i
n
i
+
1
gray_{n-1} = bin_{n-1}\\gray_i = bin_i \oplus bin_{i+1}
grayn−1=binn−1grayi=bini⊕bini+1
module bin2gray #(parameter WIDTH = 4)(
input [WIDTH-1:0] bin_i,
output reg [WIDTH-1:0] gray_o
);
always @(bin_i)
gray_o <= (bin_i>>1)^bin_i;
endmodule
2.3 基于格雷码的空/满信号产生
2.2节已经指出跨时钟域传递读写指针的时候需要使用格雷码编码,如果使用同步FIFO中的空/满信号产生方式,则需要将格雷码转换为二进制码,比较麻烦,可以直接使用格雷码生成空/满信号。
当读写指针相等时,FIFO必定处于全空或者全满的状态,需要额外的信号区分两种状态。做一个比喻:读指针和写指针就像在同一个操场绕圈跑步,如果写指针跑得很快,把读指针“套圈”了,那么继续写就会将原有的数据覆盖,所以要产生满信号阻止进一步的写入;如果读指针跑得很快,把写指针跑过的路程都追上了,那么继续读就会读出已经读过的数据,所以要产生空信号阻止进一步的读出。
写指针应始终不落后于读指针,同时最多领先读指针一个FIFO长度。在计数器里设置一位记录“套圈”与否即可分辨出读写指针相等时对应的空/满状态。N位格雷码第2N个编码为 {1,(N-1){0}} ,当计数器计数至此的时候可将记录变量翻转,用于对比。(类似于二进制数里的进位,将此位作为最高位和N位格雷码组合,姑且称为N+1位扩展格雷码)
在写时钟域,读扩展格雷码指针经过两级同步器同步后,与写扩展格雷码指针作对比,当二者最高位不同,低N位全部相同时,full信号拉高;在读时钟域,当同步后的读写扩展格雷码最高位和低N位全部相同时,empty拉高。
进一步地,RAM也可以直接使用格雷码读写,无需进行二进制码转换,不影响正常工作。
基于输出寄存器的带进位信号格雷码计数器Verilog代码如下:
module gray_counter #(parameter WIDTH = 4)(//WIDTH >= 3
input clk_i,
input rst_i,
input cnt_en_i,
output reg carry_bit_o,
output reg [WIDTH-1:0] gray_cnt_o
);
reg [WIDTH-2:0] gray_cnt_r;//record last cnt code
always @(posedge clk_i)begin
if(rst_i)begin
gray_cnt_o[0] <= 1'b0;
gray_cnt_o[1] <= 1'b0;
gray_cnt_o[WIDTH-1] <= 1'b0;
gray_cnt_r <= {(WIDTH-1){1'b0}};
carry_bit_o <= 1'b0;
end
else if(cnt_en_i)begin
//individual assignment of the lowest 2 bits and highest bit
//when the lowest bit hold its value, flip it
gray_cnt_o[0] <= (~(gray_cnt_r[0]^gray_cnt_o[0]))? ~gray_cnt_o[0]:gray_cnt_o[0];
//when the second lowest bit doesn't hold its value and lowest bit = 1, flip it
gray_cnt_o[1] <= (~(gray_cnt_r[1]^gray_cnt_o[1]) & gray_cnt_o[0])? ~gray_cnt_o[1]:gray_cnt_o[1];
//when highest bit equals the second highest bit, and other bits are 0, flip it
gray_cnt_o[WIDTH-1] <= (gray_cnt_o[WIDTH-1]^gray_cnt_o[WIDTH-2]) & ~(|gray_cnt_o[WIDTH-3:0])? ~gray_cnt_o[WIDTH-1]:gray_cnt_o[WIDTH-1];
gray_cnt_r <= gray_cnt_o[WIDTH-2:0];
//when cnt code equals 100...00, carry_bit = 1 to indicate count end
carry_bit_o <= (gray_cnt_o[WIDTH-1]&~(|gray_cnt_o[WIDTH-2:0]))? ~carry_bit_o:carry_bit_o;
end
end
genvar i;
generate for(i=2;i<WIDTH-1;i=i+1)begin
always @(posedge clk_i)begin
if(rst_i)
gray_cnt_o[i] <= 1'b0;
else if(cnt_en_i)
if(~(gray_cnt_r[i]^gray_cnt_o[i]) & gray_cnt_o[i-1] & ~(|gray_cnt_o[i-2:0]))
//when i-th bit hold its value, and other bits constitute 100...00
//flip i-th bit, i = 2 ~ WIDTH-2
gray_cnt_o[i] <= ~gray_cnt_o[i];
end
end
endgenerate
endmodule
解决上述问题之后,搭建异步FIFO,Verilog代码如下:
module Async_fifo #(parameter ADDR_WIDTH = 4,DATA_WIDTH = 8)(
(* direct_reset = "true" *) input rst_i,
input clk_wr_i,
input wr_en_i,
input [DATA_WIDTH-1:0] wr_data_i,
input clk_rd_i,
input rd_en_i,
output reg [DATA_WIDTH-1:0] rd_data_o,
output reg fifo_full_o,
output reg fifo_empty_o
);
integer i;
reg [DATA_WIDTH-1:0] data_store[({ADDR_WIDTH{1'b1}}):0];//register group to store data
reg [ADDR_WIDTH:0] wr_ptr_r,wr_sync_ptr;//extended gray code, clk_rd_i domain
reg [ADDR_WIDTH:0] rd_ptr_r,rd_sync_ptr;//extended gray code, clk_wr_i domain
wire [ADDR_WIDTH-1:0] wr_ptr,rd_ptr;//write/read pointer
wire carry_bit_wr,carry_bit_rd;
wire full_flag,empty_flag;
gray_counter #(.WIDTH(ADDR_WIDTH)) inst_gray_counter_wr(
.clk_i(clk_wr_i),
.rst_i(rst_i),
.cnt_en_i(wr_en_i & ~full_flag),
.carry_bit_o(carry_bit_wr),
.gray_cnt_o(wr_ptr)
);//gray code counter, when cnt_en_i = 1, gary_cnt_o become to next code
gray_counter #(.WIDTH(ADDR_WIDTH)) inst_gray_counter_rd(
.clk_i(clk_rd_i),
.rst_i(rst_i),
.cnt_en_i(rd_en_i & ~empty_flag),
.carry_bit_o(carry_bit_rd),
.gray_cnt_o(rd_ptr)
);
always @(posedge clk_rd_i)begin:rd_clk_sync
wr_ptr_r <= rst_i? {(ADDR_WIDTH+1){1'b0}}:{carry_bit_wr,wr_ptr};
wr_sync_ptr <= rst_i? {(ADDR_WIDTH+1){1'b0}}:wr_ptr_r;
end//synchroniazer based on 2-step FF
always @(posedge clk_wr_i)begin:wr_clk_sync
rd_ptr_r <= rst_i? {(ADDR_WIDTH+1){1'b0}}:{carry_bit_rd,rd_ptr};
rd_sync_ptr <= rst_i? {(ADDR_WIDTH+1){1'b0}}:rd_ptr_r;
end
always @(posedge clk_wr_i)begin:wr_data
if(rst_i)
for(i=0;i<={ADDR_WIDTH{1'b1}};i=i+1)
data_store[i] <= {DATA_WIDTH{1'b0}};
else if(~full_flag & wr_en_i)//write enabled and fifo not full
data_store[wr_ptr] <= wr_data_i;
end
always @(posedge clk_rd_i)begin:rd_data
if(rst_i)
rd_data_o <= {DATA_WIDTH{1'b0}};
else if(empty_flag)//when fifo is empty, read 0
rd_data_o <= {DATA_WIDTH{1'b0}};
else if(rd_en_i)//read enabled
rd_data_o <= data_store[rd_ptr];
else
rd_data_o <= {DATA_WIDTH{1'b0}};
end
assign full_flag = (rd_sync_ptr[ADDR_WIDTH]^carry_bit_wr)&(rd_sync_ptr[ADDR_WIDTH-1:0] == wr_ptr);
//when extended bit differs and other bits equal, full = 1
always @(posedge clk_wr_i)begin:full_gene
fifo_full_o <= rst_i? 1'b0:full_flag;
end
assign empty_flag = (wr_sync_ptr[ADDR_WIDTH:0] == {carry_bit_rd,rd_ptr});
//when extended bit and other bits equal, empty = 1
always @(posedge clk_rd_i)begin:empty_gene
fifo_empty_o <= rst_i? 1'b0:empty_flag;
end
endmodule
写时钟周期为6.6ns,读时钟周期为4ns,仿真测试效果如下:


**写时钟周期为4ns,读时钟周期为6.6ns,**仿真测试效果如下:



参考文献:
[1]Xilinx: Vivado Design Suite User Guide: Synthesis (UG901)
[2]Xilinx: pg057-fifo-generator
[3](印)阿罗拉. 硬件架构的艺术:数字电路的设计方法与技术[M]. 北京:机械工业出版社, 2014.