用verilog/systemverilog 設計fifo (2)

糊涂二蛋發表於2024-06-22

目錄
  • 非同步fifo實現中要解決的問題
    • 訊號同步到那個時鐘域
    • 讀寫指標轉化為格雷碼
      • 格雷碼錶示的讀寫地址如何判斷空滿?
  • 非同步fifo verilog程式碼

非同步fifo實現中要解決的問題

非同步fifo和同步fifo功能相似,但是它的讀寫由兩個時鐘訊號控制,所以它的設計和同步fifo不同,需要考慮更多的因素。
image

訊號同步到那個時鐘域

我們知道,寫fifo和寫地址更新肯定在寫時鐘域,也就是在wr_clk的時鐘上升沿用以下程式碼進行更新。

always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)
		wr_ptr <= 0;
	else if (!full && wr_en)begin								//寫使能有效且非滿
		wr_ptr <= wr_ptr + 1'd1;
		fifo_buffer[wr_ptr_true] <= data_in;
	end	

同理,讀fifo和讀地址更新在讀時鐘域,也就是在rd_clk的時鐘上升沿用以下程式碼進行更新。

always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)
		rd_ptr <= 'd0;
	else if (rd_en && !empty)begin								//讀使能有效且非空
		data_out <= fifo_buffer[rd_ptr_true];
		rd_ptr <= rd_ptr + 1'd1;
	end

但是fifo為空和為滿的判斷要同時用到讀地址和寫地址,這個時候應該在寫時鐘域還是讀時鐘域來做這個判斷呢?如果在寫時鐘域判斷,讀地址必須同步到寫時鐘域,如果在讀時鐘域判斷,寫地址必須同步到讀時鐘域。

  • 同步到寫時鐘域
    讀指標同步到寫時鐘域需要時間T,在經過T時間後,可能原來的讀指標會增加或者不變,也就是說同步後的讀指標一定是小於等於原來的讀指標的。寫指標也可能發生變化,但是寫指標本來就在這個時鐘域,所以是不需要同步的,也就意味著進行對比的寫指標就是真實的寫指標。
    • 現在來進行寫滿的判斷:也就是寫指標超過了同步後的讀指標一圈。但是原來的讀指標是大於等於同步後的讀指標的,所以實際上這個時候寫指標其實是沒有超過讀指標一圈的,也就是說這種情況是“假寫滿”。“假寫滿”不會造成功能錯誤,只會造成效能損失,大不了FIFO的深度我少用一些。事實上這還可以算是某種程度上的保守設計。
    • 接著進行讀空的判斷:也就是同步後的讀指標追上了寫指標。但是原來的讀指標是大於等於同步後的讀指標的,所以實際上這個時候讀指標實際上是超過了寫指標。這種情況意味著已經發生了“讀空”,卻仍然有錯誤資料讀出。所以這種情況會造成FIFO的功能錯誤。
  • 同步到讀時鐘域
    寫指標同步到讀時鐘域需要時間T,在經過T時間後,可能原來的讀指標會增加或者不變,也就是說同步後的寫指標一定是小於等於原來的寫指標的。讀指標也可能發生變化,但是讀指標本來就在這個時鐘域,所以是不需要同步的,也就意味著進行對比的讀指標就是真實的讀指標。
    • 現在來進行寫滿的判斷:也就是同步後的寫指標超過了讀指標一圈。但是原來的寫指標是大於等於同步後的寫指標的,所以實際上這個時候寫指標已經超過了讀指標不止一圈,這種情況意味著已經發生了“寫滿”,卻仍然資料被覆蓋寫入。所以這種情況就造成了FIFO的功能錯誤。
    • 接著進行讀空的判斷:也就是讀指標追上了同步後的指標。但是原來的寫指標是大於等於同步後的寫指標的,所以實際上這個時候讀指標其實還沒有追上寫指標,也就是說這種情況是“假讀空”。“假讀空”不會造成功能錯誤,只會造成效能損失,大不了我先不讀了,等資料多了再讀就是的。事實上這還可以算是某種程度上的保守設計。

綜合起來就是:
“寫滿”的判斷:需要將讀指標同步到寫時鐘域,再與寫指標判斷
“讀空”的判斷:需要將寫指標同步到讀時鐘域,再與讀指標判斷

讀寫指標轉化為格雷碼

