数字IC笔面常考,跨时钟域神器。——异步FIFO(简介及手撕代码)

异步FIFO

  • 写在前面的话
  • 异步FIFO相关知识点
    • FIFO简介
    • FIFO结构
      • 应用场景(来源小梅哥 《FPGA 系统设计与验证实战指南》 章节4.4)
      • 相关参数
      • 异步FIFO 内部组成
  • 异步FIFO的Verilog代码(强烈建议手敲,不要复制粘贴!)
    • 顶层模块
    • 双端口RAM
    • 写满信号判断模块
    • 读空信号判断模块
    • 信号同步模块
    • testbench
    • 波形截图
  • 总结

写在前面的话

掌握基本的数字模块是数字IC工程师的基本要求,最近几年在笔试和面试的时候会遇到要求手撕代码,一方面是考察面试者有没有良好的coding style, 重要的则是考察面试者对常用模块的了解程度。面对这种问题,没有比较好的解决方法,只能是多看、多写,时常复习复习。之所以要把异步FIFO放到分享的第一篇技术博客,是因为本人在学习过程中,从项目中简单了解异步FIFO的IP核、到学习空满信号判断再到照着资料手敲代码,最后到面试中针对异步FIFO有问必答,一路走过来,越发了解到异步FIFO的重要性。而在和经验丰富的老工程师交谈的时候,发现他们那个年代是需要手撕异步FIFO的,当时真的是人人都会,反观现在的面试者,好多人只是了解个IP核就无了。

异步FIFO相关知识点

FIFO简介

FIFO(First In First Out),先进先出。 ,对数据的存储具有先进先出的特性,FPGA 或者 ASIC 中常使用的数据缓存器,常被用于数据的缓存或者高速异步数据的交互。

注意点:
(1)它与普通存储器的区别是没有外部读写地址线,使用相对简单。
(2)缺点是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能读取指定位置的数据。
(3)有同步和异步FIFO,同步指的是读写时钟是同一个时钟,注意这里时钟的概念,是同一个,不是同频时钟。 异步指的是读写时钟不同
(4)同步FIFO一般做数据缓冲,主要作用就是buffer。
(5)异步FIFO主要作用就是跨时钟域处理,除此之外可以实现两个不同数据宽度的数据连接。

FIFO结构

FIFO从结构来区分,有两类。分别是:单时钟 FIFO(SCFIFO)双时钟 FIFO (DCFIFO),其中双时钟FIFO又可以分为 普通双时钟(DCFIFO)混合宽度双时钟 FIFO (DCFIFO_MIXED_WIDTHS)

单时钟FIFO和双时钟FIFO的输入输出信号如图所示:(来源于Intel FPGA FIFO IP User Guide)
(1)单时钟FIFO所有输入输出信号都是在Clock的上升沿进行的,均同步于Clock信号。
(2)双时钟FIFO结构中,读写端口分别有独立的时钟,所有写相关信号同步于写时钟 wrclk ,所有读相关信号同步于读时钟 rdclk
数字IC笔面常考,跨时钟域神器。——异步FIFO(简介及手撕代码)_第1张图片

应用场景(来源小梅哥 《FPGA 系统设计与验证实战指南》 章节4.4)

(1)同步FIFO
单时钟FIFO常用于片内数据交互,例如,FPGA读取传感器数据,先写入FIFO,之后再通过串口发送出去。传感器的读取和串口数据发送都可以基于同一个时钟,因此使用单时钟FIFO做同步。

(2)异步FIFO
异步FIFO典型应用就是异步数据收发,例如高速ADC采集,采集后数据通过千兆以太网发送回PC机器。
数字IC笔面常考,跨时钟域神器。——异步FIFO(简介及手撕代码)_第2张图片

相关参数

