用verilog/systemverilog 設計fifo (1)

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

目錄
  • fifo的基本原理
  • 基於計數器的同步fifo實現(1)
  • 基於計數器的同步fifo實現(2)
  • 基於高位補償法的fifo實現

fifo的基本原理

FIFO(first in first out),即先進先出儲存器,功能與資料結構中的佇列相似。
在IC設計中,FIFO常用來緩衝突發資料,流式資料與塊資料的轉換等等。
image
比如上圖中,在兩個block之間,透過輸入命令fifo來快取block1的輸入請求命令。

基於計數器的同步fifo實現(1)

image

在這種fifo實現方法中,我們用讀寫計數(或者說讀寫指標)來實現fifo的讀寫。

  • 初始讀計數rd_cnt=0,寫計數wr_cnt=0,fifo中資料計數為: data_cnt=wr_cnt-rd_cnt=0
  • 寫入四個資料,每寫入一個資料時,ram[wr_cnt]<=din;wr_cnt <= wr_cnt + 1,所以寫入四個資料後,wr_cnt=4, fifo中資料計數為:data_cnt=wr_cnt-rd_cnt=4
  • 假設地址位為3位,3位地址最大能表示深度為8的fifo,但是在這種實現fifo方法中,fifo容量只能是7或者7以下的值。因為wr_cnt7時,如果再寫一個資料,wr_cnt + 1= 3'b111 + 1'b1 = 3'000,如果此時rd_cnt=0,則fifo中資料量為wr_cnt-rd_cnt=0,但實際上我們寫了8個資料,且沒有讀取1個資料。這個時候會發生fifo溢位。
  • 透過限制fifo 容量為7或以下的值,可以防止fifo溢位,比如fifo容量為7,當ram[6]被寫以後,wr_cnt=7,rd_cnt=0,data_cnt=wr_cnt-rd_cnt=7,此時fifo full標誌會被置位,不能繼續寫fifo。
  • 接著兩個讀fifo,rd_cnt=2,這個時候fifo full又被清零,可以繼續寫fifo,寫入1個資料後, wr_cnt=0, 這個時候fifo中資料data_cnt = wr_cnt-rd_cnt=3'b000-3'b010=3'110=6,無符號數減法,高位借位。

這種基於計數器的fifo實現方法中,假設fifo地址位AW=n,則fifo容量為0<CAPACITY<2^n,下面是實現的verilog程式碼。
檔名稱:code4_40.v

