FIFO设计
🧊

FIFO设计

Tags
IC设计
FPGA
Published
July 31, 2024
Author
内容摘要
标签
是否记得
在Verilog硬件描述语言中,FIFO广泛应用于不同时钟域之间的数据传输、数据缓冲、数据流控制等场景。

用途

🧐
用途1:异步FIFO读写分别采用相互异步的不同时钟。在现代集成电路芯片中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,多时钟域带来的一个问题就是,如何设计异步时钟之间的接口电路。异步FIFO是这个问题的一种简便、快捷的解决方案,使用异步FIFO可以在两个不同时钟系统之间快速而方便地传输实时数据。
🧐
用途2:对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。

分类

🧐
同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。
🧐
异步FIFO是指读写时钟不一致,读写时钟是互相独立的。

常见参数

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

读写指针

🧐
读指针:总是指向下一个将要被写入的单元,复位时,指向第1个单元(编号为0)。
🧐
写指针:总是指向当前要被读出的数据,复位时,指向第1个单元(编号为0)。

FIFO设计要求

  • FIFO 深度、宽度参数化,输出空、满状态信号,并输出一个可配置的满状态信号。当 FIFO 内部数据达到设置的参数数量时,拉高该信号。
  • 输入数据和输出数据位宽可以不一致,但要保证写数据、写地址位宽与读数据、读地址位宽的一致性。例如写数据位宽 8bit,写地址位宽为 6bit(可以指向64个数据)。如果输出数据位宽要求 32bit,则输出地址位宽应该为 4bit(16 个数据)。
😲
这句话的意思是:写入数据位宽位,地址位宽位,则地址可以指向个数据,也就代表可以写入个数据;这这时从输出来看,如果要求数据为宽位,则只能对应个数据,则输出地址位宽只能为位。
  • FIFO 是异步的,即读写控制信号来自不同的时钟域。输出空、满状态信号之前,读写地址信号要用格雷码做同步处理,通过减少多位宽信号的翻转来减少打拍法同步时数据的传输错误。
二进制转格雷
二进制转格雷
格雷转二进制
格雷转二进制
😲
为什么要用格雷?因为在FIFO读写操作中,读地址和写地址需要保持同步。但由于时钟延迟等因素,可能会导致这两个计数器暂时失去同步,从而产生暂存错误。格雷码具有相邻代码只有一位不同的特性,可以减少由于计数器同步问题导致的错误。
如何用verilog设计实现格雷码转换与同步
在这个FIFO模块中,格雷码同步是通过将写指针和读指针转换为格雷码,然后在不同的时钟域中进行同步来实现的。具体步骤如下:

1. 写指针和读指针的格雷码转换

首先,将写指针和读指针转换为格雷码。格雷码的特点是相邻两个数之间只有一位不同,这样可以减少跨时钟域传输时的元数据竞争问题。

写指针的格雷码转换

wire [AWI:0] wptr = ({wover_flag, waddr} >> 1) ^ ({wover_flag, waddr});
这里,{wover_flag, waddr}是写指针的二进制表示,通过右移一位后与自身异或操作,得到写指针的格雷码表示。

读指针的格雷码转换

wire [AWI-1:0] raddr_ex = raddr << EXTENT_BIT; wire [AWI:0] rptr = ({rover_flag, raddr_ex} >> 1) ^ ({rover_flag, raddr_ex});
这里,{rover_flag, raddr_ex}是读指针的二进制表示,通过右移一位后与自身异或操作,得到读指针的格雷码表示。

2. 格雷码指针的同步

将写指针的格雷码同步到读时钟域,将读指针的格雷码同步到写时钟域。

写指针同步到读时钟域

reg [AWI:0] rq2_wptr_r0, rq2_wptr_r1; always @(posedge rclk or negedge rstn) begin if (!rstn) begin rq2_wptr_r0 <= 'b0; rq2_wptr_r1 <= 'b0; end else begin rq2_wptr_r0 <= wptr; rq2_wptr_r1 <= rq2_wptr_r0; end end
这里,通过两个寄存器级联的方式,将写指针的格雷码同步到读时钟域。

