异步FIFO设计

📅 2026/7/3 5:43:02
异步FIFO设计
一、题目分析本题要求设计一个异步FIFO模块底层仍然使用双口RAM作为存储单元。和同步FIFO不同的是异步FIFO的写端和读端工作在不同的时钟域中也就是写操作由wclk控制读操作由rclk控制。因此异步FIFO除了要实现先进先出的数据缓存功能还需要解决跨时钟域传递读写指针的问题。从接口上看异步FIFO主要包括写端信号、读端信号和状态信号。写端包括wclk、wrstn、winc和wdata其中winc表示写使能wdata表示写入数据读端包括rclk、rrstn和rinc其中rinc表示读使能输出信号包括wfull、rempty和rdata分别表示写满、读空和读出的数据。异步FIFO常用于两个不同时钟域之间的数据传输。例如一个模块工作在100MHz另一个模块工作在50MHz二者不能直接用普通寄存器传递多bit数据否则很容易出现数据采样错误。异步FIFO的作用就是在两个时钟域之间加一个缓冲区让写端按自己的时钟写数据读端按自己的时钟读数据。二、FIFO基本原理FIFO的基本含义仍然是先进先出。先写入FIFO的数据会先被读出来后写入的数据会排在后面。硬件中通常不会真的做一个“队列移动”而是用RAM存储数据再用读写指针控制数据的位置。在本设计中双口RAM负责真正的数据存储。写端通过wclk、wenc、waddr和wdata把数据写入RAM读端通过rclk、renc、raddr和rdata从RAM中读出数据。因为RAM本身支持不同的读写时钟所以很适合作为异步FIFO的存储部分。需要注意的是双口RAM只负责根据地址读写数据它并不知道FIFO什么时候空也不知道FIFO什么时候满。因此异步FIFO真正的重点仍然是读写指针控制和空满判断。写指针waddr表示下一次写入的位置读指针raddr表示下一次读取的位置。只要读写指针维护正确FIFO就能保证数据按照写入顺序依次读出。异步FIFO和同步FIFO最大的区别在于写指针在wclk时钟域中变化读指针在rclk时钟域中变化。写端要判断满就必须知道读指针的位置读端要判断空就必须知道写指针的位置。也就是说读写指针需要跨时钟域传递这就是异步FIFO设计的核心问题。三、设计思路拆解本设计整体可以分成四个部分第一部分是双口RAM用来保存数据第二部分是写地址控制逻辑在wclk时钟域下控制waddr递增第三部分是读地址控制逻辑在rclk时钟域下控制raddr递增第四部分是指针同步与空满判断逻辑。写控制逻辑比较直接。当wrstn复位时写地址waddr清零当winc有效并且FIFO没有满时说明当前可以写入数据于是写地址加1同时将wdata写入RAM。这里必须用wfull限制写操作如果FIFO已经满了还继续写就会覆盖还没有被读出的数据。读控制逻辑和写控制逻辑类似。当rrstn复位时读地址raddr清零当rinc有效并且FIFO不是空时说明当前可以读出数据于是读地址加1同时从RAM中读取对应地址的数据。这里必须用rempty限制读操作如果FIFO已经空了还继续读就会读到无效数据。为了进行空满判断代码中不仅定义了二进制读写地址waddr和raddr还将它们转换成了格雷码指针wptr_gray和rptr_gray。这样做的原因是读写指针需要跨时钟域同步如果直接同步二进制指针多个bit可能同时变化另一时钟域采样时容易采到错误的中间值。格雷码的特点是相邻两个数之间只有1bit变化更适合跨时钟域同步。二进制转格雷码转换规则是1)最高位保留不变2)格雷码其余位为二进制码对应位与其前一位的异或。用代码表述就是二进制码B[n:0],格雷码G[n:0];G[n] B[n];//最高位不变G[n-1 : 0] B[n-1 : 0] ^ B[n : 1];将二进制数右移一位后与原二进制数相异或便可得到格雷码。写指针wptr_gray需要同步到读时钟域用来判断读端是否为空读指针rptr_gray需要同步到写时钟域用来判断写端是否满。代码中使用两级寄存器进行同步目的就是降低亚稳态带来的影响。这也是异步FIFO中非常常见的处理方式。四、Verilog代码实现timescale 1ns/1ns /***************************************RAM*****************************************/ module dual_port_RAM #( parameter DEPTH 16, parameter WIDTH 8 )( input wclk, input wenc, input [$clog2(DEPTH)-1:0] waddr, input [WIDTH-1:0] wdata, input rclk, input renc, input [$clog2(DEPTH)-1:0] raddr, output reg [WIDTH-1:0] rdata ); reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1]; always (posedge wclk) begin if(wenc) RAM_MEM[waddr] wdata; end always (posedge rclk) begin if(renc) rdata RAM_MEM[raddr]; end endmodule /***************************************AFIFO*****************************************/ module asyn_fifo #( parameter WIDTH 8, parameter DEPTH 16 )( input wclk, input rclk, input wrstn, input rrstn, input winc, input rinc, input [WIDTH-1:0] wdata, output wire wfull, output wire rempty, output wire [WIDTH-1:0] rdata ); localparam ADDR_WIDTH $clog2(DEPTH); reg [ADDR_WIDTH:0] waddr; reg [ADDR_WIDTH:0] raddr; wire write_en; wire read_en; assign write_en winc ~wfull; assign read_en rinc ~rempty; /******************************** RAM例化 ********************************/ dual_port_RAM #( .DEPTH(DEPTH), .WIDTH(WIDTH) ) dual_port_RAM_U0 ( .wclk (wclk), .wenc (write_en), .waddr (waddr[ADDR_WIDTH-1:0]), .wdata (wdata), .rclk (rclk), .renc (read_en), .raddr (raddr[ADDR_WIDTH-1:0]), .rdata (rdata) ); /******************************** 读写地址控制 ********************************/ always (posedge wclk or negedge wrstn) begin if(~wrstn) waddr b0; else if(write_en) waddr waddr 1b1; else waddr waddr; end always (posedge rclk or negedge rrstn) begin if(~rrstn) raddr b0; else if(read_en) raddr raddr 1b1; else raddr raddr; end /******************************** 格雷码指针 ********************************/ wire [ADDR_WIDTH:0] wptr_gray; wire [ADDR_WIDTH:0] rptr_gray; reg [ADDR_WIDTH:0] wptr_gray_r_1; reg [ADDR_WIDTH:0] wptr_gray_r_2; reg [ADDR_WIDTH:0] rptr_gray_r_1; reg [ADDR_WIDTH:0] rptr_gray_r_2; assign wptr_gray waddr ^ (waddr 1); assign rptr_gray raddr ^ (raddr 1); /******************************** 写指针同步到读时钟域 ********************************/ always (posedge rclk or negedge rrstn) begin if(~rrstn) begin wptr_gray_r_1 b0; wptr_gray_r_2 b0; end else begin wptr_gray_r_1 wptr_gray; wptr_gray_r_2 wptr_gray_r_1; end end /******************************** 读指针同步到写时钟域 ********************************/ always (posedge wclk or negedge wrstn) begin if(~wrstn) begin rptr_gray_r_1 b0; rptr_gray_r_2 b0; end else begin rptr_gray_r_1 rptr_gray; rptr_gray_r_2 rptr_gray_r_1; end end /******************************** 空满判断 ********************************/ assign rempty (rptr_gray wptr_gray_r_2); assign wfull (wptr_gray[ADDR_WIDTH] ! rptr_gray_r_2[ADDR_WIDTH]) (wptr_gray[ADDR_WIDTH-1] ! rptr_gray_r_2[ADDR_WIDTH-1]) (wptr_gray[ADDR_WIDTH-2:0] rptr_gray_r_2[ADDR_WIDTH-2:0]); endmodule这段代码中双口RAM部分和同步FIFO中的RAM基本一致区别主要体现在外层FIFO控制逻辑。异步FIFO中写地址只在wclk时钟域变化读地址只在rclk时钟域变化所以不能直接拿另一个时钟域的指针来判断空满必须先把指针转换成格雷码再同步到对方时钟域。代码中的write_en和read_en分别用于保护写操作和读操作。write_en只有在winc有效并且wfull为0时才有效read_en只有在rinc有效并且rempty为0时才有效。这样可以防止FIFO满时继续写也可以防止FIFO空时继续读。五、满空判断原理详解异步FIFO的满空判断比同步FIFO多了一步就是跨时钟域同步指针。同步FIFO中读写都在同一个clk下因此waddr和raddr可以直接比较而异步FIFO中waddr属于写时钟域raddr属于读时钟域二者不能直接比较否则会出现跨时钟域采样问题。为了解决这个问题常见做法是先将二进制指针转换成格雷码。转换公式为gray bin ^ (bin 1)。格雷码的特点是相邻两个数之间只有1bit发生变化。这样在跨时钟域同步时即使采样点刚好落在变化附近也最多只有1bit存在不确定不会像二进制计数那样多个bit同时变化风险更小。读空信号复位的时候读指针和写指针相等读空信号有效当读指针赶上写指针的时候写指针等于读指针意味着最后一个数据被读完此时二进制两个数相等格雷码也是相同 读空信号有效。在异步FIFO中空判断发生在读时钟域。读端想知道FIFO是不是空需要知道写端当前写到了哪里所以写指针wptr_gray需要先同步到rclk时钟域。代码中wptr_gray_r_1和wptr_gray_r_2就是同步后的写指针。经过两级寄存器后在读时钟域中就可以用本地读指针rptr_gray和同步过来的写指针wptr_gray_r_2进行比较。写满信号当写指针比读指针多一圈时写指针等于读指针意味着写满了此时二进制最高位不同(即“多了一圈”后)格雷码的最高位和次高位不相同其余低位都相同写满信号有效。满判断发生在写时钟域。写端想知道FIFO是不是满需要知道读端当前读到了哪里所以读指针rptr_gray需要同步到wclk时钟域。代码中rptr_gray_r_1和rptr_gray_r_2就是同步后的读指针。写端将本地写指针wptr_gray和同步过来的读指针rptr_gray_r_2进行比较从而判断FIFO是否已经写满。满判断的本质仍然是写指针比读指针多绕了一圈。对于二进制扩展指针来说满状态可以理解为低位地址相同、最高位相反。但由于这里使用的是格雷码所以满状态的比较方式会稍微变一下格雷码满判断时需要最高两位相反低位相同。代码中的满判断如下assign wfull (wptr_gray[ADDR_WIDTH] ! rptr_gray_r_2[ADDR_WIDTH]) (wptr_gray[ADDR_WIDTH-1] ! rptr_gray_r_2[ADDR_WIDTH-1]) (wptr_gray[ADDR_WIDTH-2:0] rptr_gray_r_2[ADDR_WIDTH-2:0]);这段逻辑可以拆成两部分理解。前两行表示格雷码指针的最高两位相反最后一行表示剩余低位相同。满足这个条件时就说明写指针已经比读指针多绕了一圈FIFO已经满了。以DEPTH 8为例RAM地址位宽是3位扩展后的指针位宽是4位。假设读指针二进制值为0_101写指针如果多绕一圈后到达同一个RAM地址则二进制值为1_101。二者转换成格雷码后并不是简单的最高1位相反而是表现为最高两位相反、低位相同。因此在异步FIFO的格雷码满判断中通常判断最高两位取反低位保持一致。这里还要注意一点异步FIFO中的空满信号一般会有一定延迟因为读写指针需要经过两级寄存器同步。例如写端已经写入了数据读端并不会在同一个瞬间立刻知道而是需要等写指针同步到读时钟域后rempty才会更新。这种延迟是正常的异步FIFO设计中宁愿状态信号保守一点也不能出现错误读写。因此异步FIFO的空满判断可以总结为读空在读时钟域判断用本地读指针和同步过来的写指针比较写满在写时钟域判断用本地写指针和同步过来的读指针比较。由于跨时钟域传递的是多bit指针所以必须先转成格雷码再进行两级同步。六、总结本题实现的是一个典型的异步FIFO结构。相比同步FIFO异步FIFO的难点不在RAM读写本身而在于读写指针处于不同时钟域不能直接比较。因此需要引入格雷码和两级同步寄存器保证指针跨时钟域传递时更加可靠。整个设计可以简单概括为双口RAM负责存储数据二进制读写地址负责控制RAM访问格雷码指针负责跨时钟域同步wfull和rempty负责限制读写操作。写端只关心是否满读端只关心是否空两端通过同步后的格雷码指针来间接了解对方的位置。