`timescale 1ns/1ps
module SyncFifo_tb;
    logic clk = 1'b0;
    logic rst_n = 1'b0;

    initial begin
        $display("start a clock pulse");
        $dumpfile("sync_fifo_1.vcd"); 
        $dumpvars(0, SyncFifo_tb); 
        #300 $finish; 
    end

    always begin
        #5 clk = ~clk;
    end

    logic [7:0] din='0,dout;
    logic wr='0,rd='0;
    logic [2:0] wc,rc,dc;
    logic  fu, em;
    initial begin
         
//write 10 data
        repeat(10) begin
            @ (negedge clk) begin
                rst_n = 1'b1;
                wr <= 1'b1;
                rd <= 1'b0;
                din <= 8'($random());
            end
        end    
//重複讀取5次
		repeat(5) begin 
			@(negedge clk) begin
				wr <= 1'b0;
				rd <= 1'b1;

			end
		end
//再寫2次
		repeat(2) begin 
			@(negedge clk) begin
				wr <= 1'b1;
				rd <= 1'b0;
				din <= $random();
			end
		end

//重複讀取4次,讀空
		repeat(4) begin 
			@(negedge clk) begin
			wr <= 1'b0;
			rd <= 1'b1;

			end
		end
//空4個週期       
		repeat(4) begin 
			@(negedge clk) begin
			wr <= 1'b0;
			rd <= 1'b0;

			end
		end
//寫一個,讀一個
		forever begin 
			@(negedge clk) begin
			wr <= 1'b1;
			rd <= 1'b1;
			din <= $random();

			end
		end	  
                       
    end

    ScFifo #(.DW(8),.AW(3),.CAPACITY(7)) the_fifo(.clk(clk),.rst_n(rst_n),.din(din),.write(wr),
    .read(rd),.dout(dout),.wr_cnt(wc),.rd_cnt(rc),
    .data_cnt(dc),.full(fu),.empty(em));

    
endmodule

//DW是data width, AW是地址寬度
//CAPACITY是fifo容量,要求CAPACITY>=1 and CAPACITY<=2**AW-1
module ScFifo #(parameter DW=8, AW=10, CAPACITY=2**AW-1) (
    input wire clk,
    input wire rst_n,
    input wire [DW-1:0] din, 
    input wire write,
    input wire read,
    output logic [DW-1:0] dout,
    output logic [AW-1:0] wr_cnt='0,
    output logic [AW-1:0] rd_cnt='0,
    output logic [AW-1:0] data_cnt,
    output logic full,empty
);

    if(CAPACITY>2**AW-1) begin
        $error("CAPACITY must be less than 2**AW-1");
    end
    if(CAPACITY<1) begin
        $error("CAPACITY must be greater than 0");
    end
    logic [DW-1:0] ram[2**AW];

    always_ff @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            wr_cnt <= '0;
        end
        else begin
          if(write && !full) wr_cnt <= wr_cnt + 1'b1;
        end
    end
     always_ff @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            rd_cnt <= '0;
        end
        else begin
          if(read && !empty) rd_cnt <= rd_cnt + 1'b1;
        end
    end
    assign data_cnt = wr_cnt - rd_cnt;
    assign full =(data_cnt==CAPACITY);
    assign empty = (data_cnt==0);


    always_ff @(posedge clk) begin
        if(write && !full) ram[wr_cnt] <= din;
    end
    always_ff @(posedge clk) begin
        if(read && !empty) dout <= ram[rd_cnt];
    end

endmodule

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

iverilog -o myrun -g 2012 -s TestMem code4_40.v
vvp myrun
gtkwave sync_fifo_1.vcd

image

  • 在第2個時鐘上升沿開始寫fifo,輸入10個資料,但在第8時鐘上升沿時,fu訊號已經被置位,所以只寫入7個資料。
  • 在第12時鐘上升沿開始,從fifo中讀取5個資料,此時fifo剩下兩個資料。
  • 在第17個時鐘上升沿,再寫入兩個資料。
  • 在第19個時鐘上升沿,連續讀取4個資料,在第22個時鐘上升沿,fifo為空,em訊號被置位。
  • 在第27時鐘上升沿,開始寫一個資料,讀一個資料。

我們嘗試設定fifo容量為8,地址寬度為3,在vscode用iverlog模擬,發現並沒有彈出異常,竟然真的用容量8做了模擬,得到了錯誤的fifo結果。

ScFifo #(.DW(8),.AW(3),.CAPACITY(8)) the_fifo(.clk(clk),.rst_n(rst_n),.din(din),.write(wr),
    .read(rd),.dout(dout),.wr_cnt(wc),.rd_cnt(rc),
    .data_cnt(dc),.full(fu),.empty(em));

這是因為iverilog並不支援$error,我們可以在modelsim中建立工程(使用Modelsim進行簡單模擬),模擬上面的程式碼,可以得到下面的結果:

vsim -voptargs=+acc work.TestMem
# vsim -voptargs="+acc" work.TestMem 
# Start time: 09:05:42 on Jun 14,2024
# ** Note: (vsim-3812) Design is being optimized...
# ** Error: CAPACITY must be less than 2**AW-1
#    Scope: TestMem.the_fifo.genblk1 File: D:\xxx\verilog\modelsim\sync_fifo.sv Line: 18
# Optimization failed
# ** Note: (vsim-12126) Error and warning message counts have been restored: Errors=1, Warnings=0.
# Error loading design
# End time: 09:05:43 on Jun 14,2024, Elapsed time: 0:00:01
# Errors: 1, Warnings: 6

基於計數器的同步fifo實現(2)

在前面第一種同步fifo實現中,fifo地址寬度AW是固定的,它決定了fifo的深度。如果我們要實現指定fifo深度,而不需要指定fifo讀寫地址寬度,可以用下面的實現方法。
指定fifo深度
parameter FIFO_DEPTH=16
fifo地址寬度為
logic [$clog2(FIFO_DEPTH)-1:0] wr_addr, rd_addr;
我們用一個變數指定fifo中的資料數目
output logic [$clog2(FIFO_DEPTH):0] fifo_cnt
注意fifo_cnt的位寬是$clog2(FIFO_DEPTH):0,比如FIFO_DEPTH=16,則是output logic [4:0] fifo_cnt,如果FIFO_DEPTH=15到9都是output logic [4:0] fifo_cnt,因為$clog2(n)函式是向上取整。
這樣,多一位可以保證fifo_cnt不會溢位。

實現的verilog程式碼檔名稱code4_37.v

`timescale 1ns/1ps	

module sync_fifo_tb;
 
	logic clk=0;
	logic rst_n;
	logic wr_en,rd_en;
	logic [7:0] data_in;

	logic [7:0] data_out;
	logic full,empty;
//fifo cnt 位數是fifo深度$clog2(fifo_depth)+1
//保證fifo_cnt不會溢位(如果fifo_depth不為2的冪次,其實不會溢位,因為$clog2函式向上取整)
	logic [3:0] fifo_cnt;


	always #5 clk = ~clk;

	initial begin

    	$display("start a clock pulse");
    	$dumpfile("sync_fifo_2.vcd"); 
    	$dumpvars(0, sync_fifo_tb); 
   		#300 $finish;
	end

	initial begin
		rst_n=0;

