Tear code – asynchronous FIFO

Hand tear code – asynchronous FIFO

  • 1. Asynchronous FIFO principle and design
    • Read and write address pointer control
    • Reading and writing address pointer cross-clock processing and full and empty signal judgment
    • Read and write addresses and read and write operations
  • 2. Complete code and simulation files
  • 3. Simulation results

1. Asynchronous FIFO principle and design

In the design of FIFO, whether it is a synchronous FIFO or an asynchronous FIFO, the most important thing is how to judge the empty signal empty and the full signal full. In the above “Hand Tear Code – Synchronous FIFO”, the synchronous FIFO we designed uses a data counter cnt to calculate the number of data stored in the current FIFO, and judges the empty and full signal of the FIFO according to the value of the counter cnt Signal full. If the counter cnt=0, it means that the number of data currently stored in the FIFO is 0, then pull up the empty signal empty; if the counter cnt=FIFO_DEPTH (FIFO depth), it means pull up the full signal full. Then, in the design of asynchronous FIFO, can the counter be used to judge the empty and full signal? In the design of the asynchronous FIFO, we do not use the counter to judge whether it is full or empty, but increase the bit width of the read-write pointer address by one redundant bit, which is used to judge whether it is full or empty.

Read and write address pointer control

First of all, it is the control of reading and writing address pointers. In the write clock domain, when the write enable wr_en is valid and the full signal full is low, it means that the data in the FIFO is not full, and the write operation is performed, and the write address pointer wr_ptr is incremented by 1; in the read clock domain, when the read enable rd_en is valid And when the empty signal empty is low level, it means that the data in the FIFO is not read empty, and the read operation is performed, and the read address pointer rd_ptr is automatically incremented by 1.

//write pointer
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n)
        wr_ptr <= 'd0;
    else if(wr_en & amp; & amp; !full)
        wr_ptr <= wr_ptr + 1'b1;
end

//read pointer
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n)
        rd_ptr <= 'd0;
    else if(rd_en & amp; & amp; !empty)
        rd_ptr <= rd_ptr + 1'b1;
end

Read and write address pointer cross-clock processing and empty-full signal judgment

Secondly, use the read and write address pointer to judge the empty and full signal. The read address pointer rd_ptr is in the read clock domain wr_clk, and the empty signal empty is also generated in the read clock domain; while the write address pointer wr_ptr is in the write clock domain, and the full signal is also generated in the write clock domain.
Then, to use the read address pointer rd_ptr to compare with the write address pointer wr_ptr to generate an empty signal empty, can it be directly compared? The answer is no. Because these two signals are in different clock domains, cross-clock domain CDC processing is required, and multi-bit signals are processed across clock domains. The common method is to use asynchronous FIFO for synchronization, but aren’t we designing asynchronous FIFO? Therefore, the asynchronous FIFO is designed here. The problem of multi-bit cross-clock domain processing can be transformed into single-bit cross-clock domain processing. After converting the read and write address pointers into Gray codes, cross-clock domain processing is performed, because no matter how many bits of Gray Code, every time adding 1, only change 1 bit. Convert the read address pointer rd_ptr to Gray code, and then synchronize to the write clock domain wr_clk, compare and judge with the Gray code representation of the write pointer address wr_ptr, and obtain the full signal full; similarly, convert the write address pointer wr_ptr to Gray code, and then Synchronize to the read clock domain rd_clk, compare and judge with the Gray code representation of the read address pointer rd_ptr, and obtain the empty signal empty.
Therefore, it is necessary to first convert the read address pointer rd_ptr and write address pointer wr_ptr represented by binary code into Gray code (for the conversion principle and design of binary code and Gray code, refer to “The principle of mutual conversion between binary code and Gray code and Verilog implementation” ).

//write pointer(binary to gray)
assign wr_ptr_gray = wr_ptr ^ (wr_ptr>>1);

//read pointer(binary to gray)
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);

Then do single-bit cross-clock domain processing on the read-write address pointer represented by Gray code, and make two beats. It should be noted that: the gray code wr_ptr_gray of the write address pointer indicates that it needs to be synchronized to the read clock domain, so the clock signal rd_clk of the read clock domain should be used; the gray code of the read address pointer indicates that rd_ptr_gray indicates that it must be synchronized to the write clock domain, so it is necessary to Use the clock signal wr_clk of the write clock domain.

//gray write pointer syncrous
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n) begin
        wr_ptr_gray_w2r_1 <= 'd0;
        wr_ptr_gray_w2r_2 <= 'd0;
    end
    else begin
        wr_ptr_gray_w2r_1 <= wr_ptr_gray;
        wr_ptr_gray_w2r_2 <= wr_ptr_gray_w2r_1;
    end