读指针同步到写时钟域

reg [AWI:0] wq2_rptr_r0, wq2_rptr_r1; always @(posedge wclk or negedge rstn) begin if (!rstn) begin wq2_rptr_r0 <= 'b0; wq2_rptr_r1 <= 'b0; end else begin wq2_rptr_r0 <= rptr; wq2_rptr_r1 <= wq2_rptr_r0; end end
这里,通过两个寄存器级联的方式,将读指针的格雷码同步到写时钟域。

3. 格雷码解码

将同步后的格雷码指针解码回二进制形式,以便进行比较操作。

写指针的格雷码解码

reg [AWI:0] wq2_rptr_decode; integer i; always @(*) begin wq2_rptr_decode[AWI] = wq2_rptr_r1[AWI]; for (i = AWI-1; i >= 0; i = i - 1) begin wq2_rptr_decode[i] = wq2_rptr_decode[i+1] ^ wq2_rptr_r1[i]; end end
这里,通过逐位异或操作,将同步后的写指针格雷码解码回二进制形式。

读指针的格雷码解码

reg [AWI:0] rq2_wptr_decode; always @(*) begin rq2_wptr_decode[AWI] = rq2_wptr_r1[AWI]; for (i = AWI-1; i >= 0; i = i - 1) begin rq2_wptr_decode[i] = rq2_wptr_decode[i+1] ^ rq2_wptr_r1[i]; end end
这里,通过逐位异或操作,将同步后的读指针格雷码解码回二进制形式。

4. 空和满标志的生成

通过比较解码后的写指针和读指针,生成FIFO的空和满标志。

空标志

assign rempty = (rover_flag == rq2_wptr_decode[AWI]) && (raddr_ex >= rq2_wptr_decode[AWI-1:0]);
当读指针的溢出标志与解码后的写指针溢出标志相等,并且读地址大于等于解码后的写地址时,表示FIFO为空。

满标志

assign wfull = (wover_flag != wq2_rptr_decode[AWI]) && (waddr >= wq2_rptr_decode[AWI-1:0]);
当写指针的溢出标志与解码后的读指针溢出标志不相等,并且写地址大于等于解码后的读地址时,表示FIFO已满。

总结

通过上述步骤,FIFO模块实现了写指针和读指针的格雷码转换、同步和解码,从而实现了跨时钟域的数据同步。这种方式有效地避免了时钟域之间的元数据竞争问题,确保了FIFO的正确性和可靠性。

双端口RAM设计

静态 RAM 一般包括单端口 RAM (Single Port RAM,缩写为 SP RAM)、简单双端口 RAM (Simple Dual Port RAM,缩写为 SDP RAM,也叫伪双端口 RAM)和真双端口 RAM (True Dual Port RAM,缩写为 TDP RAM)。静态 RAM 的特点是存储容量相对不是很大,但是读写速度非常高,其在 FPGA 或者 ASIC 设计中都属于非常重要的器件。
动态 RAM 一般包括 SDRAM 和 DDR SDRAM。目前 DDR SDRAM 已经从 DDR1 代发展到 了DDR5 代,DDR3 和 DDR4 SDRAM 是目前市场主流的存储器,大量使用在电脑、嵌入式和 FPGA 板卡上面,其特点是存储容量非常大、但是读写速度相比于静态 RAM 略低,这一点在数据量较少的情况下尤为明显。