//在時鐘下降沿改變資料
//重複10次,將會full
		repeat(10) begin 
			@(negedge clk) begin
			rst_n <= 1'b1;
			wr_en <= 1'b1;
			rd_en <= 1'b0;
			data_in <= $random();

			end
		end
//重複讀取6次
		repeat(6) begin 
			@(negedge clk) begin
				wr_en <= 1'b0;
				rd_en <= 1'b1;

			end
		end

//再寫2次
		repeat(2) begin 
			@(negedge clk) begin
				wr_en <= 1'b1;
				rd_en <= 1'b0;
				data_in <= $random();
			end
		end

//重複讀取4次,讀空
		repeat(4) begin 
			@(negedge clk) begin
			wr_en <= 1'b0;
			rd_en <= 1'b1;

			end
		end
//空4個週期       
		repeat(4) begin 
			@(negedge clk) begin
			wr_en<= 1'b0;
			rd_en <= 1'b0;

			end
		end
//寫一個,讀一個
		forever begin 
			@(negedge clk) begin
			wr_en <= 1'b1;
			rd_en <= 1'b1;
			data_in <= $random();

			end
		end	


	end

	sync_fifo_cnt #(.DATA_WIDTH(8),.FIFO_DEPTH(8)) the_sync_fifo_cnt
	(
		.clk(clk),
		.rst_n(rst_n),
		.wr_en(wr_en),
		.rd_en(rd_en),
		.data_in(data_in),

		.data_out(data_out),
		.full(full),
		.empty(empty),
		.fifo_cnt(fifo_cnt)
	);
endmodule



module	sync_fifo_cnt
#(
	parameter DATA_WIDTH=8,
	parameter FIFO_DEPTH=16
)
(
	input wire clk,
	input wire rst_n,
	input wire wr_en,
	input wire rd_en,
	input wire [DATA_WIDTH-1:0] data_in,

	output logic [DATA_WIDTH-1:0] data_out,
	output logic full,
	output logic empty,
	output logic [$clog2(FIFO_DEPTH):0] fifo_cnt

);                                                              
 
	logic [$clog2(FIFO_DEPTH)-1:0] wr_addr, rd_addr;
	logic [DATA_WIDTH-1:0] fifo_buffer[FIFO_DEPTH];

//寫地址時序邏輯
	always_ff @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			wr_addr <= '0;
		end
		else if(!full && wr_en ) begin
			//先讀後寫模式,所以寫入的是加1前的wr_addr
			wr_addr <= wr_addr + 1'b1;
			fifo_buffer[wr_addr] <= data_in;

		end

	end

//讀時序邏輯
	always_ff @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			rd_addr <= '0;
		end
		else if(!empty && rd_en) begin
			rd_addr <= rd_addr + 1;
			data_out <= fifo_buffer[rd_addr];
		end

	end
//更新計數器
	always_ff @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			fifo_cnt <=0;
		end
		else begin
			case ({wr_en,rd_en})
			2'b00: fifo_cnt <= fifo_cnt;
			2'b01: 
				if(fifo_cnt!=0)
					fifo_cnt <= fifo_cnt-1'b1;
			2'b10: 
				if(fifo_cnt!=FIFO_DEPTH)
					fifo_cnt <= fifo_cnt + 1'b1;
			2'b11: 
			begin 
				if(empty)
				//有寫沒有讀
				   fifo_cnt <= fifo_cnt + 1'b1;
				else if(full) 
				//有讀沒有寫
					fifo_cnt <= fifo_cnt - 1'b1;
				else
				   fifo_cnt <= fifo_cnt;  
			end
			endcase
	
		end
	end


	assign full  = (fifo_cnt == FIFO_DEPTH) ? 1'b1 : 1'b0;		
	assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;				
 
endmodule

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

iverilog -o myrun -g 2012 -s TestMem code4_37.v
vvp myrun
gtkwave sync_fifo_2.vcd

image

  • 第2個時鐘上升沿開始寫fifo
  • 第9個時鐘上升沿fifo寫滿,共寫了8個陣列,full訊號置位
  • 第12個時鐘上升沿開始,連續讀6個資料
  • 第18個時鐘上升沿開始,再寫兩個資料
  • 第20個時鐘上升沿開始,讀取4個資料,fifo讀空,訊號empty置位
  • 第28個時鐘上升沿開始,寫一個資料,讀一個資料

基於高位補償法的fifo實現

