Asynchronous FIFO :

Asynchronous FIFO is needed whenever we want to transfer data between design blocks that are in different clock domains. The difference in clock domains makes writing and reading the FIFO tricky.

If appropriate precautions are not taken then we could end up in a scenario where write into FIFO has not yet finished and we are attempting to Read it or Vice-versa. This scenario often causes data loss and Metastability issues.

In order to avoid such scenarios, the reading and writing is done via a synchronizer. The synchronizer ensures that read and write pointers calculations are consistent and data in FIFO is not accidentally overwritten or read twice.

However, with the clock crossing we need to ensure that FIFO full and empty conditions are taking into account the clock crossing cycles. In other words, pessimistic full and empty conditions need to be added.

Here’s an example to 8-deep FIFO with Write in aclk domain and read in bclk domain:

logic [3:0] wraddr, wraddr_gray, rdaddr, rdaddr_gray;
logic [3:0] wraddr_b1ff,wraddr_bff, wraddr_aff;
logic [3:0] rdaddr_a1ff, rdaddr_aff, rdaddr_bff;
logic [7:0] wren, wren_qual, rden, rden_qual;
logic [31:0] data_ff[7:0];
logic [31:0] data_in, data_out;
logic wr_en, rd_en;
logic fifo_empty, fifo_full;

always_comb begin
if(!fifo_full & wr_en)
  wraddr = wraddr_ff + 1;
else
  wraddr = wraddr_aff;
end

always_ff @(posedge aclk or negedge reset) begin
 if(!reset)
   wraddr_aff <= 0;
 else if (wr_en)
   wraddr_aff <=  wraddr;
 else
   wraddr_aff <= wraddr_aff;
end

//---- Convert wraddr to Gray code ----//
assign wraddr_gray = (wraddr >> 1) ^ wraddr;

//----- Clock sync to bclk --------//
always_ff @(posedge bclk or negedge reset) begin
 if(!reset) begin
   wraddr_bff  <= 0;
   wraddr_b1ff <= 0;
 end
 else begin
   wraddr_bff  <= wraddr_gray;
   wraddr_b1ff <= wraddr_bff;
 end
end

always_comb begin
 if(!fifo_empty & rd_en)
   rdaddr = rdaddr_bff + 1;
 else
   rdaddr = rdaddr_bff;
end

always_ff @(posedge bclk or negedge reset) begin
 if(!reset)
   rdaddr_bff <= 0;
 else if (rd_en)
   rdaddr_bff <=  rdaddr;
 else
   rdaddr_bff <= rdaddr_bff;
end

//------ Convert rdaddr to Gray code -------//
assign rdaddr_gray = (rdaddr >> 1) ^ rdaddr;

//------- Clock Sync to aclk ----------//
always_ff @(posedge aclk or negedge reset) begin
 if(!reset) begin
   rdaddr_aff  <= 0;
   rdaddr_a1ff <= 0;
 end
 else begin
   rdaddr_aff  <= rdaddr_gray;
   rdaddr_a1ff <= rdaddr_aff;
 end
end

//------- Data Read and Write ---------//
assign wren_qual = (wr_en & !fifo_full) ? 
                   (1 << wraddr) : 8'b0;
assign rden_qual = (rd_en & !fifo_empty) ? 
                   (1 << rdaddr) : 8'b0;

genvar i;
generate
for (i = 0; i < 8; i++) begin  
always_ff @(posedge aclk or negedge reset) begin
  if(wren_qual[i]) 
     data_ff[i]  <= data_in;
   else
     data_ff[i] <= data_ff[i];
  end
end
endgenerate

always_comb begin
 for (int j = 0; j < 8; j++) begin
  if(rden_qual[j]) 
    data_out = data_ff[j];
  else
    data_out = 8'b0;  
end

//------ Full and Empty Conditions -----------------//
assign fifo_full = 
(wraddr_gray[2:0] == rdaddr_a1ff[2:0]) &&
(wraddr_gray[3:3] != rdaddr_a1ff[3:3]);

assign fifo_empty = 
(wraddr_b1ff[3:0] == rdaddr_gray[3:0]);

Alternately, similar to Synchronous FIFO, we can also use synchronized write and read rollover signals in calculation of full and empty conditions.