在FPGA开发中,我们经常会遇到数据跨时钟域的情况,在不需要缓存的情况下,直接对clk1域下的数据,使用clk2打两拍以消除亚稳态,即可实现数据的跨时钟域,而如果遇到需要数据缓存的情况,一般会使用异步FIFO。
本文首先对异步FIFO的跨时钟域同步原理进行介绍,然后给出异步FIFO的verilog实现。
文章目录
- 异步FIFO原理
- 代码
- FIFO.v
- Binary2Gray.v
- Gray2Binary.v
- 测试
- testbench
- 测试结果分析
- 写入过程分析
- 队列非满(读域剩余空闲位置>=2)
- 队列将满(剩余空闲个数=1)
- 写入最后一个数据(剩余空闲位置个数=0)
- 在队列满(full=H)的情况下继续写数据--满后写
- 读取过程分析
- 队列非空(剩余可读取个数>=2)
- 队列将空(剩余可读取个数=1)
- 读取最后一个数据
- 在队列空(empty=H)的情况下继续读数据--空后读
异步FIFO原理
对于FPGA实现队列,一般使用循环队列这种数据结构进行实现,通过 head、tail 两个指针标识队列的头部元素和尾部元素。头指针 head 指向即将读取的元素,tail 指向即将写入的位置。关于循环队列本身这里不再过多解释。
在 c 语言中实现的循环队列,由于写入、读取都是顺序进行的,不存在数据跨时钟域的情况,因此其 head、tail 指针在写入、读取过程中都是共用的。而在异步 FIFO 中,由于读写跨时钟域,因此在写时钟域更新 tail 指针,而在读时钟域更新 head 指针,因此若要在写时钟域判断队列满(在读时钟域判断队列空),就要把读(写)时钟域的 head (tail) 指针同步到写(读)时钟域。为了保证数据传输的稳定性,在数据同步中一般使用格雷码(Gray Code),即通过如下过程进行指针的跨时钟域数据同步:
FIFO 常用的一些信号如下:
- 写使能信号 wr_en,写时钟域
- 满标志 full,写时钟域
- 将满标志 almost_full,写时钟域
- 写有效标志 ack,写时钟域
- 写溢出标志 overflow,写时钟域
- 读使能信号 rd_en,读时钟域
- 空标志 empty,读时钟域
- 将空标志 almost_empty,读时钟域
- 读有效标志 valid,读时钟域
- 读溢出标志 underflow,读时钟域
- 写时钟域元素计数 cnt_element_wr,写时钟域
- 读时钟域元素计数 cnt_element_rd,读时钟域
上述信号分属读/写两个时钟域,其中在写时钟域的信号,均应使用 head_wr、tail_wr 构建,与 clk_wr 保持同步;而属于读时钟域的信号,均应使用 head_rd、tail_rd 构建,与 clk_rd 保持同步。
信号含义及产生方式详见下方代码实现。
代码
FIFO.v
/*
* file : FIFO.v
* author : 今朝无言
* date : 2022-10-01
* description : 异步FIFO,进行了读时钟域、写时钟域间的数据同步(head以及tail),Gray Code
*/
module FIFO(
input rst_n, //复位信号,清空队列
input wr_clk, //写时钟
input wr_en, //写使能信号,=1时每个wr_clk上升沿将数据din加入队尾
input [Width-1:0] din, //写入数据,wr域
input rd_clk, //读时钟
input rd_en, //读使能信号,=1时每个rd_clk上升沿将队首元素输出至dout
output reg [Width-1:0] dout, //读出数据,rd域
output reg full, //写满标志,wr域
output reg empty, //读空标志,rd域
output reg almost_full, //将满标志,wr域
output reg almost_empty, //将空标志,rd域
output reg overflow, //写溢出标志,wr域
output reg underflow, //读越界标志,rd域
output reg ack, //写应答标志,表示本次成功写入,wr域
output reg valid, //读有效标志,表示本次成功读取,rd域
output reg [W_cnt-1:0] cnt_element_wr, //队列元素计数,wr域
output reg [W_cnt-1:0] cnt_element_rd //队列元素计数,rd域
);
//实现技术:循环队列
parameter Width = 8; //数据位宽
parameter Deepth = 64; //此为RAM大小,实际容量减一 为保证Gray码的循环,应保证为2的次幂
localparam W_cnt = clogb2(Deepth-1); //计数器位宽
wire [W_cnt-1:0] head_wr; //队首指针,wr_clk时钟域,由head_rd同步到写时钟域
reg [W_cnt-1:0] tail_wr = 0; //队尾指针,指向即将写入的地址,wr_clk时钟域
reg [W_cnt-1:0] head_rd = 0; //队首指针,指向即将读出的数据地址,rd_clk时钟域
wire [W_cnt-1:0] tail_rd; //队尾指针,rd_clk时钟域,由tail_wr同步到读时钟域
reg [Width-1:0] Queue [0:Deepth-1]; //RAM
//这样的RAM是分布式RAM,消耗资源会比较多,可能的话可以转换成使用IP核创建的Block RAM,那样要加控制时序,复杂些
//--------------------------------数据同步----------------------------------
//-----------------tail同步---------------------
wire [W_cnt-1:0] tail_gray_wr;
reg [W_cnt-1:0] tail_gray_rd;
reg [W_cnt-1:0] tail_gray_reg1;
// bin -> gray
Binary2Gray #(.Width(W_cnt))
Binary2Gray_tail(
.bin (tail_wr),
.gray (tail_gray_wr)
);
//打两拍,同步至rd域
always @(posedge rd_clk)begin
tail_gray_reg1 <= tail_gray_wr;
tail_gray_rd <= tail_gray_reg1;
end
// gray -> bin
Gray2Binary #(.Width(W_cnt))
Gray2Binary_tail(
.gray (tail_gray_rd),
.bin (tail_rd)
);
//-----------------head同步---------------------
wire [W_cnt-1:0] head_gray_rd;
reg [W_cnt-1:0] head_gray_wr;
reg [W_cnt-1:0] head_gray_reg1;
// bin -> gray
Binary2Gray #(.Width(W_cnt))
Binary2Gray_head(
.bin (head_rd),
.gray (head_gray_rd)
);
//打两拍,同步至wr域
always @(posedge wr_clk)begin
head_gray_reg1 <= head_gray_rd;
head_gray_wr <= head_gray_reg1;
end
// gray -> bin
Gray2Binary #(.Width(W_cnt))
Gray2Binary_head(
.gray (head_gray_wr),
.bin (head_wr)
);
//--------------------------------读写控制----------------------------------
//write
always @(posedge wr_clk or negedge rst_n) begin
if(~rst_n) begin
tail_wr <= 0;
end
else begin
if(wr_en && ~full) begin
Queue[tail_wr] <= din;
tail_wr <= (tail_wr == Deepth - 1)? 0 : tail_wr + 1'b1;
ack <= 1'b1;
overflow <= 1'b0;
end
else if(wr_en && full) begin
overflow <= 1'b1;
ack <= 1'b0;
end
else begin
overflow <= 1'b0;
ack <= 1'b0;
end
end
end
//read
always @(posedge rd_clk or negedge rst_n) begin
if(~rst_n) begin
head_rd <= 0;
end
else begin
if(rd_en && ~empty) begin
dout <= Queue[head_rd];
head_rd <= (head_rd == Deepth - 1)? 0 : head_rd + 1'b1;
valid <= 1'b1;
underflow <= 1'b0;
end
else if(rd_en && empty) begin
underflow <= 1'b1;
valid <= 1'b0;
end
else begin
underflow <= 1'b0;
valid <= 1'b0;
end
end
end
//--------------------------------flags----------------------------------
always @(*) begin
//empty标志,rd域
if(head_rd == tail_rd) begin
empty <= 1;
end
else begin
empty <= 0;
end
//full标志,wr域
if((head_wr == tail_wr + 1) || ((tail_wr == Deepth) && (head_wr == 0))) begin
full <= 1;
end
else begin
full <= 0;
end
//元素计数,wr域
if(tail_wr > head_wr) begin
cnt_element_wr <= tail_wr - head_wr;
end
else begin
cnt_element_wr <= tail_wr + Deepth - head_wr;
end
//元素计数,rd域
if(tail_rd > head_rd) begin
cnt_element_rd <= tail_rd - head_rd;
end
else begin
cnt_element_rd <= tail_rd + Deepth - head_rd;
end
//将满标志almost_full,wr域
if((head_wr == tail_wr + 2) || ((tail_wr == Deepth) && (head_wr == 1))
|| ((tail_wr == Deepth - 1) && (head_wr == 0))) begin
almost_full <= 1'b1;
end
else begin
almost_full <= full; //将满标志覆盖满标志,如果希望full时almost_full为L,将这里改成0即可
end
//将空标志almost_empty,rd域
if((head_rd + 1 == tail_rd) || ((head_rd == Deepth) && (tail_rd == 0))) begin
almost_empty <= 1'b1;
end
else begin
almost_empty <= empty; //将空标志覆盖空标志,如果希望empty时almost_empty为L,将这里改成0即可
end
end
//------------------------log2-----------------------------
function integer clogb2 (input integer depth);
begin
for (clogb2=0; depth>0; clogb2=clogb2+1)
depth = depth >> 1;
end
endfunction
endmodule
Binary2Gray.v
/*
* file : Gray2Binary.v
* author : 今朝无言
* date : 2022-10-01
*/
module Binary2Gray(
input [Width-1:0] bin,
output reg [Width-1:0] gray
);
parameter Width = 8; //二进制与格雷码位宽
// 对于二进制码 B_{n-1},B_{n-2},...B_1,B_0
// 格雷码 G_{n-1},G_{n-2},...,G_1,G_0
// 对于最高位,G_{n-1} = B_{n-1}
// 对于其他位,G_i = B_{i+1} ^ B_i,i=0,1,2,...,n-2
// 其实最高位相当于 G_{n-1} = B_n ^ B_{n-1},而 B_n=0,因此 G_{n-1} = 0 ^ B_{n-1} = B_{n-1}
integer i;
always @(bin) begin
gray[Width-1] <= bin[Width-1];
for(i=0; i<Width-1; i=i+1) begin
gray[i] <= bin[i+1] ^ bin[i];
end
end
// 下方写法相同
//assign gray = (bin >> 1) ^ bin;
endmodule
Gray2Binary.v
/*
* file : Gray2Binary.v
* author : 今朝无言
* date : 2022-10-01
*/
module Gray2Binary(
input [Width-1:0] gray,
output reg [Width-1:0] bin
);
parameter Width = 8; //二进制与格雷码位宽
// 对于二进制码 B_{n-1},B_{n-2},...B_1,B_0
// 格雷码 G_{n-1},G_{n-2},...,G_1,G_0
// 对于最高位,B_{n-1} = G_{n-1}
// 对于其他位,B_i = G_i ^ B_{i+1},i=0,1,2,...,n-2
// 最高位相当于 B_{n-1} = G_{n-1} ^ B_n,而 B_n=0,因此 B_{n-1} = G_{n-1} ^ 0 = G_{n-1}
integer i;
always @(gray) begin
bin[Width-1] = gray[Width-1]; //注意要使用阻塞赋值,因为使用到了本轮计算的高位结果
for(i=Width-2; i>=0; i=i-1) begin
bin[i] = bin[i+1] ^ gray[i];
end
end
endmodule
测试
testbench
`timescale 1ns / 1ps
//FIFO.v 测试
module FIFO_tb();
//------------------------------变量声明---------------------------------
reg wr_clk = 0;
reg rd_clk = 0;
reg [7:0] din;
reg wr_en;
reg rd_en;
wire [7:0] dout;
wire full;
wire almost_full;
wire ack;
wire overflow;
wire empty;
wire almost_empty;
wire valid;
wire underflow;
wire [6:0] cnt_element_wr;
wire [6:0] cnt_element_rd;
//-------------------------------test--------------------------------
always #6 begin
wr_clk <= ~wr_clk;
end
always #5 begin
rd_clk <= ~rd_clk;
end
initial begin
din <= 0;
wr_en <= 0;
rd_en <= 0;
#200;
wr_func(32); //写32个数据
#100;
rd_func(35); //读35个数据,测试empty、almost_empty信号
#100;
wr_func(130); //写130(超出数据容量上限127),测试full、almost_full信号
#100;
rd_func(30); //读30个数据
#100;
$stop;
end
//-----------测试写入功能---------------
task wr_func;
input [7:0] cnt; //写入 0~cnt-1 共 cnt 个数据
integer i;
begin
din <= 0;
wr_en <= 1;
wait(~wr_clk);
for(i=0; i<cnt; i=i+1) begin
wait(wr_clk);
din <= i+1;
wait(~wr_clk);
end
wr_en <= 0;
din <= 0;
end
endtask
//--------------测试读出---------------
task rd_func;
input [7:0] cnt; //读 cnt 个数据
integer i;
begin
rd_en <= 1;
wait(~rd_clk);
for(i=0; i<cnt; i=i+1) begin
wait(rd_clk);
wait(~rd_clk);
end
rd_en <= 0;
end
endtask
//-----------------------FIFO模块------------------------
FIFO #(.Width(8),.Deepth(128))
FIFO_inst(
.rst_n (1'b1),
.wr_clk (wr_clk),
.wr_en (wr_en),
.din (din),
.rd_clk (rd_clk),
.rd_en (rd_en),
.dout (dout),
.full (full),
.empty (empty),
.almost_full (almost_full),
.almost_empty (almost_empty),
.overflow (overflow),
.underflow (underflow),
.ack (ack),
.valid (valid),
.cnt_element_wr (cnt_element_wr),
.cnt_element_rd (cnt_element_rd)
);
endmodule
测试结果分析
testbench中,初始化队列长度128,因此最多可写入127个数据。以下对写入过程以及读取过程 flags 信号的变化进行测试分析。
写入过程分析
队列非满(读域剩余空闲位置>=2)
如图所示,当写域时钟 wr_clk 上升沿到达时,检测写使能信号 wr_en ,若为 H,则进行数据写入。由于此时队列非满,成功写入数据,因此同时给出写应答信号 ack=H 表示数据成功写入。写域数据个数 cnt_element_wr 也同步更新。
队列将满(剩余空闲个数=1)
如图所示,由于队列大小128,最大可装填数据个数为127,因此当写入第126个数据(125)后,队列将满,因此将满信号 almost_full 置高。由于数据成功写入,因此给出写入成功标志 ack=H,同时更新写域数据个数 cnt_element_wr 。
写入最后一个数据(剩余空闲位置个数=0)
如图所示,写入第127个数据(126)后,队列满,因此给出满信号 full=H。由于数据成功写入,因此给出写入成功标志 ack=H,同时更新写域数据个数 cnt_element_wr 。
在队列满(full=H)的情况下继续写数据–满后写
如图所示,由于此时队列已满,无法继续向 FIFO 中写入数据,因此写入失败,给出写失败标志 ack=L ,同时给出写溢出标志 overflow=H 。
读取过程分析
队列非空(剩余可读取个数>=2)
如图所示,在读域时钟 rd_clk 的上升沿,若读使能信号有效(rd_en=H),进行数据读取。由于此情况下成功读到数据,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd。
队列将空(剩余可读取个数=1)
如图所示,由于之前写入了32个数据(0~31),因此进行第31次读取后,队列中仅剩最后一个元素,此时给出将空信号 almost_empty=H。由于成功读取,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd,此时 cnt_element_rd 将等于1,表示还剩最后一个可读取元素。
读取最后一个数据
如图所示,读取最后一个元素,读取后队列空,因此给出队空信号 empty=H。由于成功读取,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd,此时 cnt_element_rd 将等于0,表示队列已无可继续读取的数据。
在队列空(empty=H)的情况下继续读数据–空后读
如图所示,由于队列已空,已无可读取数据,因此读取失败,给出读无效标志 valid=L,同时给出读溢出标志 underflow=H。