掰开揉碎讲 FIFO(同步FIFO和异步FIFO)

2025-05-29 19:24:50

一、什么是FIFO

FIFO 是 First In First Out 的简称。是指在FPGA内部用逻辑资源实现的能对数据的存储具有先进先出特性的一种缓存器。

FIFO 与 RAM 和 ROM 的区别是 FIFO 没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,其数据地址由内部读写指针自动加1完成。FIFO 使用起来简单方便,由此带来的缺点是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。

二、为什么要用FIFO

用Verilog实现的电路最终映射到FPGA内是由一个个独立的功能模块组成的,各个模块又通过相关信号关联在一起,当存在模块间处理数据速度不同(有快有慢)时,处理得快的模块就需要等一等处理得慢的模块,这个等待其实就是缓存的实现。我们可以采用FIFO来解决数据的缓存。

打个比方,就像水龙头放水慢(输入慢),但我们人提水的时候是一次处理一桶水(输出快),所以需要一个水桶作为缓存,等存满一桶水,再一次被人提走。

又或者像我们人喝水,一次接一杯水(输入快), 渴的时候喝两口(输出慢)。这里,杯子作为缓存。

在现代集成电路芯片中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,此时,异步时钟之间的接口电路的设计将成为关键。而使用异步FIFO可以在两个不同时钟系统之间快速而方便地传输实时数据。

FIFO的应用场景一般有数据缓存、协议处理、串并转换、跨时钟域数据处理。

三、FIFO分类

FIFO根据读写时钟是否为同一时钟分为同步FIFO和异步FIFO。

同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时可同时发生读写操作。

异步FIFO是指读写时钟不一致,读写时钟是互相独立的2个时钟。

同步FIFO在实际应用中比较少见,常用的是异步FIFO,但基于学习的目的,下文对两种FIFO都进行讲解。

四、同步FIFO

1. 同步FIFO电路框图

截图来自https://blog.csdn.net/HengZo/article/details/49683707。

简单来说,同步FIFO其实就是一个双口RAM加上两个读写控制模块。FIFO的常见参数和信号如下:

FIFO的宽度:即FIFO一次读写操作的数据位;

FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。

满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。

空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。

读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。(同步FIFO 读写只有一个时钟)

写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。(异步FIFO读写时钟分开)

读指针:总是指向下一个将要被写入的单元,写完后自动加1,复位时,指向第1个单元(编号为0)。

写指针:总是指向下一个将要被读出的单元,读完后自动加1,复位时,指向第1个单元(编号为0)

其实可以把FIFO比作一个单向行驶的隧道,隧道两端都有一个门进行控制,FIFO宽度就是这个隧道单向有几个车道,FIFO的深度就是一个车道能容纳多少辆车,当隧道内停满车辆时,这就是FIFO的写满状态,当隧道内没有一辆车时,这便是FIFO的读空状态。

2. 同步FIFO空满判断(计数器)

FIFO 的设计原则是任何时候都不能向满FIFO中写入数据(写溢出),任何时候都不能从空FIFO中读取数据(读溢出)。FIFO 设计的核心是空满判断。FIFO设置读、写地址指针,FIFO初始化的时候读指针和写指针都指向地址为0的位置, 当往FIFO里面每写一个数据,写地址指针自动加1指向下一个要写入的地址。当从FIFO里面每读一个数据,读地址指针自动加1指向下一个要读出的地址,最后通过比较读地址指针和写地址指针的大小来确定空满状态。

当读地址指针追上写地址指针,写地址指针跟读地址指针相等,此时FIFO是读空状态。

当写地址指针追上读地址指针,写指针跟读地址指针再次相等的时候,此时FIFO是写满状态。

FIFO的空满判断一般有两种方式,一种是计数器,另一种是拓展最高位。本文将在同步FIFO里面讲解计数器的方法。在异步FIFO里面讲解拓展最高位的方法。

3. 同步FIFO设计代码

同步FIFO基本接口:

信号

描述

clk

系统时钟

rstn

系统复位信号

wr_en

写使能端

wr_data

FIFO写数据

fifo_full

FIFO的满标志位

rd_en

读使能端

rd_data

FIFO读数据

fifo_empty

FIFO的空标志位

同步FIFO当中,当写使能有效的时候计数器加一;当读使能有效的时候,计数器减一,将计数器与FIFO的size进行比较来判断FIFO的空满状态。这种方法设计比较简单,但是需要的额外的计数器,就会产生额外的资源,而且当FIFO比较大时,会降低FIFO最终可以达到的速度。

同步FIFO实现代码如下:

1 module sync_fifo#(parameter BUF_SIZE=8, BUF_WIDTH=8) (

2 //FIFO的数据位宽默认为8bit

3 //FIFO深度默认为8

4

5

6 input i_clk,//输入时钟

7 input i_rst,//复位信号

8 input i_w_en,//写使能信号

9 input i_r_en,//读使能信号

10 input [BUF_WIDTH-1:0] i_data,//写入数据

11

12 output reg [BUF_WIDTH-1:0] o_data,//读出数据

13 output o_buf_empty,//FIFO空标志

14 output o_buf_full );//FIFO满标志

15

16 reg [3:0] fifo_cnt; //记录FIFO数据个数

17 reg [$clog2(BUF_SIZE)-1:0] r_ptr,w_ptr; //数据指针为3位宽度,0-7索引,8个数据深度,循环指针0-7-0-7