如何选择合适静态RAM

  1. 当我们需要读取一个配置,且这个配置只在上电的时候配置一次,其他时候不需要写操作,那么我们直接选择单端口 RAM 即可,通过一个端口要么进行写操作,要么进行读操作。
  1. 当我们需要使用 FIFO(先进先出存储器)来存储数据,就可以选择伪双端口 RAM,一个写端口,一个读端口,且读写可以同时进行。
  1. 当我们想要实现一个对 10000 节车厢的人数进行统计的功能,每节车厢有两个门,一个门仅用于上车,另一个门仅用于下车,有人上车时需要在原有的人数基础上加一,有人下车时需要在原有的人数基础上减一,每时每刻都可能有人上下车,那么要使用逻辑统计这么多节车厢的人数时,就需要有两个写端口的RAM,深度为10000(对应 10000 节车厢),这时就要选择真双端口 RAM 了。因为如果单纯的使用寄存器资源进行统计的话,仅这 10000 节车厢的计数就很可能把 FPGA 的寄存器资源消耗殆尽了。
因此,在FIFO的设计中就需要设计一个为双端口的RAM。

verilog设计

module  ramdp     #( parameter AWI = 5 , //输入地址位宽 parameter AWO = 7 , //输出地址位宽 parameter DWI = 64 , //输入数据长度 parameter DWO = 16 //输出数据长度     (         input                   CLK_WR , //写时钟         input                   WR_EN ,  //写使能         input [AWI-1:0]         ADDR_WR ,//写地址         input [DWI-1:0]         D ,      //写数据         input                   CLK_RD , //读时钟         input                   RD_EN ,  //读使能         input [AWO-1:0]         ADDR_RD ,//读地址         output reg [DWO-1:0]    Q        //读数据      );    //输出位宽大于输入位宽,求取扩大的倍数及对应的位数    parameter       EXTENT       = DWO/DWI ;    parameter       EXTENT_BIT   = AWI-AWO > 0 ? AWI-AWO : 'b1 ;    //输入位宽大于输出位宽,求取缩小的倍数及对应的位数    parameter       SHRINK       = DWI/DWO ;    parameter       SHRINK_BIT   = AWO-AWI > 0 ? AWO-AWI : 'b1;    genvar i ;    generate       //数据位宽展宽(地址位宽缩小)       if (DWO >= DWI) begin          //写逻辑,每时钟写一次          reg [DWI-1:0]         mem [(1<<AWI)-1 : 0] ;          always @(posedge CLK_WR) begin             if (WR_EN) begin                mem[ADDR_WR]  <= D ;             end          end          //读逻辑,每时钟读 DWO/DWI (对于本次程序则为4次) 次          for (i=0; i<EXTENT; i=i+1) begin             always @(posedge CLK_RD) begin                if (RD_EN) begin                   Q[(i+1)*DWI-1: i*DWI]  <= mem[(ADDR_RD*EXTENT) + i ] ;                end             end          end       end       //=================================================       //数据位宽缩小(地址位宽展宽)       else begin          //写逻辑,每时钟写DWI/DWO (对于本次程序则为4次) 次          reg [DWO-1:0]         mem [(1<<AWO)-1 : 0] ;          for (i=0; i<SHRINK; i=i+1) begin             always @(posedge CLK_WR) begin                if (WR_EN) begin                   mem[(ADDR_WR*SHRINK)+i]  <= D[(i+1)*DWO -1: i*DWO] ;                end             end          end          //读逻辑,每时钟读 1 次          always @(posedge CLK_RD) begin             if (RD_EN) begin                 Q <= mem[ADDR_RD] ;             end          end       end    endgenerate endmodule

乘方设计技巧

💡
在这里,必须着重强调一个verilog设计技巧——乘方设计
reg [63:0] mem[(1<<6)-1:0]; //1<<6 实现2的6次方
寄存器设计
这里定义了一个深度、数据位宽64位的寄存器。1<<6就可以实现乘方运算。

RAM内部寄存器配置

notion image
上图写的“写快点”与“读快点”是针对内部寄存器而言的。我们在定义寄存器时将数据位宽作为考虑因素,输入输出中谁的数据宽度小,则在定义寄存器时也以这个参数为依据。比如在上面的设计代码中,当输出数据位宽小于输入数据位宽时,我们定义寄存器的参数为reg [输出数据宽度-1:0] mem [(1<<输出地址位宽)-1:0]; 反之则类似。

