在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),即通过如下过程进行指针的跨时钟域数据同步:

function异步函数_数据

  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 也同步更新。

function异步函数_fpga开发_02

队列将满(剩余空闲个数=1)

  如图所示,由于队列大小128,最大可装填数据个数为127,因此当写入第126个数据(125)后,队列将满,因此将满信号 almost_full 置高。由于数据成功写入,因此给出写入成功标志 ack=H,同时更新写域数据个数 cnt_element_wr 。

function异步函数_function异步函数_03

写入最后一个数据(剩余空闲位置个数=0)

  如图所示,写入第127个数据(126)后,队列满,因此给出满信号 full=H。由于数据成功写入,因此给出写入成功标志 ack=H,同时更新写域数据个数 cnt_element_wr 。

function异步函数_fpga开发_04

在队列满(full=H)的情况下继续写数据–满后写

  如图所示,由于此时队列已满,无法继续向 FIFO 中写入数据,因此写入失败,给出写失败标志 ack=L ,同时给出写溢出标志 overflow=H 。

function异步函数_上升沿_05

读取过程分析

队列非空(剩余可读取个数>=2)

  如图所示,在读域时钟 rd_clk 的上升沿,若读使能信号有效(rd_en=H),进行数据读取。由于此情况下成功读到数据,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd。

function异步函数_上升沿_06

队列将空(剩余可读取个数=1)

  如图所示,由于之前写入了32个数据(0~31),因此进行第31次读取后,队列中仅剩最后一个元素,此时给出将空信号 almost_empty=H。由于成功读取,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd,此时 cnt_element_rd 将等于1,表示还剩最后一个可读取元素。

function异步函数_数据_07

读取最后一个数据

  如图所示,读取最后一个元素,读取后队列空,因此给出队空信号 empty=H。由于成功读取,因此给出读有效信号 valid=H,同时更新读域元素个数 cnt_element_rd,此时 cnt_element_rd 将等于0,表示队列已无可继续读取的数据。

function异步函数_上升沿_08

在队列空(empty=H)的情况下继续读数据–空后读

  如图所示,由于队列已空,已无可读取数据,因此读取失败,给出读无效标志 valid=L,同时给出读溢出标志 underflow=H。

function异步函数_function异步函数_09