(1)宽度(WIDTH),FIFO一次读写的数据位宽
(2)深度(DEEPTH),FIFO可以存储多少个数据
(3)满标志:FIFO 已满或将要满时送出的信号,停止写操作而造成溢出(overflow)。
(4)空标志:FIFO 已空或将要空时送出的信号,停止读出数据而造成无效数据的读出(underflow)。
(5)读时钟:读取数据端的时钟信号。
(6)写时钟:写入数据端的时钟信号。
(7)读指针:内部读地址,时钟有效自动加一。
(8)写指针:内部写地址,时钟有效自动加一。
(9)读使能:读取端数据有效。
(10)写使能:写入端数据有效。

异步FIFO 内部组成

如下图所示,简单的异步FIFO内部主要包含以下五个模块,分别是双端口RAM、写满标志判断write_full、读空标志判断read_empty、读时钟到写时钟同步、写时钟到读时钟同步。

这里常考的几个关键点是:
(1)双端口RAM相关知识。
(2)写满和读空信号的判断。 用格雷码怎么判断,二进制码怎么判断,有何优缺点?
(3)同步模块的作用? 二进制转格雷码怎么做?同步之后的写满和读空是真满 真空吗?
数字IC笔面常考,跨时钟域神器。——异步FIFO(简介及手撕代码)_第3张图片

异步FIFO的Verilog代码(强烈建议手敲,不要复制粘贴!)

下面将附上异步FIFO的Verilog代码,这里建议初学者一定要手敲,用心理解各个模块的作用以及信号连接。同时,也能作为基础的代码练习,培养手速和coding style。最后,这里附上的代码仅供学习,重点是学习异步FIFO的内部结构,感兴趣的同学可以自己再找找异步FIFO相关的代码。

注:这里代码借鉴了猪肉白菜的博客园,各位同学一定要去看看这篇博客。
另外,还有一篇菜鸟教程的博客,也很不错

(1)猪肉白菜博客链接: link

(2)菜鸟教程博客: link

顶层模块

顶层包含了模块例化、内部连线、端口声明等。

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : FIFO_async.v
// Create : 2022-03-04 09:47:04
// Revise : 2022-03-04 21:03:52
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------
//异步fifo设计 包括五个部分 RAM、write_full、read_empty、synchronization(写到读、读到写)
module FIFO_async #( 
	//------------------paramter------------------------------

 	parameter 					FIFO_data_size	=	6 	,
 	parameter					FIFO_addr_size 	=	5 		
	
	)(	
	//write signals
	input 				 					clk_w		,
	input 				 					rst_w		,
	input 				 					w_en		,
 	//read signals
	input 				 					clk_r		,
	input 				 					rst_r		,
	input 				 					r_en		,
	//data in & out
	input 		[FIFO_data_size-1:0] 		data_in 	,
	output 		[FIFO_data_size-1:0] 		data_out  	,
	//key signals
	output 		wire						empty 		,
	output 		wire						full 		
);



//==============================================================
//------------paramter reg  wire  ------------------------------

wire 		[FIFO_addr_size:0]		r_pointer_gray_sync		 	;
wire		[FIFO_addr_size:0]		w_pointer_gray_sync		 	;

wire 		[FIFO_addr_size:0]		r_pointer_gray 				;	
wire 	 	[FIFO_addr_size:0] 		w_pointer_gray 				;

wire 		[FIFO_addr_size-1:0] 	w_addr 						;
wire 		[FIFO_addr_size-1:0] 	r_addr 						;



