Verilog笔试题:同步FIFO,异步FIFO的实现

目录

1. FIFO简介

2. 使用场景

3. 分类

4. FIFO的常见参数

5.同步FIFO

1.基础知识:

2.同步FIFO的空满检测

6.异步FIFO


1. FIFO简介

FIFO是一种先进先出数据缓存器,它与普通存储器的区别是没有外部读写地址线,使用起来非常简单,缺点是只能顺序读写,而不能随机读写。

2. 使用场景

  1. 当输入数据速率和输出数据速率不匹配时,作为临时存储单元。例如,CPU可以先将数据写入FIFO,然后继续做其他工作,设备可以很方便地从FIFO中读取数据。再如因特网控制器,它将从网络接收来的数据存入FIFO,后端DMA(Direct Memory Access,直接存储器访问)控制器(位于PCIe或者PCI接口电路中)从FIFO中读取数据,然后写入系统存储器。
  2. 用于不同时钟域之间的同步。实际应用中,数据将不得不从一个时钟域进入另一个时钟域,此时FIFO不仅作为临时数据存储单元,也起到数据同步的作用。
  3. 输入数据路径和输出数据路径之间数据位宽不匹配时,可用于数据位宽调整电路。

3. 分类

同步FIFO:指读时钟和写时钟是同一个时钟
异步FIFO:指读写时钟是不同的时钟。

4. FIFO的常见参数

  • FIFO的宽度:即FIFO一次读写操作的数据位;
  • FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
  • 满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
  • 空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
  • 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
  • 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。

5.同步FIFO代码

1.基础知识

同步FIFO的读写控制信号以及数据均处于同一时钟域,即:FIFO在同一时钟驱动下进行读写操作,读控制信号有效且FIFO不为空时,输出读指针对应地址的数据,随后读指针加1;写控制信号有效且FIFO不为满时,将输入数据存储到写指针对应地址处,随后写指针加1;

2.同步FIFO的空满检测

读写逻辑是同一个时钟,因此可以在每次时钟来临时进行判断,如果不执行读写操作/同时读写,则计数值不变;只执行读操作,计数值减1;只执行写操作,计数值加1;

如果计数值为0,则说明FIFO空,只能写不能读(直到写入一次数据,计数值加1,FIFO不再为空才能执行读操作);

如果计数值为FIFO深度,则说明FIFO满,只能读不能写(直到读出一次数据,计数值减1,FIFO不再为满才能执行写操作)

module syn_fifo(
	clk,
	rst_n,
	data_in,
	w_en,
	full,
	data_out,
	r_en,
	empty);

parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 16;
parameter ADDR_WIDTH = 4;

input wire clk, rst_n;
input wire [DATA_WIDTH-1:0] data_in;
input wire w_en, r_en;

output wire empty, full;
output reg [DATA_WIDTH-1:0] data_out;

reg [ADDR_WIDTH   : 0] data_count;
reg [ADDR_WIDTH-1 : 0] w_ptr, r_ptr;
reg [DATA_WIDTH-1 : 0] mem[0 : DATA_DEPTH-1];

assign empty = (data_count == 'd0) ? 1 : 0;
assign full = (data_count == DATA_DEPTH) ? 1 : 0; //data_count == DATA_DEPTH

always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		data_count <= 'd0;
	else if (w_en && r_en)
		data_count <= data_count;  //边读边写
	else if (!full && w_en)     //写使能且没写满
		data_count <= data_count + 1'b1;
	else if (!empty && r_en)    //读使能且没读满
		data_count <= data_count - 1'b1;   //记录数据数量
	else
		data_count <= data_count;
end

always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		w_ptr <= 'd0;
	else if (w_en && !full)    //写地址一直加
		w_ptr <= w_ptr + 1'b1;
	else
		w_ptr <= w_ptr;
end

always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		r_ptr <= 'd0;
	else if (r_en && !empty)   //读地址一直加
		r_ptr <= r_ptr + 1'b1;
	else
		r_ptr <= r_ptr;
end

always @ (posedge clk  or negedge rst_n) begin
	if (!rst_n)
		mem[w_ptr] <= 'd0;
	else if (!full && w_en)     //写入数据
		mem[w_ptr] <= data_in;
end

always @ (posedge clk  or negedge rst_n) begin
	if (!rst_n)
		data_out <= 'd0;
	else if (!empty && r_en)  
		data_out <= mem[r_ptr];
end

endmodule

6.异步FIFO代码

1.异步FIFO的空满检测

1)计数检测空满:异步FIFO不能采用同步FIFO这种计数方式来实现空满检测,因为用两个时钟去控制同一个计数器的加剪很明显是不可取的。