跨時鐘域傳輸的一旦沒處理好就會引起亞穩態問題,造成讀寫地址的值異常,從而引發FIFO的功能錯誤。那麼應該如何將讀寫指標同步到對方的時鐘域呢?
將二進位制的讀寫地址轉化成格雷碼後再進行同步可以有效減少亞穩態問題。格雷碼轉化verilog實現可以參考
讀寫二進位制地址轉化為格雷碼

格雷碼錶示的讀寫地址如何判斷空滿?

二進位制表示的讀寫地址,我們可以擴充套件一位地址,用擴充套件的高位和其餘位地址來比較。

  • 讀寫地址高位相同,其它位也相同,則fifo為空。
  • 讀寫地址高位不同,其它位相同,則fifo為滿。

但格雷碼的特點決定我們不能用這種方法。

10進位制數 2進位制數 典型格雷碼
0 4'b0000 4'b0000
1 4'b0001 4'b0001
2 4'b0010 4'b0011
3 4'b0011 4'b0010
4 4'b0100 4'b0110
5 4'b0101 4'b0111
6 4'b0110 4'b0101
7 4'b0111 4'b0100
8 4'b1000 4'b1100
9 4'b1001 4'b1101
10 4'b1010 4'b1111
11 4'b1011 4'b1110
12 4'b1100 4'b1010
13 4'b1101 4'b1011
14 4'b1110 4'b1001
15 4'b1111 4'b1000

比如下面圖中,左邊是空,寫地址和讀地址相等,無論二進位制碼和格雷碼都是如此。
右邊是滿,寫地址繞了一圈回來,其值是11,其對應的格雷碼是4'b1110, 讀地址3對應的格雷碼是4'b0010,可見其高兩位相反,其餘位相同。

總結起來用格雷碼判斷空滿的方法就是:

  • 當最高位和次高位相同,其餘位相同認為是讀空
  • 當最高位和次高位不同,其餘位相同認為是寫滿

image

非同步fifo verilog程式碼

檔名稱: code4_43.v