//inst model
	RAM #(
			.FIFO_data_size(FIFO_data_size),
			.FIFO_addr_size(FIFO_addr_size)
		) inst_RAM (
			.clk_w    (clk_w		),
			.rst_w    (rst_w		),
			.clk_r    (clk_r		),
			.rst_r    (rst_r		),
			.full     (full 		),
			.empty    (empty 		),
			.w_en     (w_en 		),
			.r_en     (r_en 		),
			.r_addr   (r_addr		),
			.w_addr   (w_addr 		),
			.data_in  (data_in 		),
			.data_out (data_out		)
		);


	write_full #(
			.FIFO_addr_size(FIFO_addr_size)
		) inst_write_full (
			.clk_w               (clk_w 				),
			.rst_w               (rst_w 				),
			.w_en                (w_en 					),
			.r_pointer_gray_sync (r_pointer_gray_sync 	),
			.w_pointer_gray      (w_pointer_gray 		),
			.w_addr              (w_addr 				),
			.full                (full 					)
		);


	read_empty #(
			.FIFO_addr_size(FIFO_addr_size)
		) inst_read_empty (
			.clk_r               (clk_r	 				),
			.rst_r               (rst_r 				),
			.r_en                (r_en 					),
			.w_pointer_gray_sync (w_pointer_gray_sync 	),
			.r_pointer_gray      (r_pointer_gray 		),
			.r_addr              (r_addr 				),
			.empty               (empty 				)
		);


	synchronization #(
			.FIFO_addr_size(FIFO_addr_size)
		) inst1_synchronization (
			.clk      (clk_r 					),
			.rst      (rst_r 					),
			.din  	  (r_pointer_gray 			),
			.dout 	  (r_pointer_gray_sync		)
		);

	synchronization #(
			.FIFO_addr_size(FIFO_addr_size)
		) inst2_synchronization (
			.clk      (clk_w 					),
			.rst      (rst_w 					),
			.din  	  (w_pointer_gray 			),
			.dout 	  (w_pointer_gray_sync		)
		);



endmodule 


双端口RAM

双口RAM是FIFO的重要组成部分,用来完成数据存储。

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : RAM.v
// Create : 2022-03-04 10:02:58
// Revise : 2022-03-04 17:03:43
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------

//双口RAM模块

module RAM #(
//------------------------paramter-------------------
	parameter 			FIFO_data_size 		=		3		,
	parameter 			FIFO_addr_size 		= 		2		

	)(
	//----------------------port	define		-----------------
	//write clock & reset
	input				 					clk_w				,
	input 				 					rst_w				,
	//read clock & reset
	input 				 					clk_r 				,
	input				 					rst_r				,
	//key signals
	input 				 					full				,
	input 				 					empty 				,
 	//enable			
	input 				 					w_en				,
	input				 					r_en				,
	//wr rd addr
	input 		[FIFO_addr_size-1:0] 		w_addr				,
	input 		[FIFO_addr_size-1:0]		r_addr				,

	input 		[FIFO_data_size-1:0]		data_in				,
	output	reg	[FIFO_data_size-1:0]		data_out			
	
);