2)指针比较检测空满:读写指针指向读写操作面向的FIFO地址空间,因此空满检测的另一个思路是比较读写指针。每次写操作执行,写指针加1;而每次读操作执行,读指针加1,因此:

FIFO空发生在:读指针追上写指针时;

FIFO满发生在:写指针追上读指针时;

但是这种处理仍存在一个问题,就是读写指针相等时,难以区分FIFO是空还是满。

具体来说:当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时,或者当读指针读出FIFO中最后一个字后,追赶上了写指针时,如下图所示:

当读写指针再次相等时,表明FIFO为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around)又追上了读指针,如下图:

3)扩展指针比较检测空满:
如上分析,直接比较读写指针时存在一个特例:读写指针相等时,难以区分FIFO是空还是满。

因此,有人提出,将指针进行高位扩展。即指针加宽一位,当写指针超出FIFO深度时,这额外的一位就会改变。FIFO的深度决定了指针扩展前(即除了最高位的其余位)的宽度,而这扩展的一位与FIFO深度无关,是为了标志指针多转了一圈,因此:

当读写指针完全相同时,FIFO空;

当读写指针高位不同,其余位完全相同时,FIFO满;

对于深度为2^n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器。

4)格雷码指针比较检测空满:

经过指针扩展,可以明确的进行空满检测。但是这种处理仍然不够,因为异步FIFO读写时钟相互独立,分属不同时钟域,相邻二进制地址位变化时,不止一个地址位都要变化,这样指针在进行同步过程中很容易出错,比如写指针在从0111到1000跳变时4位同时改变,这样读时钟在进行写指针同步后得到的写指针可能是0000-1111的某个值,一共有2^4个可能的情况,而这些都是不可控制的,你并不能确定会出现哪个值,那出错的概率非常大。如上分析,直接比较扩展读写指针时可能因多位改变导致错误。因此,进一步采用gray码形式的指针,利用格雷码每次只变化一位的特性,降低同步发生时错误的概率。

格雷码指针下的空满检测条件为:

当最高位和次高位均相同,其余位相同:FIFO空

当最高位和次高位均相反,其余位相同:FIFO满

因此,最终的空满检测方式为:将二进制指针转换为格雷码,用于另一时钟域接收,随后按照检测条件进行检测。

二进制码转格雷码:

代码:

assign w_ptr_gray = w_ptr ^ (w_ptr >> 1);   //读写地址转为格雷码
assign r_ptr_gray = r_ptr ^ (r_ptr >> 1);  

2.异步FIFO的同步处理

异步FIFO通过比较读写指针进行满空判断,但是读写指针属于不同的时钟域,所以在比较之前需要先将读写指针进行同步处理(两级寄存器打两拍的方式进行同步),将读写指针同步后再进行比较,判断FIFO空满状态。即,判断FIFO空满状态时,需要在读FIFO时获取写时钟域的写指针,与读指针比较来判断FIFO是否为空;需要在写FIFO时获取读时钟域的读指针,与写指针比较来判断FIFO是否为满。

但是因为在同步指针时需要时间(如延迟两拍同步),而在这个同步的时间内有可能还会写入/读出新的数据,因此同步后的指针一定是小于或者等于当前实际的读/写指针,那么此时判断FIFO满空状态时是否会出错?是否会导致错误?异步逻辑进行同步时,不可避免需要额外的时钟开销,这会导致满空趋于保守,但是保守并不等于错误,这么写会稍微有性能损失,但是不会出错。

 //异步FIFO设计文件
 //使用扩展地址位的方式来判断空满  
 //读写信信号时钟不同,存在跨时钟域的操作(地址位跨时钟域传递)
 //关键:格雷码的使用
 //问题:跨时钟域的信号的处理方法
 //单位宽的信号跨时钟域,要分为从快时钟域跨到慢时钟域
 //还是慢时钟域跨到快时钟域这两种情况
 //单位宽:clk1--clk2(频率可能不相等,相位也是不确定的)
 //1、慢--快:通过两级级联D触发器构成的同步器就可以实现慢到快时钟域的转换
 //慢信号肯定能被快信号采到 
 //2、快--慢:不能用上面的,因为快时钟域里面的信号比较窄,不一定会被采到
 //2、快--慢:方法、一、要不产生一个握手信号,快时钟域要等到慢时钟域采到
 //之后告诉我一个信号,才结束本次传递
 //方法二、把脉冲转化为跳变的电平
 //多位宽的data信号,用异步FIFO来做
 //多位宽的counter计数值,通过转换格雷码以后,把格雷码通过同步器就可以了
 //格雷码即使出错了,也可以认为地址还是在上一个值
 