18 reg [BUF_WIDTH-1:0] buf_mem[0:BUF_SIZE-1]; //定义FIFO大小

19

20

21 //判断空满

22 assign o_buf_empty=(fifo_cnt==4'd0)?1'b1:1'b0;

23 assign o_buf_full=(fifo_cnt==4'd8)?1'b1:1'b0;

24

25

26 always@(posedge i_clk or posedge i_rst) //用于修改计数器

27 begin

28 if(i_rst)

29 fifo_cnt<=4'd0;

30 else if((!o_buf_full&&i_w_en)&&(!o_buf_empty&&i_r_en)) //同时读写,计数器不变

31 fifo_cnt<=fifo_cnt;

32 else if(!o_buf_full&&i_w_en) //写数据,计数器加1

33 fifo_cnt<=fifo_cnt+1;

34 else if(!o_buf_empty&&i_r_en) //读数据,计数器减1

35 fifo_cnt<=fifo_cnt-1;

36 else

37 fifo_cnt <= fifo_cnt; //其他情况,计数器不变

38 end

39

40 always@(posedge i_clk or posedge i_rst) //读数据

41 begin

42 if(i_rst)

43 o_data<=8'd0;

44 else if(!o_buf_empty&&i_r_en)

45 o_data<=buf_mem[r_ptr];

46 end

47

48 always@(posedge i_clk) //写数据

49 begin

50 if(!o_buf_full&&i_w_en)

51 buf_mem[w_ptr]<=i_data;

52 end

53

54 always@(posedge i_clk or posedge i_rst) //读写地址指针变化

55 begin

56 if(i_rst) begin

57 w_ptr <= 0;

58 r_ptr <= 0;

59 end

60 else begin

61 if(!o_buf_full&&i_w_en) // 写数据,地址加1,溢出后自动回到0开始

62 w_ptr <= w_ptr + 1;

63 if(!o_buf_empty&&i_r_en) // 读数据,地址加1,溢出后自动回到0开始

64 r_ptr <= r_ptr + 1;

65 end

66 end

67

68 endmodule

4. 同步FIFO仿真结果

同步FIFO仿真测试文件

1 `timescale 1ns/1ns

2

3 module sync_fifo_tb;

4 reg i_clk,i_rst;

5 reg i_w_en,i_r_en;

6 reg [7:0] i_data;

7 wire [7:0] o_data;

8 wire o_buf_empty,o_buf_full;

9

10 sync_fifo dut(

11 .i_clk(i_clk),

12 .i_rst(i_rst),

13 .i_data(i_data),

14 .i_w_en(i_w_en),

15 .i_r_en(i_r_en),

16 .o_buf_empty(o_buf_empty),

17 .o_buf_full(o_buf_full),

18 .o_data(o_data)

19 );

20

21 initial begin

22 #30;

23 forever #10 i_clk = ~i_clk; //时钟

24 end

25 reg [7:0] r_data=8'd0;

26 initial begin

27 i_clk=1'b0;

28 i_rst=1'b0;

29 i_w_en=1'b0;

30 i_r_en=1'b0;

31 i_data=8'd0;

32 #5 i_rst=1'b1;

33 #10 i_rst=1'b0;

34

35 push(1);

36 fork //同时执行push和pop

37 push(2);

38 pop(r_data);

39 join

40 push(3);

41 push(4);

42 push(5);

43 push(6);

44 push(7);

45 push(8);

46 push(9);

47 push(10);

48 push(11);

49 push(12);

50 push(13);

51 push(14);

52 push(15);

53 push(16);

54 push(17);

55 pop(r_data);

56 push(18);

57 pop(r_data);

58 pop(r_data);

59 pop(r_data);

60 pop(r_data);

61 push(19);

62 pop(r_data);

63 push(20);

64 pop(r_data);

65 pop(r_data);

66 pop(r_data);

67 pop(r_data);

68 pop(r_data);

69 pop(r_data);

70 pop(r_data);

71 pop(r_data);

72 pop(r_data);

73 pop(r_data);

74 pop(r_data);

75 push(21);

76 pop(r_data);

77 pop(r_data);

78 pop(r_data);

79 pop(r_data);

80 #100 $stop;

81 end

82

83 task push (input [7:0] data);

84 if(o_buf_full)

85 $display("Cannot push %d: Buffer Full",data);

86 else begin

87 $display("Push",,data);

88 i_data=data;

89 i_w_en=1;

90 @(posedge i_clk) #4 i_w_en= 0; //时钟上升沿后4ns,写使能清零

91 end

92 endtask

93

94 task pop(output[7:0] data);

95 if(o_buf_empty)

96 $display("Cannot Pop: Buffer Empty");

97 else begin

98 i_r_en=1;

99 @(posedge i_clk) #4 i_r_en= 0; //时钟上升沿4ns后,读使能清零

100 data = o_data;

101 $display("Pop:",,data);

102 end

103 endtask

104 endmodule

采用Modelsim仿真得到如下波形:

可以在Modelsim的View——Transcript窗口看到有如下打印信息:

# run -all

# Push 1

# Push 2

# Pop: 1

# Push 3

# Push 4

# Push 5

# Push 6

# Push 7

# Push 8

# Push 9

# Cannot push 10: Buffer Full

# Cannot push 11: Buffer Full

# Cannot push 12: Buffer Full

# Cannot push 13: Buffer Full

# Cannot push 14: Buffer Full

# Cannot push 15: Buffer Full

# Cannot push 16: Buffer Full

# Cannot push 17: Buffer Full

# Pop: 2

# Push 18

# Pop: 3

# Pop: 4

# Pop: 5

# Pop: 6

# Push 19

# Pop: 7

# Push 20

# Pop: 8

# Pop: 9

# Pop: 18

# Pop: 19

# Pop: 20

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Push 21

# Pop: 21

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

# Cannot Pop: Buffer Empty

五、异步FIFO

虽然各FPGA大厂商都有自己的现成 FIFO IP 可供调用, 而且用户自己设计异步FIFO是比较复杂的。但是我们仍然需要学习FIFO的设计原理,这样我们在设计或移植的过程中查找问题起来将有据可循。所以掌握异步FIFO设计原理是一名合格FPGA工程师的基本功。

1、异步FIFO的电路框图

异步FIFO有两个时钟信号,读和写接口分别采用不同时钟,这两个时钟可能时钟频率不同,也可能时钟相位不同,可能是同源时钟,也可能是不同源时钟。

由 Clifford E. Cummings(Sunburst Design 联合创始人、Verilog/SystemVerilog 领域权威专家)提出的 异步 FIFO(Asynchronous FIFO) 的实现方法成为硬件设计中处理跨时钟域数据通信的标杆方案(论文《Simulation and Synthesis Techniques for Asynchronous FIFO Design》)。

下面来看该方案里异步FIFO的系统框图:

可以看到异步FIFO实质上也是基于中间的双口RAM,外加一些读写控制电路组成的。因为这里读写用的是两个不同的时钟,这将涉及到跨时钟域(CDC, Clock Domain Crossing)问题。跨时钟域的电路会带来亚稳态。

2 、亚稳态

在了解亚稳态之前先了解建立时间(setup time,Tsu)和保持时间(hold time,Th)的概念。如下是一个触发器:

建立时间是指在触发器的时钟信号有效沿(如上升沿)到来之前,输入数据必须保持稳定不变的最短时间。只有满足建立时间,时钟才能可靠地对数据进行采样,若不满足,该时钟沿无法正确将数据打入触发器。

保持时间是指在触发器的时钟信号有效沿(如上升沿)到来之后,输入数据仍需保持稳定不变的最短时间。这确保时钟触发后,数据有足够时间被正确读取和转换。若保持时间不足,数据可能无法被有效处理。

建立时间和保持时间的时序图如下(Tco 代表数据经过触发器的时延):

在同步系统中,数据相对于时钟总有固定的关系。当这种关系满足器件的建立和保持时间的要求时,输出端会在特定的传输延迟时间内输出一个有效状态。因为在同步系统中输入信号总是满足触发器的时序要求,所以不会发生亚稳态。

其实换句话说就是,同步系统中时钟可以做到与数据同步:

如上图,假设同步系统中数据从0跳变到1,一般数据的跳变不是立马跳变,而是有一个斜坡。第一个时钟周期采集到的是0, 第二个周期电平已经稳定到1,所以同步时钟系统不会出现亚稳态。

但是,在异步系统中,由于数据和时钟的关系不是固定的,因此有时会出现违反建立和保持时间的现象。当违反建立和保持时间时,就会输出介于两个有效状态之间的中间级电平且无法确定停留在中间状态的时间,这就是出现了亚稳态(Metastability)。

当触发器处在亚稳态时,输出会在高低电平之间波动,这会导致延迟输出转换过程,并超出所规定的时钟到输出的延迟值( tco)。亚稳态输出恢复到稳定状态所需的超出tco的额外时间部分称为稳定时间 ( tMET)。并非所有不满足建立和保持时间的输入变化都会导致亚稳态输出。触发器是否进入亚稳态和返回稳态所需时间取决于生产器件的工艺技术与外界环境。一般来说,触发器都会在一个或者两个时钟周期内返回稳态。(参考Mohit Arora的《The Art of Hardware Architecture》)

亚稳态的过程可类比小球处于坡顶,处于不稳定状态,可能向左或转向右滚下山坡,具体是稳定到0还是1是随机的,与输入没有必然的关系。

拿异步FIFO读写时钟不同步来举例,当判断异步FIFO空满状态时,将读时钟域的读地址指针传输到写时钟域然后与写地址指针进行比较判断FIFO是否为满,将写时钟域的写地址指针传输到读时钟域然后与读地址指针进行比较判断FIFO是否为空。这种跨时钟域的处理就很可能会产生亚稳态:

假设数据跟clk1是同步的,第2个时钟比第1个时钟滞后一点点,那么第2个时钟在采集数据的时候有可能时钟上升沿正好对应在数据跳变的阶段,那此时读到的数据可能是0, 可能是1, 也可能进入振荡状态。这种不确定的电平输出会沿着信号通道上的电路继续传递下去,对电路造成很大危害,极有可能让整个系统挂死。

亚稳态不可完全避免, 只能通过一些处理机制如 引入同步机制(打2拍) 、 格雷码以及乒乓操作等来降低亚稳态出现的机率。

3、打两拍

异步FIFO的跨时钟域处理所带来的亚稳态可以通过同步机制(打两拍)来降低亚稳态发生的概率。

如下图,A时钟域的数据Q1传递给B时钟域, 当B时钟上升沿来时,可能恰好数据Q1从0跳变到1,这样Q2极有可能出现亚稳态。如果我们将Q2的值直接拿来用,将会导致亚稳态传播下去。所以后面再设置一个D触发器继续对Q2进行采样得到Q3。

可能Q2会产生亚稳态,但等到Q3时候电平就会稳定到0或者1(也有可能继续是亚稳态,但一个电路出现亚稳态概率非常低, 然后连续两次出现亚稳态的概率更低, 低到我们可以忽略, 因此我们可以假设打两拍以后Q3 不存在亚稳态了,因此打两拍可以解决亚稳态传播的问题)。

Q1经过B时钟打两拍同步以后的数据Q3才能在B时钟域被使用。

当然,可能有人会问,如果Q1当时跳变为1时却被识别为0 ,对电路就没有影响吗? 答案是,如果只是一个地方判断错误不会有太大影响。怕就怕亚稳态一直被传播下去。

4、格雷码

格雷码是一种相邻数据只有1bit变化的码制。

十进制数

自然二进制码

格雷码

0

0000

0000

1

0001

0001

2

0010

0011

3

0011

0010

4

0100

0110

5

0101

0111

6

0110

0101

7

0111

0100

8

1000

1100

9

1001

1101

10

1010

1111

11

1011

1110

12

1100

1010

13

1101

1011

14

1110

1001

15

1111

1000

如果地址采用二进制码,地址从3(0011)跳变到4(0100),有3个bit发生了变化, 每个bit 都有可能发生亚稳态,那么此时亚稳态出现的几率是1bit 跳变的3倍。

因为格雷码每次跳变只有一个bit,所以采用格雷码将大大降低了亚稳态发生的概率。

格雷码是二进制码右移1位再与原码相异或的结果。

二进制码转格雷码的Verilog代码实现如下:

graycode = (bincode>>1) ^ bincode;

如果二进制变化没有任何规律,那么采用格雷码也可能发生多 bit 的跳变,而 FIFO 设计中的读写地址都是连续变化的,因此格雷码适用于 FIFO 的地址处理。

5、 FIFO的乒乓操作

FIFO的乒乓操作是一种数据处理技术,它利用两个或多个FIFO交替进行读写。例如,在一个典型的乒乓操作中,当数据被写入到第一个FIFO时,第二个FIFO可以被读取;当第一个FIFO写完且第二个FIFO也读完时切换到第二个FIFO进行写入,同时读取第一个FIFO。

这样就确保读写操作不会同时访问同一缓冲区,起到了读和写的隔离,从而减少了亚稳态的发生。

当一个缓冲区完成写入后,读取端可以立即处理其数据,而写入端已开始填充另一个缓冲区。这种“填一个,读一个”的流水线模式,一定时间内确保数据流无中断。例如,在图像处理过程中,DMT(Digital Multiplier Timing)时序要求在行同步信号之后,必须没有时间间隔地输出一整行像素数据。如果使用带有反压机制的FIFO,可能会出现输出像素不完整的情况,因为FIFO的反压机制(为了防止数据流量过大导致fifo溢出,会在fifo前面加一个ready反压前级的数据,ready与fifo的full相关联,fifo满则ready为0让前级不会再写进去,这种称为反压机制)可能会导致数据的流动受到限制,从而影响每一行像素的连续输出。相反,采用乒乓操作可以有效解决这一问题。通过使用双缓冲区机制,乒乓操作允许系统在一个缓冲区中存储完整的一行像素数据,同时另一个缓冲区可以用来进行数据输出或进一步处理。这种方式确保了在行同步信号到达时,可以先完全存储好一整行的像素数据,然后再进行连续输出,避免了因数据流动受限而导致的像素输出不完整的问题。(参考文献《Proposed VESA and Industry Standards and Guidelines for Computer Display Monitor Timing (DMT)》)

总的来说,选择乒乓FIFO的场景有:高速数据流处理、对数据连续性要求较高、异步FIFO跨时钟域等。

6、异步FIFO的空满判断(拓展最高位)

FIFO只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成。在指针中添加一个额外的位(extra bit),当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其它位回零。对读指针也进行同样的操作。此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。

(1)空判断

如果两个指针相同,则说明两个指针折回的次数相等。其余位相等,说明FIFO为空;

(2)满判断

如果两个指针的MSB不同,其余位相同,说明写指针比读指针多折回了一次;如r_addr=0000,而w_addr = 1000,为满。

使用gray码判断“空”与“满”

使用gray码降低亚稳态概率,但同时也带来另一个问题,即在格雷码域如何判断空与满。

对于“空”的判断依然依据二者完全相等(包括MSB);

而对于“满”的判断,如下图,由于gray码除了MSB外,具有镜像对称的特点,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,不能说它为满。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:

wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。

wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。

剩下的其余位完全相等。截图参考 http://www.sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf

(3)虚空、虚满

当前计数器值和正在比较的计数器值不同。

当写地址同步到读时钟域时 这个地址需要在读时钟域打两拍,而这两拍的过程中写控制端还可以继续向FIFO里面写 数据,如果此时判断FIFO为空的话,这个空属于虚空。

当读地址同步到写时钟域时 这个地址需要在写时钟域打两拍,而这两拍的过程中读控制端还可以继续从FIFO里面读取 数据,如果此时判断FIFO为满的话,这个满属于虚满。

虚空虚满不会产生错误, 只是影响FIFO 效率。 理解这些原理后,分析问题就知道去哪里分析。

7、如何选择FIFO深度

设计FIFO的时候其深度的计算至关重要,深度设置小了有丢数据的风险,太大了又浪费资源。

其实FIFO的深度主要取决于传输时的极端情况。那么我们先了解跟极端情况相关的一个概念:突发传输。

突发传输(burst)

突发(burst)传输是这样定义的:In telecommunication, a burst transmission or data burst is the broadcast of a relatively high-bandwidth transmission over a short period。某个短时间内相对高带宽的数据传输。

其实突发传输也就是一个又一个的数据包,每一个数据包之间是有时间间隔的。假如模块A不间断地往FIFO中写数据,模块B同样不间断地从FIFO中读数据,不同的是模块A写数据的时钟频率要大于模块B读数据的时钟频率,那么在一段时间内总是有一些数据没来得及被读走,如果系统一直在工作,那么那些没有被读走的数据会越累积越多,那么FIFO的深度需要是无穷大的,因此只有在突发数据传输过程中讨论FIFO深度才是有意义的。

突发长度

一次传递一包数据完成后再去传递下一包数据,一段时间内传递的数据个数称为burst length。比如每100个写周期内写入80个数据,如果输入数据的模式是固定的,那么这里burst length就是80个数据。如果传输模式不是固定的,那就要考了极端情况,也就是所谓的“背靠背”。

背靠背

假设一个异步FIFO,每100个cycle可以写入80个数据,输入数据模式不固定,那么这就意味着这80个数据可以在100个cycle的周期的任意时间写进FIFO里面。极端情况下可以是连续的200个cycle中,两个80个数据是“背靠背”接着输入的(Case 5):

那么这种场景下的burst length就是160个数据了。

FIFO深度计算公式

FIFO的最小深度与突发长度(burst length), 读出率(rd_rate),写入率(wr_rate),读写时钟(rd_clk和wr_clk)等因素有关。

当写比读快,FIFO深度计算公式如下:

举例:写数据时钟wr_clk=80MHz,读数据时钟rd_clk=50MHz,每100个写时钟周期就有80个数据写入FIFO,每10个读时钟周期可以有6个数据读出FIFO。

分析:这里可能会出现极端情况,即背靠背时burst length为160,rd_rate就是6/10也就是0.6。根据上面的公式计算可得出FIFO深度:

160-160*(50/80)*(6/10)=100

因此FIFO的最小深度为100。由于FIFO深度只能取2的整数次幂,因此最小深度为:2^7 = 128。

在读写时钟相同或者读比写快时,理论上FIFO的最小深度是1即可,但在实际应用中,还需要考虑时钟相位差异、数据位宽、传输效率和模式等因素,并在计算出的最小深度基础上增加适当的余量。

当读比写快,FIFO深度计算公式如下:

举例:写数据时钟wr_clk=80MHz,读数据时钟rd_clk=100MHz,数据位宽1byte。写侧连续写入均为8192B长度的数据包。问:为保证输出的数据包是连续不间断,存够多少数据后才能开始发送?

分析:此场景为写入一定数据后开始读取FIFO,那么最恶劣的时刻当然是刚开始读取的时间点。想要保证输出连续,则必须满足:读取8192B数据期间写入的数据+已缓存数据 ≥8192B,即burst length为8192。在读完t = 8192*(1/rd_clk)这段时间里, 写入数据个数为:t/(1/wr_clk),8192 - 8192*(1/rd_clk)/(1/wr_clk) = 1638.4。同样,将参数带入上述公式:

8192−8192∗(wr_clk/rd_clk)*wr_rate=1638.4

(因为是连续写入,则写入率wr_rate为1)

即FIFO深度最小应是1638.4,在FIFO写满1638.4个数据后启动读,保证了读侧不出现”拉断”现象。如此,深度既满足了数据不丢失,也最大化节省了资源。由于FIFO深度只能取2的整数次幂,因此最小深度为:2^11 = 2048。

8、异步FIFO的设计代码

(1)顶层模块

1 module async_fifo#(parameter BUF_SIZE=8, BUF_WIDTH=8)

2 //FIFO深度默认为8

3 //FIFO的数据位宽默认为8bit

4

5 (

6 input [BUF_WIDTH-1:0] i_wdata,

7 input i_w_en, i_wclk, i_wrst_n, //写请求信号,写时钟,写复位

8 input i_r_en, i_rclk, i_rrst_n, //读请求信号,读时钟,读复位

9 output [BUF_WIDTH-1:0] o_rdata,

10 output o_buf_full,

11 output o_buf_empty

12 );

13 wire [$clog2(BUF_SIZE)-1:0] waddr, raddr;

14 wire [$clog2(BUF_SIZE):0] wptr, rptr, wq2_rptr, rq2_wptr;

15

16

17 /*在检测“满”或“空”状态之前,需要将指针同步到其它时钟域时,使用格雷码,可以降低同步过程中亚稳态出现的概率*/

18

19 sync_r2w I1_sync_r2w(

20 .wq2_rptr(wq2_rptr),

21 .rptr(rptr),

22 .wclk(i_wclk),

23 .wrst_n(i_wrst_n));

24 sync_w2r I2_sync_w2r (

25 .rq2_wptr(rq2_wptr),

26 .wptr(wptr),

27 .rclk(i_rclk),

28 .rrst_n(i_rrst_n));

29

30 /* DualRAM */

31

32 dualram #(BUF_WIDTH, BUF_SIZE) I3_DualRAM(

33 .rdata(o_rdata),

34 .wdata(i_wdata),

35 .waddr(waddr),

36 .raddr(raddr),

37 .wclken(i_w_en),

38 .wclk(i_wclk));

39

40

41 /*空、满比较逻辑*/

42

43 rptr_empty #(BUF_SIZE) I4_rptr_empty(

44 .rempty(o_buf_empty),

45 .raddr(raddr),

46 .rptr(rptr),

47 .rq2_wptr(rq2_wptr),

48 .rinc(i_r_en),

49 .rclk(i_rclk),

50 .rrst_n(i_rrst_n));

51 wptr_full #(BUF_SIZE) I5_wptr_full(

52 .wfull(o_buf_full),

53 .waddr(waddr),

54 .wptr(wptr),

55 .wq2_rptr(wq2_rptr),

56 .winc(i_w_en),

57 .wclk(i_wclk),

58 .wrst_n(i_wrst_n));

59 endmodule

(2)双端口RAM模块

双端口RAM模块用于存储数据。

1 module dualram

2 #(

3 parameter BUF_WIDTH = 8, // 数据位宽

4 parameter BUF_SIZE = 8 // FIFO深度

5 )

6 (

7 input wclken,wclk,

8 input [$clog2(BUF_SIZE)-1:0] raddr, //RAM 读地址

9 input [$clog2(BUF_SIZE)-1:0] waddr, //RAM 写地址

10 input [BUF_WIDTH-1:0] wdata, //写数据

11 output [BUF_WIDTH-1:0] rdata //读数据

12 );

13

14 reg [BUF_WIDTH-1:0] Mem[BUF_SIZE-1:0];

15

16

17 always@(posedge wclk)

18 begin

19 if(wclken)

20 Mem[waddr] <= wdata;

21 end

22

23 assign rdata = Mem[raddr];

24

25

26 endmodule

(3)同步模块1

sync_r2w 模块用于读地址同步到写控制端。

1 module sync_r2w

2 #(parameter BUF_SIZE = 8)

3 (

4 output reg [$clog2(BUF_SIZE):0] wq2_rptr,

5 input [$clog2(BUF_SIZE):0] rptr,

6 input wclk, wrst_n

7 );

8 reg [$clog2(BUF_SIZE):0] wq1_rptr;

9

10 always @(posedge wclk or negedge wrst_n) begin

11 if (!wrst_n)

12 {wq2_rptr,wq1_rptr} <= 0;

13 else

14 {wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};// 将写时钟域传过来的地址打两拍

15

16 end

17

18 endmodule

(4)同步模块2

sync_w2r模块用于写地址同步到读控制端。

1 module sync_w2r

2 #(parameter BUF_SIZE = 8)

3 (

4 output reg [$clog2(BUF_SIZE)+1:0] rq2_wptr,

5 input [$clog2(BUF_SIZE)+1:0] wptr,

6 input rclk, rrst_n

7 ); reg [$clog2(BUF_SIZE)+1:0] rq1_wptr;

8

9 always @(posedge rclk or negedge rrst_n) begin

10 if (!rrst_n)

11 {rq2_wptr,rq1_wptr} <= 0;

12 else

13 {rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};

14 end

15 endmodule

(5)空判断模块

空判断模块用于判断是否可以读取数据。

读操作时,读使能rinc有效且FIFO未空。

1 module rptr_empty

2 #(parameter BUF_SIZE = 8)

3 (

4 output reg rempty, //输出空信号

5 output [$clog2(BUF_SIZE)-1:0] raddr, //输出读数据地址

6 output reg [$clog2(BUF_SIZE):0] rptr, //读数据指针

7 input [$clog2(BUF_SIZE):0] rq2_wptr, //写数据指针的格雷码经过打两拍后输入

8 input rinc, rclk, rrst_n);

9 reg [$clog2(BUF_SIZE):0] rbin;

10 wire [$clog2(BUF_SIZE):0] rgraynext, rbinnext;

11 wire rempty_val;

12

13 always @(posedge rclk or negedge rrst_n)

14 if (!rrst_n)

15 begin

16 rbin <= 0;

17 rptr <= 0;

18 end

19 else

20 begin

21 rbin <= rbinnext ;

22 rptr <= rgraynext;

23 end

24 // gray码计数逻辑

25 assign rbinnext = !rempty ? (rbin + rinc) : rbin; //如果为空,则指针不变,如果不为空,指针+1

26 assign rgraynext = (rbinnext>>1) ^ rbinnext; //二进制到gray码的转换

27 assign raddr = rbin[$clog2(BUF_SIZE)-1:0];

28

29

30 /*读指针是一个n位的gray码计数器,比FIFO寻址所需的位宽大一位

31 当系统复位或者读指针和同步过来的写指针完全相等时(包括MSB),说明二者折回次数一致,FIFO为空*/

32 assign rempty_val = (rgraynext == rq2_wptr);

33 always @(posedge rclk or negedge rrst_n)

34 if (!rrst_n)

35 rempty <= 1'b1;

36 else

37 rempty <= rempty_val;

38 endmodule

(6)满判断模块

满判断模块用于判断是否可以写入数据。

写操作时,写使能winc有效且FIFO未满。

1 module wptr_full

2 #(

3 parameter BUF_SIZE = 8

4 )

5 (

6 output reg wfull, //输出满信号

7 output [$clog2(BUF_SIZE)-1:0] waddr, //输出写地址

8 output reg [$clog2(BUF_SIZE):0] wptr, //输出写指针

9 input [$clog2(BUF_SIZE):0] wq2_rptr, //读指针的格雷码打两拍后输入

10 input winc, wclk, wrst_n);

11 reg [$clog2(BUF_SIZE):0] wbin;

12 wire [$clog2(BUF_SIZE):0] wgraynext, wbinnext;

13 wire wfull_val;

14 // GRAYSTYLE2 pointer

15 always @(posedge wclk or negedge wrst_n)

16 if (!wrst_n)

17 begin

18 wbin <= 0;

19 wptr <= 0;

20 end

21 else

22 begin

23 wbin <= wbinnext;

24 wptr <= wgraynext;

25 end

26 //gray 码计数逻辑

27 assign wbinnext = !wfull ? (wbin + winc) : wbin;

28 assign wgraynext = (wbinnext>>1) ^ wbinnext;

29 assign waddr = wbin[$clog2(BUF_SIZE)-1:0];

30 /*由于满标志在写时钟域产生,因此比较安全的做法是将读指针同步到写时钟域*/

31

32 assign wfull_val = (wgraynext=={~wq2_rptr[$clog2(BUF_SIZE):$clog2(BUF_SIZE)-1],

33 wq2_rptr[$clog2(BUF_SIZE)-2:0]});

34 always @(posedge wclk or negedge wrst_n)

35 if (!wrst_n)

36 wfull <= 1'b0;

37 else

38 wfull <= wfull_val;

39 endmodule

异步FIFO设计的整体RTL Viewer如下图所示:

9、 异步FIFO仿真

(1)异步FIFO仿真文件

1 `timescale 1 ps/ 1 ps

2 module async_fifo_vlg_tst();

3 reg i_r_en;

4 reg i_rclk;

5 reg i_rrst_n;

6 reg i_w_en;

7 reg i_wclk;

8 reg i_wrst_n;

9 reg [7:0] i_wdata;

10

11 wire o_buf_empty;

12 wire o_buf_full;

13 wire [7:0] o_rdata;

14

15 async_fifo i1 (

16

17 .i_r_en(i_r_en),

18 .i_rclk(i_rclk),

19 .i_rrst_n(i_rrst_n),

20 .i_w_en(i_w_en),

21 .i_wclk(i_wclk),

22 .i_wdata(i_wdata),

23 .i_wrst_n(i_wrst_n),

24 .o_buf_empty(o_buf_empty),

25 .o_buf_full(o_buf_full),

26 .o_rdata(o_rdata)

27 );

28

29

30

31 always #10 i_wclk = ~i_wclk;

32 always #5 i_rclk = ~i_rclk;

33

34

35 reg [7:0] r_data=8'd0;

36

37 initial begin

38 i_wclk=1'b0;

39 i_rclk=1'b0;

40 i_wrst_n=1'b1;

41 i_rrst_n=1'b1;

42 i_w_en=1'b0;

43 i_r_en=1'b0;

44 i_wdata=8'd0;

45 #1 i_wrst_n=1'b0;

46 i_rrst_n=1'b0;

47

48 #1 i_wrst_n=1'b1;

49 i_rrst_n=1'b1;

50

51 #20 push(1);

52

53

54 push(2);

55 //pop(r_data);

56 push(3);

57 push(4);

58 push(5);

59 push(6);

60 push(7);

61 push(8);

62 push(9);

63 pop(r_data);

64 push(10);

65 push(11);

66 push(12);

67 push(13);

68 push(14);

69 push(15);

70 push(16);

71 pop(r_data);

72 push(17);

73 pop(r_data);

74 push(18);

75 pop(r_data);

76 pop(r_data);

77 pop(r_data);

78 pop(r_data);

79 push(19);

80 pop(r_data);

81 push(20);

82 pop(r_data);

83 pop(r_data);

84 pop(r_data);

85 pop(r_data);

86 pop(r_data);

87 pop(r_data);

88 pop(r_data);

89 pop(r_data);

90 pop(r_data);

91 pop(r_data);

92 pop(r_data);

93 push(21);

94 pop(r_data);

95 pop(r_data);

96 pop(r_data);

97 pop(r_data);

98 #100 $stop;

99 end

100

101 task push (input [7:0] data);

102 if(o_buf_full)

103 $display("Cannot push %d: Buffer Full",data);

104 else begin

105 $display("Push",,data);

106 i_wdata=data;

107 i_w_en=1;

108 @(posedge i_wclk) #4 i_w_en= 0; //时钟上升沿后4ns,写使能清零

109 end

110 endtask

111

112 task pop(output[7:0] data);

113 if(o_buf_empty)

114 $display("Cannot Pop: Buffer Empty");

115 else begin

116 data = o_rdata;

117 $display("Pop:",,data);

118 i_r_en=1;

119 @(posedge i_rclk) #4 i_r_en= 0; //时钟上升沿4ns后,读使能清零

120 end

121 endtask

122

123

124 endmodule

这里选择的是读写频率相同,但读是在时钟下降沿, 写在时钟的上升沿。

(2)异步FIFO仿真结果

打开Quartus 的 菜单栏的 Tools——Run Simulation Tool——RTL Simulation看到波形如下:

当复位撤销(复位信号低有效)之后,在写使能 i_w_en 拉高有效之后,写数据也开始变化:

empty 空标记也开始在几拍之后变为非空(有一个写到读侧的异步转换,打了两拍):

当读使能i_ r_en 拉高有效之后,读数据在下一拍也开始变化:

可以在Modelsim的View——Transcript窗口看到有如下打印信息:

1 # run -all

2 # Push 1

3 # Push 2

4 # Push 3

5 # Push 4

6 # Push 5

7 # Push 6

8 # Push 7

9 # Push 8

10 # Cannot push 9: Buffer Full

11 # Pop: 1

12 # Cannot push 10: Buffer Full

13 # Cannot push 11: Buffer Full

14 # Cannot push 12: Buffer Full

15 # Cannot push 13: Buffer Full

16 # Cannot push 14: Buffer Full

17 # Cannot push 15: Buffer Full

18 # Cannot push 16: Buffer Full

19 # Pop: 2

20 # Cannot push 17: Buffer Full

21 # Pop: 3

22 # Cannot push 18: Buffer Full

23 # Pop: 4

24 # Pop: 5

25 # Pop: 6

26 # Pop: 7

27 # Push 19

28 # Pop: 8

29 # Push 20

30 # Cannot Pop: Buffer Empty

31 # Cannot Pop: Buffer Empty

32 # Cannot Pop: Buffer Empty

33 # Cannot Pop: Buffer Empty

34 # Cannot Pop: Buffer Empty

35 # Cannot Pop: Buffer Empty

36 # Cannot Pop: Buffer Empty

37 # Cannot Pop: Buffer Empty

38 # Cannot Pop: Buffer Empty

39 # Cannot Pop: Buffer Empty

40 # Cannot Pop: Buffer Empty

41 # Push 21

42 # Cannot Pop: Buffer Empty

43 # Cannot Pop: Buffer Empty

44 # Cannot Pop: Buffer Empty

45 # Cannot Pop: Buffer Empty

46 # ** Note: $stop :

10. 异步复位同步释放

同步复位是指复位信号只有在时钟上升沿到来时,才能有效。否则,无法完成对系统的复位工作。参考设计如下:

always @(posedge clk) begin

if (!rst_n) begin // 低电平复位有效

q <= 1'b0;

end else begin

q <= d;

end

end

异步复位是指无论时钟沿是否到来,只要复位信号有效,就对系统进行复位。

always @(posedge clk or negedge rst_n) begin // 时钟或复位边沿触发

if (!rst_n) begin // 低电平复位有效

q <= 1'b0;

end else begin

q <= d;

end

end

在异步FIFO中,需为写时钟域和读时钟域分别设计独立的异步复位同步释放电路。

异步复位的必要性:

异步复位(Async Reset)能立即响应复位信号,无论时钟是否有效,确保系统在异常情况下快速进入确定状态。

在异步FIFO中,写时钟域和读时钟域可能独立运行,使用异步复位则可同时复位两个时钟域的电路。

同步释放的必要性:

若复位信号直接异步释放(撤销),不同时钟域的触发器可能在不同时钟边沿退出复位状态,导致以下问题:

亚稳态:复位释放时,若释放信号与时钟边沿太接近,可能违反触发器的时序要求(恢复时间/移除时间)。

状态不一致:写指针和读指针可能在复位释放后不同步,导致FIFO的空满判断错误(如误判为空或满)。

异步复位同步释放的核心是通过两级触发器对复位信号的释放进行同步,确保复位信号在目标时钟域中稳定撤销。以下是一个典型的异步复位同步释放电路:

module async_reset_sync_release (

input wire clk, // 目标时钟

input wire async_rst, // 异步复位输入

output wire sync_rst // 同步释放后的复位输出

);

reg rst_ff1, rst_ff2;

always @(posedge clk or negedge async_rst) begin

if (~async_rst) begin

rst_ff1 <= 1'b0;

rst_ff2 <= 1'b0;

end else begin

rst_ff1 <= 1'b1;

rst_ff2 <= rst_ff1;

end

end

assign sync_rst = rst_ff2;

endmodule

异步复位阶段:

当 async_rst 有效时,rst_ff1 和 rst_ff2 立即被置位,输出 sync_rst 立即生效。

同步释放阶段:

当 async_rst 撤销时,rst_ff1 和 rst_ff2 需要等待 clk 的上升沿才能依次清零。

经过两个时钟周期后,sync_rst 同步释放,确保复位撤销与时钟边沿对齐,避免亚稳态。

代码对应的 RTL Viewer 视图:

将异步复位同步释放电路添加到前面的异步fifo工程当中:

对应的RTL Viewer视图如下:

六、FIFO应用实例 (ADC)

接下来将异步fifo设计成IP核,并在实际工程当中进行调用。

参考:

手写异步FIFO的FPGA 工程实例验证(基于DE10-Standard)(LTC2308)

自己设计FIFO的目的一般是为了学习一下FIFO的结构,设计思路等,如果是一般的项目设计 ,建议可以直接调用厂商提供的FIFO IP 进行简单配置会不容易出错一点。

使用Quartus II软件提供的免费FIFO IP核,Quartus II软件为用户提供了友好的图形化界面方便用户对FIFO的各种参数和结构进行配置,生成的FIFO IP核针对Altera不同系列的器件,还可以实现结构上的优化。

Quartus 里面提供的FIFO可分为两种结构:单时钟FIFO(SCFIFO)和双时钟FIFO(DCFIFO), 这个实验调用DCFIFO:

1-【友晶科技Terasic】基于FPGA实现LTC2308控制器的设计——总概述