//==============================================================
//------------paramter reg  wire  ------------------------------

	reg 	[FIFO_data_size-1:0]  mem [{FIFO_addr_size{1'b1}}:0 ]	; 

	integer		i 		;
/*------------------------------------------------------------------------------
--  	wire 			flag_wr				;
		wire 			flag_rd	 			;
------------------------------------------------------------------------------*/


	//always block
	always @(posedge clk_w or negedge rst_w) begin 
		if(~rst_w) begin
				for ( i = 0; i <= FIFO_data_size; i=i+1) begin
					mem[i]		<=		{FIFO_data_size{1'b0}}		;
				end

		end 
		else if ((w_en == 1) && (full == 0))begin
				mem[w_addr] 	<=		data_in						;
		end
		else begin
				mem[w_addr] 	<=		{FIFO_data_size{1'b0}}		;
		end
	end

	//rd
	always @(posedge clk_r or negedge rst_r) begin 
		if(~rst_r) begin
			data_out		<=		{FIFO_data_size{1'b0}}		;		//'d0	
		end 
		else if ((r_en == 1) && (empty == 0))begin
				data_out 	<=		mem[r_addr] 					;
		end
		else begin
			data_out		<=		{FIFO_data_size{1'b0}}		;
		end
	end

	//assign
	/*
	assign 			flag_wr		=	(w_en == 1) && (full == 0)		;
	assign 			flag_rd		=	(r_en == 1) && (empty == 0)		;
	*/


endmodule 

写满信号判断模块

输出当前FIFO写满标志。

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : write_full.v
// Create : 2022-03-04 10:32:09
// Revise : 2022-03-04 15:25:14
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------
module write_full #(
	//-----------------paramter-------------------------------
	parameter 			FIFO_addr_size 		=		2		

	)(
		//==================	port define ===========================

	input										clk_w					,
	input										rst_w					,
	input										w_en 					,
	
	input		[FIFO_addr_size:0]				r_pointer_gray_sync		,
	output										full 					,
	output	wire	[FIFO_addr_size-1:0]		w_addr					,
	output	wire	[FIFO_addr_size:0]			w_pointer_gray 					 

);


//========================reg wire =================================

	reg 		[FIFO_addr_size:0]		w_pointer_bin				;
	wire 		flag_wr												;


//===================Main  Code=====================================

//always block
always @(posedge clk_w or negedge rst_w) begin : proc_
	if(~rst_w) begin
		 		w_pointer_bin 		<= 		0				;
	end 
	else if (flag_wr)begin
				w_pointer_bin	 	<= 		w_pointer_bin + 1;
	end
	else begin
		 w_pointer_bin 				<= 			w_pointer_bin 	;
	end
end



//assign
assign		flag_wr				=		(w_en == 1) && (full == 0)			;

assign		w_pointer_gray		=		(w_pointer_bin >> 1)^w_pointer_bin	;
//二进制码转换为gary码		Gn-1 = 	Bn-1 Gi= Bi+1 ^ Bi

assign 		w_addr				=		w_pointer_bin[FIFO_addr_size-1:0]	;

assign		full 				=		(w_pointer_gray == {~r_pointer_gray_sync[FIFO_addr_size:FIFO_addr_size-1],r_pointer_gray_sync[FIFO_addr_size-2:0]} ) ? 1: 0	;

//满信号  最高位取反,其他位相等就是满
endmodule 


读空信号判断模块

输出当前FIFO读空标志。

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : read_empty.v
// Create : 2022-03-04 11:10:59
// Revise : 2022-03-04 17:19:20
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------
//读空信号产生模块
module read_empty #(
//=====================paramter ================================================

	parameter  				FIFO_addr_size		=		2			
	)(
					//=========================port define===========================================

	input 									clk_r							,
	input									rst_r							,
	input									r_en 							,
	
	input 		[FIFO_addr_size:0]			w_pointer_gray_sync				,
				
	output  wire               				empty 							,
	output 	wire	[FIFO_addr_size-1:0]	r_addr							,
	output	wire	[FIFO_addr_size:0]		r_pointer_gray 					

	
);


//======================reg wire ==================================================
	reg 			[FIFO_addr_size:0] 		r_pointer_bin 					;
	//wire 									flag_rd							;

//============================Main code============================================

always @(posedge clk_r or negedge rst_r) begin : proc_
	if(~rst_r) begin
		 r_pointer_bin		<= 		{FIFO_addr_size{1'b0}}			;
	end
	else if ((r_en == 1) &&(empty == 0)) begin
		 r_pointer_bin		<=    r_pointer_bin + 1   				;
	end
end


//assign

//assign 		flag_rd 			= 	(r_en == 1) &&(empty == 0)						;
assign		r_pointer_gray		=	(r_pointer_bin>>1)^r_pointer_bin				;
assign		r_addr				=	r_pointer_bin[FIFO_addr_size-1:0]				;
assign 		empty 				=	r_pointer_gray == w_pointer_gray_sync? 1: 0 	;	//MSB 最高位相等就判断为空

endmodule 


信号同步模块

这里用到的是单bit同步的方式,结构简单,在高速应用中则采用其他的同步方式。

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : synchronization.v
// Create : 2022-03-04 14:26:40
// Revise : 2022-03-04 20:55:24
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------
///同步模块 打两拍进行同步
module synchronization #(
//=======================paramter==========================================
	parameter		FIFO_addr_size		=		2		
	)(
						
	//====================port define=======================================
		input							clk 				,
		input 							rst 				,
		input 		[FIFO_addr_size:0]	din					,
		output	reg	[FIFO_addr_size:0]	dout				
	
);

//====================reg wire ==========================================
	reg 		[FIFO_addr_size:0]	dout_t			;

//always block
always @(posedge clk or negedge rst) begin 
	if(~rst) begin
		 		dout 		<= 		{(FIFO_addr_size){1'b0}}				;
		 		dout_t		<=		{(FIFO_addr_size){1'b0}}				;
	end 		
	else begin		
		 		dout_t		<= 		din			;
		 		dout 		<=		dout_t		;
	end
end

endmodule 

testbench

// -----------------------------------------------------------------------------
// Copyright (c) 2014-2022 All rights reserved
// -----------------------------------------------------------------------------
// Author : hfut904
// File   : tb_FIFO_async.v
// Create : 2022-03-04 15:46:00
// Revise : 2022-03-04 16:44:32
// Editor : sublime text4, tab size (4)
// -----------------------------------------------------------------------------
//testbench 测试模块
`timescale 		1ns/1ns
module tb_FIFO_async ();


	//============parameter	=================
	parameter 			FIFO_data_size		=		3 		;
	parameter 			FIFO_addr_size		=		2 		;

	//	======	reg wire======================================
	reg 									clk_r 			;
	reg 									rst_r 			;
	reg        								r_en 			;
			
	reg 									clk_w 			;
	reg 									rst_w 			;
	reg        								w_en 			;

	reg   		[FIFO_data_size-1:0]		data_in 		;
	wire 		[FIFO_data_size-1:0]		data_out 		;

	wire 									empty 			;
	wire 									full 			;

	integer  i  	;

	// clk_w  写模块信号
	initial 	begin
		clk_w 		=		0 						;
		rst_w 		= 		1 						;
		data_in 	=	{FIFO_data_size{1'b0}}		;

	#15
		rst_w  		= 		0 				;
	#20
		rst_w 		= 		1 				;

	end

	//clk_r  读模块信号
	initial 	begin
		clk_r 		= 		0 			;
		rst_r  		= 		1 			;
		r_en  		= 		0 			;
	#25
		rst_r 		= 		0 			;
	#50
		rst_r 		= 		1 			;
	end

	//w_en 写使能
	initial 	begin
		w_en 		= 		0 			;
	#450
		w_en 		= 		1 			;
	#400
		w_en 		= 		0 			;
	#750
		w_en 		= 		1 			;
	end

	//r_en 读使能
	initial  	begin
		r_en 		= 		0 			;
	#900
		r_en 		= 		1 			;
	#400
		r_en 		= 		0 			;
	#300
	 	r_en 		= 		1 			;

	end

	initial 	begin
		for ( i = 0; i <= 50; i=i+1) begin
			/* code */
			#100
				data_in 	= 	i 		;
		end
	end

	//always block
	always #25 clk_w 	= 		~clk_w 			;

	always #50 clk_r 	=		~clk_r 			;

	FIFO_async #(
			.FIFO_data_size(FIFO_data_size),
			.FIFO_addr_size(FIFO_addr_size)
		) inst_FIFO_async (
			.clk_w    (clk_w 		),
			.rst_w    (rst_w 		),
			.w_en     (w_en 		),
			.clk_r    (clk_r 		),
			.rst_r    (rst_r 		),
			.r_en     (r_en 		),
			.data_in  (data_in 		),
			.data_out (data_out 	),
			.empty    (empty 		),
			.full     (full 		)
		);

endmodule 


波形截图

数字IC笔面常考,跨时钟域神器。——异步FIFO(简介及手撕代码)_第4张图片

总结

(1)需要了解异步FIFO内部的基本结构
(2)掌握写满和读空信号的判断
(3)会在项目中使用FIFO

注:异步FIFO内部结构大差不差,但是不同结构的异步FIFO在实际项目中的性能有所差异,学习异步FIFO重要的是体会内部的基本组成,了解异步FIFO的作用。在做FPGA或者ASIC设计时,尽量使用自带的IP核。(好的设计一定是在之前的基础上优化而来的,成熟的IP是设计时的优先选择!同理,一个设计只有在经过无数次debug和移植验证后,才能称为一个可用的设计。)后续会上传本次源码和仿真文件,不过还是建议初学者按照自己的代码风格手敲一遍,解决过程中出现的小bug,IC设计需要经验而不是复制粘贴。

你可能感兴趣的:(数字IC设计,fpga开发)