`timescale 1ns/1ns	
 
module async_fifo_tb;
  
	logic	wr_clk;				
	logic	wr_rst_n;       		
	logic	wr_en;       		
	logic	[7:0] data_in;       	
 
	logic	rd_clk;			
	logic	rd_rst_n;       	
	logic	rd_en;						                                        
	logic  [7:0] data_out;			
	logic   empty;	
	logic   full;  

	initial begin

    	$display("start a clock pulse");
    	$dumpfile("async_fifo.vcd"); 
    	$dumpvars(0, async_fifo_tb); 
   		#600 $finish;
	end
 

	async_fifo
		#(
			.DATA_WIDTH	(8),			//FIFO位寬
    		.DATA_DEPTH	(8)			//FIFO深度
		)
	async_fifo_inst(
		.wr_clk		(wr_clk		),
		.wr_rst_n	(wr_rst_n	),
		.wr_en		(wr_en		),
		.data_in	(data_in	),	
		.rd_clk		(rd_clk		),               
		.rd_rst_n	(rd_rst_n	),	
		.rd_en		(rd_en		),	
		.data_out	(data_out	),
	
		.empty		(empty		),		
		.full		(full		)
	);
 

initial begin
	rd_clk = 1'b0;					//初始時鐘為0
	wr_clk = 1'b0;					//初始時鐘為0
	wr_rst_n <= 1'b0;				//初始復位
	rd_rst_n <= 1'b0;				//初始復位
	wr_en <= 1'b0;
	rd_en <= 1'b0;	
	data_in <= 'd0;
	#5
	wr_rst_n <= 1'b1;				
	rd_rst_n <= 1'b1;					
//重複10次寫操作
	repeat(10) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位隨機數
		end
	end
//拉低寫使能	
	@(negedge wr_clk)	wr_en <= 1'b0;
	
//重複6次讀操作,讓FIFO讀空 
	repeat(6) begin
		@(negedge rd_clk) rd_en <= 1'd1;		
	end
//拉低讀使能
	@(negedge rd_clk) rd_en <= 1'd0;		
//再寫2次
	repeat(2) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位隨機數
		end
	end
//持續同時對FIFO讀
	@(negedge rd_clk)rd_en <= 1'b1;

//持續同時對FIFO寫,寫入資料為隨機資料	
	forever begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位隨機數
		end

	end
end
 

always #5 rd_clk = ~rd_clk;		

always #10 wr_clk = ~wr_clk;			
 
endmodule

//非同步FIFO
module	async_fifo
#(
	parameter   DATA_WIDTH = 'd8,								
    parameter   DATA_DEPTH = 'd16							
)		
(		
//寫資料		
	input	wire wr_clk,				
	input	wire wr_rst_n, 
	input	wire wr_en, 
	input	[DATA_WIDTH-1:0]		data_in,  
//讀資料			
	input	wire rd_clk,
	input	wire rd_rst_n, 
	input	wire rd_en,				                                        
	output	logic	[DATA_WIDTH-1:0]	data_out,
//狀態標誌					
	output	logic	empty,	
	output	logic	full	
);                                                              
 

logic [DATA_WIDTH - 1 : 0]			fifo_buffer[DATA_DEPTH - 1 : 0];
	
logic [$clog2(DATA_DEPTH) : 0]		wr_ptr;		
logic [$clog2(DATA_DEPTH) : 0]		rd_ptr;	
logic	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d1;				//讀指標格雷碼在寫時鐘域下同步1拍
logic	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d2;				//讀指標格雷碼在寫時鐘域下同步2拍
logic	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d1;				//寫指標格雷碼在讀時鐘域下同步1拍
logic	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d2;				//寫指標格雷碼在讀時鐘域下同步2拍
	
//wire define
wire [$clog2(DATA_DEPTH) : 0]		wr_ptr_g;					//寫地址指標,格雷碼
wire [$clog2(DATA_DEPTH) : 0]		rd_ptr_g;					//讀地址指標,格雷碼
wire [$clog2(DATA_DEPTH) - 1 : 0]	wr_ptr_true;				//真實寫地址指標,作為寫ram的地址
wire [$clog2(DATA_DEPTH) - 1 : 0]	rd_ptr_true;				//真實讀地址指標,作為讀ram的地址
 
//地址指標從二進位制轉換成格雷碼
assign 	wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);					
assign 	rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
//讀寫RAM地址賦值
assign	wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//寫RAM地址等於寫指標的低DATA_DEPTH位(去除最高位)
assign	rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//讀RAM地址等於讀指標的低DATA_DEPTH位(去除最高位)
 
 
//寫操作,更新寫地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)
		wr_ptr <= 0;
	else if (!full && wr_en)begin								//寫使能有效且非滿
		wr_ptr <= wr_ptr + 1'd1;
		fifo_buffer[wr_ptr_true] <= data_in;
	end	
end
//將讀指標的格雷碼同步到寫時鐘域,來判斷是否寫滿
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)begin
		rd_ptr_g_d1 <= 0;										//寄存1拍
		rd_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		rd_ptr_g_d1 <= rd_ptr_g;								//寄存1拍
		rd_ptr_g_d2 <= rd_ptr_g_d1;								//寄存2拍
	end	
end
//讀操作,更新讀地址
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)
		rd_ptr <= 'd0;
	else if (rd_en && !empty)begin								//讀使能有效且非空
		data_out <= fifo_buffer[rd_ptr_true];
		rd_ptr <= rd_ptr + 1'd1;
	end
end
//將寫指標的格雷碼同步到讀時鐘域,來判斷是否讀空
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)begin
		wr_ptr_g_d1 <= 0;										//寄存1拍
		wr_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		wr_ptr_g_d1 <= wr_ptr_g;								//寄存1拍
		wr_ptr_g_d2 <= wr_ptr_g_d1;								//寄存2拍		
	end	
end

//當所有位相等時,讀指標追到到了寫指標,FIFO被讀空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
//高兩位相反,其它位相同,FIFO被寫滿
assign	full  = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
				,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;
endmodule

在vscode中,使用下面命令編譯,執行程式碼,然後用gtkwave 開啟波形檔案:

iverilog -o myrun -g 2012 -s TestMem code4_43.v
vvp myrun
gtkwave async_fifo.vcd

image

  • 在wr_clk時鐘域,從第2個時鐘上升沿到第9個時鐘上升沿,寫入8個資料,fifo full
  • 在rd_clk時鐘域,從第24個時鐘上升沿到29個時鐘上升沿,讀取8個資料
  • 在wr_clk時鐘域,從第16個時鐘上升沿開始,在每個時鐘上升沿寫fifo
  • 在rd_clk時鐘域,從第34個時鐘上升沿開始,在每個時鐘上升沿讀fifo,由於rd_clk更快,所以它很快就會讀空fifo,然後寫時鐘域每寫一個,讀時鐘域就讀一個。

相關文章