计数器设计

计数器用于产生读写地址信息,位宽可配置,不需要设置结束值,让其溢出后自动重新计数即可
module ccnt #(parameter W ) ( input rstn , input clk , input en , output [W-1:0] count ); reg [W-1:0] count_r ; always @(posedge clk or negedge rstn) begin if (!rstn) begin count_r <= 'b0 ; end else if (en) begin count_r <= count_r + 1'b1 ; end end assign count = count_r ; endmodule

FIFO顶层设计

module fifo #( parameter AWI = 7 , parameter AWO = 5 , parameter DWI = 16 , parameter DWO = 64 , parameter PROG_DEPTH = 64) //programmable full ( input rstn, input wclk, input winc, input [DWI-1: 0] wdata, input rclk, input rinc, output [DWO-1 : 0] rdata, output wfull, output rempty, output prog_full ); parameter EXTENT = DWO/DWI ; parameter EXTENT_BIT = AWI-AWO ; parameter SHRINK = DWI/DWO ; parameter SHRINK_BIT = AWO-AWI ; //======================= push counter ===================== wire [AWI-1:0] waddr ; wire wover_flag ; //counter overflow ccnt #(.W(AWI+1)) //128 u_push_cnt( .rstn (rstn), .clk (wclk), .en (winc && !wfull), .count ({wover_flag, waddr}) ); //========================== pop counter ================================== wire [AWO-1:0] raddr ; wire rover_flag ; //counter overflow ccnt #(.W(AWO+1)) //128 u_pop_cnt( .rstn (rstn), .clk (rclk), .en (rinc & !rempty), //read forbidden when empty .count ({rover_flag, raddr}) ); //============================================== //small in and big out generate if (DWO >= DWI) begin : EXTENT_WIDTH //===================================== //gray code wire [AWI:0] wptr = ({wover_flag, waddr}>>1) ^ ({wover_flag, waddr}) ; //sync wr ptr reg [AWI:0] rq2_wptr_r0 ; reg [AWI:0] rq2_wptr_r1 ; always @(posedge rclk or negedge rstn) begin if (!rstn) begin rq2_wptr_r0 <= 'b0 ; rq2_wptr_r1 <= 'b0 ; end else begin rq2_wptr_r0 <= wptr ; rq2_wptr_r1 <= rq2_wptr_r0 ; end end //gray code wire [AWI-1:0] raddr_ex = raddr << EXTENT_BIT ; wire [AWI:0] rptr = ({rover_flag, raddr_ex}>>1) ^ ({rover_flag, raddr_ex}) ; //sync rd ptr reg [AWI:0] wq2_rptr_r0 ; reg [AWI:0] wq2_rptr_r1 ; always @(posedge wclk or negedge rstn) begin if (!rstn) begin wq2_rptr_r0 <= 'b0 ; wq2_rptr_r1 <= 'b0 ; end else begin wq2_rptr_r0 <= rptr ; wq2_rptr_r1 <= wq2_rptr_r0 ; end end //decode reg [AWI:0] wq2_rptr_decode ; reg [AWI:0] rq2_wptr_decode ; integer i ; always @(*) begin wq2_rptr_decode[AWI] = wq2_rptr_r1[AWI]; for (i=AWI-1; i>=0; i=i-1) begin wq2_rptr_decode[i] = wq2_rptr_decode[i+1] ^ wq2_rptr_r1[i] ; end end always @(*) begin rq2_wptr_decode[AWI] = rq2_wptr_r1[AWI]; for (i=AWI-1; i>=0; i=i-1) begin rq2_wptr_decode[i] = rq2_wptr_decode[i+1] ^ rq2_wptr_r1[i] ; end end assign rempty = (rover_flag == rq2_wptr_decode[AWI]) && (raddr_ex >= rq2_wptr_decode[AWI-1:0]); assign wfull = (wover_flag != wq2_rptr_decode[AWI]) && (waddr >= wq2_rptr_decode[AWI-1:0]) ; assign prog_full = (wover_flag == wq2_rptr_decode[AWI]) ? waddr - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1 : waddr + (1<<AWI) - wq2_rptr_decode[AWI-1:0] >= PROG_DEPTH-1; ramdp #( .AWI (AWI), .AWO (AWO), .DWI (DWI), .DWO (DWO)) u_ramdp ( .CLK_WR (wclk), .WR_EN (winc & !wfull), .ADDR_WR (waddr), .D (wdata[DWI-1:0]), .CLK_RD (rclk), .RD_EN (rinc & !rempty), .ADDR_RD (raddr), .Q (rdata[DWO-1:0]) ); end //============================================== //big in and small out else begin: SHRINK_WIDTH //===================================== //gray code wire [AWO-1:0] waddr_ex = waddr << SHRINK_BIT ; wire [AWO:0] wptr = ({wover_flag, waddr_ex}>>1) ^ ({wover_flag, waddr_ex}) ; //sync rd ptr reg [AWO:0] rq2_wptr_r0 ; reg [AWO:0] rq2_wptr_r1 ; always @(posedge rclk or negedge rstn) begin if (!rstn) begin rq2_wptr_r0 <= 'b0 ; rq2_wptr_r1 <= 'b0 ; end else begin rq2_wptr_r0 <= wptr ; rq2_wptr_r1 <= rq2_wptr_r0 ; end end //sync rp ptr reg [AWO:0] wq2_rptr_r0 ; reg [AWO:0] wq2_rptr_r1 ; wire [AWO:0] rptr = ({rover_flag, raddr}>>1) ^ ({rover_flag, raddr}) ; always @(posedge rclk or negedge rstn) begin if (!rstn) begin wq2_rptr_r0 <= 'b0 ; wq2_rptr_r1 <= 'b0 ; end else begin wq2_rptr_r0 <= rptr ; wq2_rptr_r1 <= wq2_rptr_r0 ; end end //decode reg [AWO:0] wq2_rptr_decode ; reg [AWO:0] rq2_wptr_decode ; integer i ; always @(*) begin wq2_rptr_decode[AWO] = wq2_rptr_r1[AWO]; for (i=AWO-1; i>=0; i=i-1) begin wq2_rptr_decode[i] = wq2_rptr_decode[i+1] ^ wq2_rptr_r1[i] ; end end always @(*) begin rq2_wptr_decode[AWO] = rq2_wptr_r1[AWO]; for (i=AWO-1; i>=0; i=i-1) begin rq2_wptr_decode[i] = rq2_wptr_decode[i+1] ^ rq2_wptr_r1[i] ; end end assign rempty = (rover_flag == rq2_wptr_decode[AWO]) && (raddr >= rq2_wptr_decode[AWO-1:0]); assign wfull = (wover_flag != wq2_rptr_decode[AWO]) && (waddr_ex >= wq2_rptr_decode[AWO-1:0]) ; assign prog_full = (wover_flag == wq2_rptr_decode[AWO]) ? waddr_ex - wq2_rptr_decode[AWO-1:0] >= PROG_DEPTH-1 : waddr_ex + (1<<AWO) - wq2_rptr_decode[AWO-1:0] >= PROG_DEPTH-1; ramdp #( .AWI (AWI), .AWO (AWO), .DWI (DWI), .DWO (DWO)) u_ramdp ( .CLK_WR (wclk), .WR_EN (winc & !wfull), .ADDR_WR (waddr), .D (wdata[DWI-1:0]), .CLK_RD (rclk), .RD_EN (rinc & !rempty), .ADDR_RD (raddr), .Q (rdata[DWO-1:0]) ); end endgenerate endmodule