前面兩種同步fifo實現方法中,讀寫地址位數都是$clog2(DEPTH),這樣可能會出現fifo地址溢位的問題,我們可以增加一位,這樣rd_addr/wr_addr都是$clog2(DEPTH)+1位,我們用最高位msb來作為指示位。

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

verilog實現程式碼檔案為:
code4_41.v

`timescale 1ns/1ps	

module sync_fifo_tb;
 
	logic clk=0;
	logic rst_n;
	logic wr_en,rd_en;
	logic [7:0] data_in;

	logic [7:0] data_out;
	logic full,empty;
//fifo cnt 位數是fifo深度$clog2(fifo_depth)+1
//保證fifo_cnt不會溢位(如果fifo_depth不為2的冪次,其實不會溢位,因為$clog2函式向上取整)
	logic [3:0] fifo_cnt;


	always #5 clk = ~clk;

	initial begin

    	$display("start a clock pulse");
    	$dumpfile("sync_fifo_3.vcd"); 
    	$dumpvars(0, sync_fifo_tb); 
   		#300 $finish;
	end

	initial begin
		rst_n=0;

//在時鐘下降沿改變資料
//重複10次,將會full
		repeat(10) begin 
			@(negedge clk) begin
			rst_n <= 1'b1;
			wr_en <= 1'b1;
			rd_en <= 1'b0;
			data_in <= $random();

			end
		end
//重複讀取6次
		repeat(6) begin 
			@(negedge clk) begin
				wr_en <= 1'b0;
				rd_en <= 1'b1;

			end
		end

//再寫2次
		repeat(2) begin 
			@(negedge clk) begin
				wr_en <= 1'b1;
				rd_en <= 1'b0;
				data_in <= $random();
			end
		end

//重複讀取4次,讀空
		repeat(4) begin 
			@(negedge clk) begin
			wr_en <= 1'b0;
			rd_en <= 1'b1;

			end
		end
//空4個週期       
		repeat(4) begin 
			@(negedge clk) begin
			wr_en<= 1'b0;
			rd_en <= 1'b0;

			end
		end
//寫一個,讀一個
		forever begin 
			@(negedge clk) begin
			wr_en <= 1'b1;
			rd_en <= 1'b1;
			data_in <= $random();

			end
		end	


	end

	sync_fifo_hibit_ext #(.DATA_WIDTH(8),.FIFO_DEPTH(8)) the_sync_fifo_ext
	(
		.clk(clk),
		.rst_n(rst_n),
		.wr_en(wr_en),
		.rd_en(rd_en),
		.data_in(data_in),
		.data_out(data_out),
		.full(full),
		.empty(empty)
	);
endmodule


//高位擴充套件法,讀寫地址增加一位,用高位表示滿或空
module	sync_fifo_hibit_ext
#(
	parameter DATA_WIDTH=8,
	parameter FIFO_DEPTH=16
)
(
	input wire clk,
	input wire rst_n,
	input wire wr_en,
	input wire rd_en,
	input wire [DATA_WIDTH-1:0] data_in,

	output logic [DATA_WIDTH-1:0] data_out,
	output logic full,
	output logic empty
);                                                              
 
	logic [$clog2(FIFO_DEPTH):0] wr_addr, rd_addr;
	logic [DATA_WIDTH-1:0] fifo_buffer[FIFO_DEPTH];
	//低位地址,和深度匹配
	logic [$clog2(FIFO_DEPTH)-1:0] wr_addr_true, rd_addr_true;	
	logic msb_wr_addr, msb_rd_addr;

//連續賦值得到高位地址和低位地址
    assign {msb_wr_addr,wr_addr_true} = wr_addr;
	assign {msb_rd_addr,rd_addr_true} = rd_addr;


//寫地址時序邏輯
	always_ff @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			wr_addr <= '0;
		end
		else if(!full && wr_en ) begin
			//先讀後寫模式,所以寫入的是加1前的wr_addr
			wr_addr <= wr_addr + 1'b1;
			fifo_buffer[wr_addr_true] <= data_in;

		end

	end

//讀時序邏輯
	always_ff @(posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			rd_addr <= '0;
		end
		else if(!empty && rd_en) begin
			rd_addr <= rd_addr + 1;
			data_out <= fifo_buffer[rd_addr_true];
		end

	end


//高位相同,且讀地址=寫地址,fifo空
	assign empty  = (rd_addr == wr_addr) ? 1'b1 : 1'b0;	
//高位不同,讀地址等於寫地址,fifo滿
	assign full = ((msb_rd_addr!=msb_wr_addr) && (rd_addr_true==wr_addr_true))? 1'b1 : 1'b0;				
 
endmodule

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

iverilog -o myrun -g 2012 -s TestMem code4_41.v
vvp myrun
gtkwave sync_fifo_3.vcd

我們可以得到和第二種實現方法一樣的波形。

相關文章