Sign in on the Premium hub to save lesson progress. This lesson is open to read so the page is a complete article, not an empty login wall.
Lesson 4.3

Complete async FIFO RTL

One module, two clocks, dual-port RAM, registered Gray pointers, combinational full/empty from next Gray. This is the block you should be able to reconstruct in an interview.

1

Synthesizable listing

async_fifo.v
module async_fifo #(
  parameter WIDTH  = 8,
  parameter DEPTH  = 16,                 // MUST be power of 2
  parameter ADDR_W = $clog2(DEPTH)
)(
  input  wire             wr_clk,
  input  wire             wr_rst_n,
  input  wire             wr_en,
  input  wire [WIDTH-1:0] wdata,
  output wire             full,
  input  wire             rd_clk,
  input  wire             rd_rst_n,
  input  wire             rd_en,
  output reg  [WIDTH-1:0] rdata,
  output wire             empty
);
  localparam PTR_W = ADDR_W + 1;

  reg [WIDTH-1:0] mem [0:DEPTH-1];

  // Pointers declared first so the 2FF can sample rgray / wgray.
  reg  [PTR_W-1:0] wbin, wgray, rbin, rgray;
  reg  [PTR_W-1:0] wq1_rgray, wq2_rgray;
  reg  [PTR_W-1:0] rq1_wgray, rq2_wgray;

  // Registered-pointer flags (no combinational loop). The beat that fills
  // the last slot is legal; full rises on the following wr_clk.
  assign full  = (wgray == {~wq2_rgray[PTR_W-1:PTR_W-2], wq2_rgray[PTR_W-3:0]});
  assign empty = (rgray == rq2_wgray);

  wire wr_fire = wr_en & ~full;
  wire rd_fire = rd_en & ~empty;

  wire [PTR_W-1:0] wbin_next  = wbin + wr_fire;
  wire [PTR_W-1:0] wgray_next = wbin_next ^ (wbin_next >> 1);
  wire [PTR_W-1:0] rbin_next  = rbin + rd_fire;
  wire [PTR_W-1:0] rgray_next = rbin_next ^ (rbin_next >> 1);

  // ---- write domain ----

  always @(posedge wr_clk or negedge wr_rst_n) begin
    if (!wr_rst_n) begin
      wbin  <= {PTR_W{1'b0}};
      wgray <= {PTR_W{1'b0}};
    end else begin
      wbin  <= wbin_next;
      wgray <= wgray_next;
    end
  end

  always @(posedge wr_clk)
    if (wr_fire) mem[wbin[ADDR_W-1:0]] <= wdata;

  always @(posedge wr_clk or negedge wr_rst_n) begin
    if (!wr_rst_n) {wq2_rgray, wq1_rgray} <= {{PTR_W{1'b0}},{PTR_W{1'b0}}};
    else           {wq2_rgray, wq1_rgray} <= {wq1_rgray, /* rgray from rd domain */ rgray};
  end

  always @(posedge rd_clk or negedge rd_rst_n) begin
    if (!rd_rst_n) begin
      rbin  <= {PTR_W{1'b0}};
      rgray <= {PTR_W{1'b0}};
    end else begin
      rbin  <= rbin_next;
      rgray <= rgray_next;
    end
  end

  always @(posedge rd_clk)
    if (rd_fire) rdata <= mem[rbin[ADDR_W-1:0]];

  always @(posedge rd_clk or negedge rd_rst_n) begin
    if (!rd_rst_n) {rq2_wgray, rq1_wgray} <= {{PTR_W{1'b0}},{PTR_W{1'b0}}};
    else           {rq2_wgray, rq1_wgray} <= {rq1_wgray, wgray};
  end
endmodule
DEPTH = 2

For PTR_W == 2 there are no [PTR_W-3:0] bits — full is just wgray == ~wq2_rgray (both bits inverted). The listing assumes DEPTH >= 4 (PTR_W >= 3), which is the usual teaching and product case.

Verilog cross-domain read of rgray / wgray

The listing reads rgray inside the write-domain 2FF and wgray inside the read-domain 2FF. Those source flops must be the registered Gray nets. The first stage of each 2FF is the metastability absorber. Do not add combo logic on that path. Some lint tools still flag the cross-module clock; the intent is a synchronizer — constrain it in SDC as a false path into FF1, or use a dedicated sync cell.

2

Line-by-line

  1. wr_fire / rd_fire never commit when the registered flag is already set.
  2. wgray_next is Gray of the next binary — converted before the flop, then registered.
  3. full / empty compare registered Gray (no combinational loop through the incrementer).
  4. RAM write address is current wbin (the location being filled this cycle).
  5. Read side is the mirror: empty from rgray vs 2FF-synced write Gray.
3

Test in the simulator

Icarus in the browser uses one simulation time wheel — you can still generate two clocks with different periods:

two clocks
reg wr_clk=0, rd_clk=0;
always #4 wr_clk = ~wr_clk;   // 125 MHz
always #7 rd_clk = ~rd_clk;   // ~71 MHz
Open Verilog simulator →

Each flag is a flop/combo in one domain. A waveform that shows both flags changing on a shared clock is the wrong picture for this RTL.

4

Self-checking dual-clock testbench outline

Fork two procedural threads. Writer: wait posedge wr_clk, if (!full) drive wr_en and increment a source counter. Reader: wait posedge rd_clk, if (!empty) pulse rd_en and check rdata against a queue one cycle later. Use different seed rates (write every cycle, read every other) until full, then drain. Pass criteria: no scoreboard mismatch, no X, full and empty both observed, blocked-write count > 0 when you hammer writes.

Do not use #0 races between the two clocks to sample flags. Sample flags with the domain that owns them: full only after wr_clk, empty only after rd_clk.

5

FPGA RAM inference notes

Simple dual-port: write on wr_clk, read on rd_clk, different addresses. Avoid read/write to the same address in a way the BRAM collision mode does not match your RTL (new vs old data). When occupancy is correct, the writer never uses an address the reader is popping except at the exact full/empty boundaries your flags already forbid. Still set the RAM to “no change” or “old data” consistently in the generator if you instance a primitive instead of inferred mem[].

Q

Frequently asked questions

What are the four sequential pieces of an async FIFO?

Write binary+Gray incrementer, read binary+Gray incrementer, 2FF of rgray on wr_clk, 2FF of wgray on rd_clk, plus dual-port RAM write and read processes.

Why is DEPTH=2 a special case?

PTR_W=2 has no [PTR_W-3:0] slice. Full is wgray == ~wq2_rgray (both bits inverted). Use DEPTH>=4 for the textbook slice.

How do you simulate two clocks in one testbench?

Two always blocks with different delays, e.g. always #4 wr_clk=~wr_clk; always #7 rd_clk=~rd_clk. Do not derive rd_clk from wr_clk with a divider if you are claiming to test async behavior.

Is reading rgray in the write-domain 2FF legal?

Yes: that is the CDC. Lint will complain. Mark ASYNC_REG, constrain FF1, and do not add combo on that D input.

Key takeaways

  • Two incrementers, two Gray registers, two 2FF chains, one RAM.
  • full / empty from registered Gray vs 2FF-synced opposite Gray (two MSBs inverted for full).
  • DEPTH power of two. PTR_W = ADDR_W+1.