个人知乎文章搬运,欢迎关注h700 - 知乎电子科技大学 硕士研究生 回答数 0,获得 2 次赞同https://www.zhihu.com/people/h700-87知乎文章链接(verilog)一步步带你手写异步FIFO - 知乎FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器,没有外部读写地址线,使用起来非常简单,只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以…
https://zhuanlan.zhihu.com/p/545512508
FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器,没有外部读写地址线,使用起来非常简单,只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。也正是由于这个特性,使得FIFO可以用作跨时钟域数据传输和数据位宽变换。
本篇文章分析异步FIFO的实现原理并用verilog手写实现异步FIFO。
一、双端口RAM
FIFO中用来存储数据的器件为双口RAM,我们首先搭建一个Dual Ram(双口RAM)。我们以一个深度为16,数据位宽为8的Dual Ram为例,框图和时序如下。
Dual Ram读端和写端采用两个时钟,可以实现读写时钟为异步时钟,也可以实现读写同时进行的功能。代码实现如下:
声明端口与内部信号
module Dual_Ram#(parameter ADDR_WIDTH=4 ,DATA_WIDTH=8)( input wclk,input rclk, input wr_en,input rd_en, input [ADDR_WIDTH-1:0] wr_addr, input [ADDR_WIDTH-1:0] rd_addr, input [DATA_WIDTH-1:0] wr_data, output reg [DATA_WIDTH-1:0] rd_data); //输入数据打一拍 reg [DATA_WIDTH-1:0] wdin_d1,waddr_d1,raddr_d1; reg wen_d1,ren_d1; //输出数据打一拍 reg [DATA_WIDTH-1:0] rdout; //数据寄存 reg [DATA_WIDTH-1:0] DQ [2**ADDR_WIDTH-1:0];
插入寄存器及读写操作
//输入打一拍 always@(posedge wclk) begin wdin_d1 <= wr_data; waddr_d1 <= wr_addr; wen_d1 <= wr_en; raddr_d1 <= rd_addr; ren_d1 <= rd_en; end //读 always@(posedge wclk) begin if(wen_d1) DQ[waddr_d1] <= wdin_d1; end //写 always@(posedge rclk) begin if(ren_d1) rdout <= DQ[raddr_d1]; end //输出打一拍 always@(posedge rclk) begin rd_data <= rdout; end endmodule
如此就实现了一个双口RAM,下一步就在此基础上思考如何继续实现异步fifo
二、FIFO地址设计
我们知道FIFO中是没有地址线的,地址靠自身计数器自加1来控制,那么我们很容易想到把外部输入信号wr_addr和rd_addr换成内部信号并用计数器来控制其自加,计数器加满之后直接清零,从0重新开始写/读,循环往复。由于写端和读端的时钟速率不同,就会有快慢的问题,那么就出现了一个问题,以地址3为例,写入的数据还没有被读出,又被新的数据覆盖了,造成数据丢失;或者写入的数据已经被读出,新的数据还没有写进来,地址3的老数据又被读了一遍,造成数据重复。
为了解决上述问题,我们引入 full 和 empty 信号来表示内部RAM中的数据写满或者读空,新的框图如下所示。
如何产生full和empty信号呢,我们可以用 wr_addr 和 rd_addr 来做判断,当 wclk 大于 rclk 时,会产生写满的情况,如下图中黄色部分代表已经写入数据,还未被读取,白色代表数据已被读取,图1中当 waddr>raddr时,waddr-raddr → 1111 - 0001 = 1110 可以表示两者的差值。
图2中当 waddr<raddr 时,计算两者的差值为16 – raddr + waddr → 10000 - 1100 +1010 = 1110,此时的 waddr – raddr → 1010-1100 →1010+0011+0001=1110,两者结果相同,所以无论 waddr 大于 raddr 还是小于 raddr,都可以用 waddr-raddr 来表示写比读多几个数据。此时再引入一个full_limit用来设置一个写满的阈值。当waddr – raddr >= full_limit 时,full信号拉高,停止写入。
同理,读比写快的情况下引入一个empty_limit来作为读空的阈值,当 waddr – raddr <= empty_limit 时。empty信号拉高,停止读出。在实际工程中可以根据实际需要和 fifo 的设计区别灵活设置 full_limit 和empty_limit 的数值,在本次设计中另 full_limit = 15,empty_limit = 1。
三、二进制转格雷码
但是由于waddr和raddr在两个时钟域,两个信号在做减法的时候需要进行跨时钟域操作,在时钟域同步时可能会产生漏采,采重和亚稳态的情况,从而产生错误的空满状态。
首先考虑亚稳态情况,由于二进制的读写指针会产生多位同时跳变,直接用二进制同步会产生错误的中间值,错误结果不可控。如下图所示,从地址7跳变到地址8的过程,出现亚稳态后会产生24个结果(每一位都有采到0和1的可能)。
换成格雷码之后,由于格雷码每一次只改变一位bit,数据出现亚稳态之后,产生的结果也只能是正确结果或正确结果的前一个值,比如地址7跳变到地址8,0100 → 1100,出现亚稳态后可能的结果为 1100 或 0100,即保持地址7不变,或成功跳到地址8。由于fifo中写地址和读地址都是依次增加的,所以如果出现亚稳态,则代表着读写地址正常加1,或者不增加,错误结果是可控的。
二进制转格雷码:二进制的最高位作为格雷码的最高位,次高位的格雷码为二进制的高位和次高位相异或得到,其他位与次高位相同。
Verilog实现:
assign wr_gray = (wr_addr>>1) ^ wr_addr; assign rd_gray = (rd_addr>>1) ^ rd_addr;
格雷码转二进制:使用格雷码的最高位作为二进制的最高位,二进制次高位产生过程是使用二进制的高位和次高位格雷码相异或得到,其他位的值与次高位产生过程相同。
Verilog实现:
genvar i; generate for(i=0;i<3;i=i+1)begin:wg2b assign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d3[i]; end endgenerate assign rd_bin[3] = rd_gray_d3[3]; genvar j; generate for(j=0;j<3;j=j+1)begin:rg2b assign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d3[j]; end endgenerate
四、跨时钟域同步
再考虑如何避免漏采和重采,首先考虑一个问题,地址同步要在哪个时钟域进行呢,我们所期望的结果是慢时钟地址同步到快时钟域,以免发生快时钟域信号漏采导致的读空或者写满。至于重采的情况,即慢时钟域信号被多采了一次,只会在判断空满状态时更安全,不会导致读空和写满这种不安全现象的出现。不过这样会产生虚假的full和empty信号,即full信号已经拉高,但ram中仍存有可用的地址,或者empty信号已经拉高,但ram中仍存有可被读出的数据。虽然效率和资源上有一点浪费,但不会发生丢失数据或读错数据的不安全行为。
那怎么实现慢时钟域的信号同步到快时钟域呢?因为若同时读写时出现 empty 则一定是读时钟快于写时钟,所以在判断 empty 状态时,读时钟域为快时钟,把较慢的写时钟同步到读时钟域来判断 empty。同理,若同时读写时出现 full 则一定是写时钟快于读时钟,所以在判断 full 状态时,写时钟域为快时钟,把较慢的读时钟同步到写时钟域来判断 full。以判断empty状态为例,过程如下图所示:
其中B2G模块(二进制转格雷码),G2B模块(格雷码转二进制),empty判断模块均为组合逻辑,所以加一级D触发器以满足时序。蓝色圈中的两级D触发器用作消除跨时钟域同步的亚稳态。empty信号在RCLK快于WCLK时产生,中间虽然加入了四级D触发器,导致写地址同步到读时钟域时是之前的老地址,这和之前采重的问题一样,只会让empty的判断更安全,但会造成少许的资源浪费,属于保守但安全的做法。
至此,一个简易的异步fifo就被设计出来了,总体框图如下。
全部verilog代码:
声明端口与内部信号
module a_fifo#(parameter ADDR_WIDTH=4 ,DATA_WIDTH=8,empty_limit=1'b1,full_limit=4'd15)( input wclk, input wr_en, input [DATA_WIDTH-1:0] wr_data, input rclk, input rd_en, input wrst_n, input rrst_n, output reg [DATA_WIDTH-1:0] rd_data, output reg empty, output reg full ); //输入信号打一拍 reg [DATA_WIDTH-1:0] wr_data_d1; reg wen_d1; reg ren_d1; //输出数据打一拍 reg [DATA_WIDTH-1:0] rdout; //ram寄存器 reg [DATA_WIDTH-1:0] DQ [2**ADDR_WIDTH-1:0]; //写入/读取指针 reg [ADDR_WIDTH-1:0] wr_addr,rd_addr; //二进制转格雷码 wire [ADDR_WIDTH-1:0] wr_gray,rd_gray; reg [ADDR_WIDTH-1:0] rd_gray_d1,wr_gray_d1; //格雷码转二进制 wire [ADDR_WIDTH-1:0] wr_bin,rd_bin; reg [ADDR_WIDTH-1:0] rd_gray_d2,rd_gray_d3; reg [ADDR_WIDTH-1:0] wr_gray_d2,wr_gray_d3; reg [ADDR_WIDTH-1:0] rd_bin_d1,wr_bin_d1; //empty判断 wire full_logic; //full判断 wire empty_logic;
读写操作
//输入数据打一拍 always@(posedge wclk) begin wr_data_d1 <= wr_data; wen_d1 <= wr_en; ren_d1 <= rd_en; end //写地址 always@(posedge wclk) begin if(~wrst_n) wr_addr <= 'd0; else if(wen_d1 && ~full) if (wr_addr == 'd15) wr_addr <= 'd0; else wr_addr <= wr_addr + 1'b1; end //读地址 always@(posedge rclk) begin if(~rrst_n) rd_addr <= 'd0; else if(ren_d1 && ~empty) if (rd_addr == 'd15) rd_addr <= 'd0; else rd_addr <= rd_addr + 1'b1; end //写 always@(posedge wclk) begin if(wen_d1 && ~full) DQ[wr_addr] <= wr_data_d1; end //读 always@(posedge rclk) begin if(ren_d1 && ~empty) rdout <= DQ[rd_addr]; end always@(posedge rclk) begin rd_data <= rdout; end
格雷码转换,跨时钟域及full、empty判断
//二进制转格雷码 assign wr_gray = (wr_addr>>1) ^ wr_addr; assign rd_gray = (rd_addr>>1) ^ rd_addr; always@(posedge wclk) wr_gray_d1 <= wr_gray; always@(posedge rclk) rd_gray_d1 <= rd_gray; //格雷码转二进制 always@(posedge wclk) begin rd_gray_d2 <= rd_gray_d1; rd_gray_d3 <= rd_gray_d2; end always@(posedge rclk) begin wr_gray_d2 <= wr_gray_d1; wr_gray_d3 <= wr_gray_d2; end assign wr_bin[ADDR_WIDTH-1] = wr_gray_d3[ADDR_WIDTH-1]; genvar i; generate for(i=0;i<ADDR_WIDTH-1;i=i+1)begin:wg2b assign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d3[i]; end endgenerate assign rd_bin[ADDR_WIDTH-1] = rd_gray_d3[ADDR_WIDTH-1]; genvar j; generate for(j=0;j<ADDR_WIDTH-1;j=j+1)begin:rg2b assign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d3[j]; end endgenerate always@(posedge wclk) rd_bin_d1 <= rd_bin; always@(posedge wclk) wr_bin_d1 <= wr_bin; //empty assign empty_logic = ((wr_bin_d1 - rd_addr) <= empty_limit)? 1'b1 : 1'b0; always@(posedge rclk) empty <= empty_logic; //full assign full_logic = ((wr_addr - rd_bin_d1) >= full_limit)? 1'b1 : 1'b0; always@(posedge rclk) full <= full_logic; endmodule
功能仿真波形图