testbench设计

`timescale 1ns/1ns `define SMALL2BIG module test ; `ifdef SMALL2BIG reg rstn ; reg clk_fast, clk_slow ; reg [3:0] din ; reg din_en ; wire [15:0] dout ; wire dout_en ; //reset initial begin clk_fast = 0 ; clk_slow = 0 ; rstn = 0 ; #50 rstn = 1 ; end //clock parameter CYCLE_WR = 40 ; always #(CYCLE_WR/2/4) clk_slow = ~clk_slow ; always #(CYCLE_WR/2-1) clk_fast = ~clk_fast ; //data generate initial begin din = 16'h4321 ; din_en = 0 ; wait (rstn) ; //(1) test prog_full and full force test.u_data_buf2.u_buf_s2b.rinc = 1'b0 ; repeat(32) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = {$random()} % 16; end @(negedge clk_slow) din_en = 1'b0 ; //(2) test read and write fifo #500 ; rstn = 0 ; #10 rstn = 1 ; release test.u_data_buf2.u_buf_s2b.rinc; repeat(100) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = {$random()} % 16; end //(3) test again: prog_full and full force test.u_data_buf2.u_buf_s2b.rinc = 1'b0 ; repeat(18) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = {$random()} % 16; end end //data buffer fifo_s2b u_data_buf2( .rstn (rstn), .din (din), .din_clk (clk_slow), .din_en (din_en), .dout (dout), .dout_clk (clk_fast), .dout_en (dout_en)); `else reg rstn ; reg clk_fast, clk_slow ; reg [15:0] din ; reg din_en ; wire [3:0] dout ; wire dout_en ; //reset initial begin clk_fast = 0 ; clk_slow = 0 ; rstn = 0 ; #50 rstn = 1 ; end //clock parameter CYCLE_WR = 40 ; always #(CYCLE_WR/2) clk_slow = ~clk_slow ; always #(CYCLE_WR/2/4-1) clk_fast = ~clk_fast ; //data generate initial begin din = 16'h8888 ; din_en = 0 ; //(1) test prog_full and full force test.u_data_buf1.u_buf_b2s.rinc = 1'b0 ; repeat(18) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = din + 16'h4321 ; end //(2) test read and write fifo rstn = 0 ; #10 rstn = 1 ; release test.u_data_buf1.u_buf_b2s.rinc ; repeat(48) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = din + 16'h4321 ; end //(3) test again: prog_full and full force test.u_data_buf1.u_buf_b2s.rinc = 1'b0 ; repeat(18) begin @(negedge clk_slow) ; din_en = 1'b1 ; din = din + 16'h4321 ; end end //data buffer fifo_b2s u_data_buf1( .rstn (rstn), .din (din), .din_clk (clk_slow), .din_en (din_en), .dout (dout), .dout_clk (clk_fast), .dout_en (dout_en)); `endif //stop sim initial begin forever begin #100; if ($time >= 5000) $finish ; end end endmodule // test
这里解释一下testbench中出现的`ifdef

ifdef编译器指令

`ifdef MACRO_NAME // 代码块1 `else // 代码块2 `endif
在Verilog中,ifdef是一个编译器指令,它的作用是有条件地包含或排除源代码中的某些部分。
其中:
  • ifdef是指令关键字,表示"如果宏被定义"。
  • MACRO_NAME是一个宏名称,通常用大写字母表示。
  • 代码块1是当MACRO_NAME被定义时要包含的代码。
  • else部分是可选的,用于指定当MACRO_NAME未被定义时要包含的代码。
  • endif表示条件编译块的结束。
ifdef指令通常与define指令结合使用,后者用于定义一个宏。例如:
`define DEBUG ... `ifdef DEBUG $display("Debug info: ..."); `endif
在这个例子中,如果定义了DEBUG宏,则会包含$display语句,否则该语句将被排除在编译之外。
条件编译指令的主要目的包括:
  1. 根据不同的配置或平台selective地包含或排除代码。
  1. 在设计中临时启用或禁用调试代码。
  1. 有条件地实例化不同的模块。