module fifo(rclk, wclk, rst, wr_en, rd_en, rd_en, data_in, data_out, empty, full, halffull);
 
input rclk, wclk, rst, wr_en, rd_en;
input [7:0] data_in;
output empty, full, halffull;  //这里的halffull半满信号没有用到
output [7:0] data_out;

reg  halffull;
reg [7:0] mem[15:0];  //16*8 RAM,深度为16
reg [7:0] data_out;
reg [4:0] w_addr_a, r_addr_a;  //加了标志指针的那一位,扩展成5位
wire [4:0] w_addr_b, r_addr_b;  //gray addr格雷码地址
reg [4:0] w_addr0_b, r_addr0_b;  //打拍一次的信号
reg [4:0] w_addr_r, r_addr_w;  //打拍两次的信号
 
assign w_addr_b = w_addr_a ^ (w_addr_a >> 1);  //写地址转成格雷码
assign r_addr_b = r_addr_a ^ (r_addr_a >> 1);  //读地址转成格雷码

//判断空满的时候用的地址是格雷码转换过来且同步过来的地址
//比较原则:一定要同码比较,要么就把传递过来的格雷码转换到二进制码地址再跟本时钟域的二进制码比较,要么就是把传递过来的格雷码跟本时钟域二进制码地址转换后的格雷码进行比较(就是同时钟域,且要么同格雷码地址要么同二进制码进行比较)
//从写时钟域同步到读时钟域的写地址和自身的地址进行判断
//和同步FIFO一样,如果所有位相同就是empty,如果高位相反,低位相同就是满
assign empty=(r_addr_b == r_addr_w)?1:0;  //w_addr_b
assign full=({(~w_addr_b[4]),w_addr_b[3:0]} == w_addr_r) ? 1:0;


always @(posedge rclk or negedge rst)
 begin
    if(!rst)
        begin
            r_addr_a <= 5'b0;  //读时钟域下的读地址
            r_addr_w <= 5'b0;  //从写时钟域下同步到读时钟域的写地址
        end
    else
        begin
            //把写地址转成格雷码以后同步到读时钟域上去
            //为什么要使用两级非阻塞赋值,即两个D触发器构成同步器?注意:两级寄存器并不能完全消除亚稳态危害,但是会大大提高可靠性,减少发生的概率。
            //一级寄存器的亚稳态发生的概率太大,三级寄存器消除亚稳态的概率比二级提升不多。
            w_addr0_b <= w_addr_b;  //把格雷码信号锁存起来,需要两排  
            r_addr_w <= w_addr0_b;  //sample write_addr
            if(rd_en == 1 && empty == 0)  //读有效且空无效
                begin
                    data_out <= mem[r_addr_a];  
        //读的时候memory操作的不能是格雷码的,一定是二进制且没扩展的地址
                    r_addr_a <= r_addr_a+1; 
        //每次读的时候,读时钟域的地址要加1
                end
        end
end

always @(posedge wclk or negedge rst)
begin
    if(!rst)
        begin
            w_addr_a <= 5'b0;  //写时钟域下自身的写地址
            w_addr_r <= 5'b0;  //从读时钟域下同步到写时钟域的读地址
        end
    else
        begin
            r_addr0_b <= r_addr_b;  //把格雷码信号锁存起来,需要两排
            w_addr_r <= r_addr0_b;  //sample read_addr
        if(wr_en == 1 && full ==0)  //写有效且满无效
            begin
                mem[w_addr_a] <= data_in;  
        //相应的要写的内容写到地址上去,写时钟域里面自身的地址,不是转换的
                w_addr_a <= w_addr_a +1;  //写地址自加1
            end
        end
end

endmodule

7.参考文献 

同步FIFO和异步FIFO的Verilog语言的实现,并附有详细的代码注释_fifo算法代码加注释-CSDN博客

Verilog实现FIFO专题5-异步FIFO设计(异步FIFO工作方式、异步FIFO介绍、异步FIFO介绍)_verilog参考文献-CSDN博客

【FPGA——基础篇】同步FIFO与异步FIFO——Verilog实现「建议收藏」-腾讯云开发者社区-腾讯云 (tencent.com)

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值