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),这样读写地址加1时候可能会出现fifo地址溢出的问题,我们可以增加一位,这样rd_addr/wr_addr都是$clog2(DEPTH)+1位,我们用最高位msb来作为指示位。
image

  • 读写地址高位相同,其它位也相同,则fifo为空。
    • 上图左边深度为8的FIFO中,写地址和读地址都是4位,最高位被用来当作指示位。写三个数据,读三个数据,此时写地址和读地址,高位和其余位都相同,所以此时fifo为空,右边的深度为8的fifo中,则是写11个数据,读11个数据,读写地址高位都是1,其余位也相同,显然此时fifo也为空。
  • 读写地址高位不同,其它位相同,则fifo为满。
    • 中间深度为8的fifo中,写11个数据,读3个数据,写地址高位为1,读地址高位为0,其余位相同,此时fifo中的数据还有8个,显然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

我们可以得到和第二种实现方法一样的波形。

posted on 2024-06-14 18:57  糊涂二蛋  阅读(129)  评论(0编辑  收藏  举报