end

//gray read pointer syncrous
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n) begin
        rd_ptr_gray_r2w_1 <= 'd0;
        rd_ptr_gray_r2w_2 <= 'd0;
    end
    else begin
        rd_ptr_gray_r2w_1 <= rd_ptr_gray;
        rd_ptr_gray_r2w_2 <= rd_ptr_gray_r2w_1;
    end
end

Then use the gray code representation of the read and write address pointer after synchronous processing for comparison, and get the empty and full signal. If the Gray code of the write address pointer indicates that it is synchronized to the read clock domain, it is consistent with the Gray code of the write address pointer, indicating that it is empty; if the Gray code of the read address pointer indicates that it is synchronized to the write clock domain, the upper two bits are the same as the write The gray code of the address pointer is opposite, and the remaining bits are consistent with the gray code of the write address pointer, which means that it is full.

//full and empty
assign full = (wr_ptr_gray == {<!-- -->~rd_ptr_gray_r2w_2[ADDR_WIDTH:ADDR_WIDTH-1],rd_ptr_gray_r2w_2[ADDR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
assign empty = (rd_ptr_gray == wr_ptr_gray_w2r_2) ? 1'b1 : 1'b0;

Read and write addresses and read and write operations

In the read-write address pointer, we set a redundant bit width of one bit to judge whether it is empty or full. Then, in fact, the read-write address should be the second highest bit to the 0th bit of the read-write address pointer. Take the second highest bit of the read-write address pointer to the 0th bit to get the read-write address, and perform read-write operations according to the read-write address.

//write address and read address
assign wr_addr = wr_ptr[ADDR_WIDTH-1:0];
assign rd_addr = rd_ptr[ADDR_WIDTH-1:0];
//write
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n)
        for(i=0;i<FIFO_DEPTH;i=i + 1) begin
            mem[i] <= 'd0;
        end
    else if(wr_en & amp; & amp; !full)
        mem[wr_addr] <= din;
end

//read
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n)
        dout_r <= 'd0;
    else if(rd_en & amp; & amp; !empty)
        dout_r <= mem[rd_addr];
end
assign dout = dout_r;

2. Complete code and simulation file

The complete code of asynchronous FIFO is as follows:

`timescale 1ns / 1ps
//
// Company:
//Engineer:
//
// Create Date: 2023/05/17 20:24:34
// Design Name:
// Module Name: async_fifo
// Project Name:
//Target Devices:
// Tool Versions:
// Description: Asynchronous FIFO
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//

module async_fifo
#(
    parameter FIFO_WIDTH = 8,
    parameter FIFO_DEPTH = 8,
    parameter ADDR_WIDTH = $clog2(FIFO_DEPTH)
)
(
    //write clock domain
    input wr_clk,
    input wr_rst_n ,
    input wr_en ,
    input [FIFO_WIDTH-1:0] din ,
    output full ,
    //read clock domain
    input rd_clk,
    input rd_rst_n,
    input rd_en ,
    output [FIFO_WIDTH-1:0] dout ,
    output empty
);

//------------------------------------------------ //
integer i;
//memory
reg [FIFO_WIDTH-1:0] mem [FIFO_DEPTH-1:0];
//write address and read address
wire [ADDR_WIDTH-1:0] wr_addr;
wire [ADDR_WIDTH-1:0] rd_addr;
//write pointer
reg [ADDR_WIDTH:0] wr_ptr;
wire [ADDR_WIDTH:0] wr_ptr_gray;
reg [ADDR_WIDTH:0] wr_ptr_gray_w2r_1;
reg [ADDR_WIDTH:0] wr_ptr_gray_w2r_2;
//read pointer
reg [ADDR_WIDTH:0] rd_ptr;
wire [ADDR_WIDTH:0] rd_ptr_gray;
reg [ADDR_WIDTH:0] rd_ptr_gray_r2w_1;
reg [ADDR_WIDTH:0] rd_ptr_gray_r2w_2;
//
reg [FIFO_WIDTH-1:0] dout_r;

//------------------------------------------------ //
//----------------------//
//write pointer
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n)
        wr_ptr <= 'd0;
    else if(wr_en & amp; & amp; !full)
        wr_ptr <= wr_ptr + 1'b1;
end

//write pointer(binary to gray)
assign wr_ptr_gray = wr_ptr ^ (wr_ptr>>1);

//gray write pointer syncrous
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n) begin
        wr_ptr_gray_w2r_1 <= 'd0;
        wr_ptr_gray_w2r_2 <= 'd0;
    end
    else begin
        wr_ptr_gray_w2r_1 <= wr_ptr_gray;
        wr_ptr_gray_w2r_2 <= wr_ptr_gray_w2r_1;
    end
end

//----------------------//
//read pointer
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n)
        rd_ptr <= 'd0;
    else if(rd_en & amp; & amp; !empty)
        rd_ptr <= rd_ptr + 1'b1;
end

//read pointer(binary to gray)
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);

//gray read pointer syncrous
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n) begin
        rd_ptr_gray_r2w_1 <= 'd0;
        rd_ptr_gray_r2w_2 <= 'd0;
    end
    else begin
        rd_ptr_gray_r2w_1 <= rd_ptr_gray;
        rd_ptr_gray_r2w_2 <= rd_ptr_gray_r2w_1;
    end
end

//----------------------//
//write address and read address
assign wr_addr = wr_ptr[ADDR_WIDTH-1:0];
assign rd_addr = rd_ptr[ADDR_WIDTH-1:0];

//full
assign full = (wr_ptr_gray == {~rd_ptr_gray_r2w_2[ADDR_WIDTH:ADDR_WIDTH-1],rd_ptr_gray_r2w_2[ADDR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
//empty
assign empty = (rd_ptr_gray == wr_ptr_gray_w2r_2) ? 1'b1 : 1'b0;

//----------------------//
//write
always @(posedge wr_clk or negedge wr_rst_n) begin
    if(!wr_rst_n)
        for(i=0;i<FIFO_DEPTH;i=i + 1) begin
            mem[i] <= 'd0;
        end
    else if(wr_en & amp; & amp; !full)
        mem[wr_addr] <= din;
end

//read
always @(posedge rd_clk or negedge rd_rst_n) begin
    if(!rd_rst_n)
        dout_r <= 'd0;
    else if(rd_en & amp; & amp; !empty)
        dout_r <= mem[rd_addr];
end
assign dout = dout_r;

endmodule

The simulation file is as follows:

`timescale 1ns / 1ps
//
// Company:
//Engineer:
//
// Create Date: 2023/05/17 21:18:22
// Design Name:
// Module Name: tb_async_fifo
// Project Name:
//Target Devices:
// Tool Versions:
//Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//


module tb_async_fifo();
parameter FIFO_WIDTH = 8;
parameter FIFO_DEPTH = 8;
parameter ADDR_WIDTH = $clog2(FIFO_DEPTH);

reg rst_n;
reg wr_clk;
reg wr_en;
reg [FIFO_WIDTH-1:0] din;
wire full;
reg rd_clk;
reg rd_en;
wire [FIFO_WIDTH-1:0] dout;
wire empty;


initial begin
  rst_n <= 1'b0;
  wr_clk = 1'b1;
  rd_clk = 1'b1;
  wr_en <= 1'b0;
  rd_en <= 1'b0;
  din <= 8'b0;

  # 5 rst_n <= 1'b1;

end

initial begin
  #20 wr_en <= 1'b1;
      rd_en <= 1'b0;
  #40 wr_en <= 1'b0;
      rd_en <= 1'b1;
  #30 wr_en <= 1'b1;
      rd_en <= 1'b0;
  #13 rd_en <= 1'b1;
  #10
  repeat(100)
  begin
      #5 wr_en <= {$random}%2 ;
         rd_en <= {<!-- -->$random}%2 ;
  end

  #100
  $finish;

end

always #1.5 wr_clk = ~wr_clk;
always #1 rd_clk = ~rd_clk;
always #3 din <= {$random}%8'hFF;

async_fifo
#(
    .FIFO_WIDTH(FIFO_WIDTH),
    .FIFO_DEPTH(FIFO_DEPTH),
    .ADDR_WIDTH(ADDR_WIDTH)
)
async_fifo
(
    .wr_clk (wr_clk ),
    .wr_rst_n (rst_n ),
    .wr_en (wr_en ),
    .din (din ),
    .full (full ),
    .rd_clk (rd_clk ),
    .rd_rst_n (rst_n ),
    .rd_en (rd_en ),
    .dout (dout ),
    .empty (empty )
);

endmodule

3. Simulation results

According to the TestBench file, simulate the asynchronous FIFO. It can be seen that in the write clock domain wr_clk, the write enable wr_en is valid, and the data write operation is successfully performed. After 8 data are successfully written, the write full signal full is pulled high. During the full signal full is high, Even if the write enable wr_en is valid, no data write operation is performed.

At the same time, it can be seen that the write address pointer wr_ptr is under the write clock domain wr_clk. When the write enable wr_en is valid, it will be incremented by 1. At the same time, the Gray code representation of the write pointer address wr_ptr_gray is synchronized to the read clock domain by two beats.

You can see that the FIFO read data is correct. After reading all the data in the FIFO, pull up the empty read signal empty.



In summary, the asynchronous FIFO design has passed the verification.