Asynchronous FIFO Verilog Code (Dual-Clock, Gray-Code)

An asynchronous FIFO is a first-in, first-out buffer whose write port and read port run on two unrelated clocks. It is the workhorse for moving data safely between two clock domains — for example from a fast sensor interface into a slower processing block. If you try this with a plain register and a single clock, metastability and lost data are almost guaranteed. This tutorial builds a complete, synthesizable dual-clock design and gives you runnable asynchronous fifo verilog code you can simulate right here.

The design has four moving parts: a dual-port memory, a binary/Gray write pointer in the write clock, a binary/Gray read pointer in the read clock, and two-flop synchronizers that carry each Gray pointer into the opposite domain. The write side compares its pointer against the synchronized read pointer to generate wfull; the read side compares against the synchronized write pointer to generate rempty.

You will find this block anywhere two independent clocks meet: a camera or ADC front end feeding a slower datapath, a PCIe or Ethernet MAC handing packets to core logic, or an audio codec talking to a CPU. The depth is a design choice — it must absorb the worst-case burst while the slower side catches up. Here the depth is a power of two (four entries) because the pointer arithmetic wraps naturally on a power-of-two boundary, which keeps the Gray conversion and the full/empty comparison cheap.

Why Gray-code pointers and two-flop synchronizers

A multi-bit binary counter can change several bits at once (0011 to 0100 flips three bits). When another clock samples those bits mid-transition, it may latch a value that was never real. A Gray code changes exactly one bit per increment, so a sampling clock either sees the old value or the new one — never a corrupt in-between. Each pointer still needs a two-flop synchronizer (a pair of registers) in the destination domain to let any metastable bit settle before the comparison logic uses it. Run the warm-up below: the log proves the single-bit property and shows the Gray value arriving in a second clock domain.

// gray_demo.v  -- why async FIFOs use Gray code (Verilog-2005)
// Proves that a Gray counter changes exactly ONE bit per step, then shows a
// two-flop synchronizer carrying that Gray value into a second clock domain.

module gray_counter #(parameter W = 4)(
    input  wire        clk,
    input  wire        rst_n,
    output reg  [W-1:0] bin,
    output wire [W-1:0] gray
);
    assign gray = (bin >> 1) ^ bin;   // binary -> Gray
    always @(posedge clk or negedge rst_n)
        if (!rst_n) bin <= 0;
        else        bin <= bin + 1'b1;
endmodule

// generic two-flop synchronizer
module sync2 #(parameter W = 4)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire [W-1:0] d,
    output reg  [W-1:0] q
);
    reg [W-1:0] meta;
    always @(posedge clk or negedge rst_n)
        if (!rst_n) {q, meta} <= 0;
        else        {q, meta} <= {meta, d};
endmodule

module tb_gray_demo;
    localparam W = 4;
    reg  aclk = 0, bclk = 0, rst_n = 0;
    wire [W-1:0] bin, gray, gray_synced;
    reg  [W-1:0] prev_gray;
    integer      diff, i;

    gray_counter #(.W(W)) gc   (.clk(aclk), .rst_n(rst_n), .bin(bin), .gray(gray));
    sync2        #(.W(W)) sync (.clk(bclk), .rst_n(rst_n), .d(gray), .q(gray_synced));

    always #5 aclk = ~aclk;    // source clock
    always #6 bclk = ~bclk;    // destination clock (different frequency)

    // count how many bits flip between consecutive Gray values
    function integer popcount(input [W-1:0] v);
        integer k;
        begin
            popcount = 0;
            for (k = 0; k < W; k = k + 1) popcount = popcount + v[k];
        end
    endfunction

    initial begin
        repeat (2) @(negedge aclk);
        rst_n = 1;
        prev_gray = gray;
        for (i = 0; i < 12; i = i + 1) begin
            @(posedge aclk);
            #1;
            diff = popcount(gray ^ prev_gray);
            $display("step %0d  bin=%b  gray=%b  bits_changed=%0d", i, bin, gray, diff);
            prev_gray = gray;
        end
        $display("note: bits_changed is always 1 -> safe to sample across clocks");
        $display("synchronized Gray in bclk domain = %b", gray_synced);
        $finish;
    end
endmodule
step 0  bin=0001  gray=0001  bits_changed=1
step 1  bin=0010  gray=0011  bits_changed=1
step 2  bin=0011  gray=0010  bits_changed=1
step 3  bin=0100  gray=0110  bits_changed=1
step 4  bin=0101  gray=0111  bits_changed=1
step 5  bin=0110  gray=0101  bits_changed=1
step 6  bin=0111  gray=0100  bits_changed=1
step 7  bin=1000  gray=1100  bits_changed=1
step 8  bin=1001  gray=1101  bits_changed=1
step 9  bin=1010  gray=1111  bits_changed=1
step 10  bin=1011  gray=1110  bits_changed=1
step 11  bin=1100  gray=1010  bits_changed=1
note: bits_changed is always 1 -> safe to sample across clocks
synchronized Gray in bclk domain = 1101
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpwc5fqcul/gray_demo.v:65: $finish called at 136 (1s)

Complete asynchronous FIFO Verilog code

Here is the full module plus a two-clock testbench. The write clock has a 10 ns period and the read clock 16 ns, so the two domains drift against each other exactly as real hardware would. Simulate it — you will see six bytes written, the FIFO filling to its depth of four, wfull back-pressuring the writer, then the reader draining every byte in order.

// async_fifo.v  -- dual-clock asynchronous FIFO (Verilog-2005)
// Gray-code read/write pointers + two-flop synchronizers + full/empty flags.

module async_fifo #(
    parameter DW = 8,   // data width
    parameter AW = 2    // address width -> depth = 2**AW
)(
    // write clock domain
    input  wire          wclk,
    input  wire          wrst_n,
    input  wire          wr_en,
    input  wire [DW-1:0] wdata,
    output reg           wfull,
    // read clock domain
    input  wire          rclk,
    input  wire          rrst_n,
    input  wire          rd_en,
    output wire [DW-1:0] rdata,
    output reg           rempty
);
    localparam DEPTH = (1 << AW);

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

    // Write-domain pointers (AW+1 bits: extra MSB distinguishes full from empty)
    reg  [AW:0] wbin, wgray;
    reg  [AW:0] wq1, wq2;              // read-gray synchronized into write clock
    wire [AW:0] wbin_nxt  = wbin + (wr_en & ~wfull);
    wire [AW:0] wgray_nxt = (wbin_nxt >> 1) ^ wbin_nxt;

    // Read-domain pointers
    reg  [AW:0] rbin, rgray;
    reg  [AW:0] rq1, rq2;             // write-gray synchronized into read clock
    wire [AW:0] rbin_nxt  = rbin + (rd_en & ~rempty);
    wire [AW:0] rgray_nxt = (rbin_nxt >> 1) ^ rbin_nxt;

    // ---------------- write clock domain ----------------
    always @(posedge wclk or negedge wrst_n)
        if (!wrst_n) {wq2, wq1} <= 0;
        else         {wq2, wq1} <= {wq1, rgray};   // 2-flop sync of read pointer

    always @(posedge wclk or negedge wrst_n)
        if (!wrst_n) begin wbin <= 0; wgray <= 0; end
        else         begin wbin <= wbin_nxt; wgray <= wgray_nxt; end

    // Full when next write-gray equals read-gray with top two bits inverted
    wire wfull_nxt = (wgray_nxt == {~wq2[AW:AW-1], wq2[AW-2:0]});
    always @(posedge wclk or negedge wrst_n)
        if (!wrst_n) wfull <= 1'b0;
        else         wfull <= wfull_nxt;

    always @(posedge wclk)
        if (wr_en & ~wfull) mem[wbin[AW-1:0]] <= wdata;

    // ---------------- read clock domain ----------------
    always @(posedge rclk or negedge rrst_n)
        if (!rrst_n) {rq2, rq1} <= 0;
        else         {rq2, rq1} <= {rq1, wgray};   // 2-flop sync of write pointer

    always @(posedge rclk or negedge rrst_n)
        if (!rrst_n) begin rbin <= 0; rgray <= 0; end
        else         begin rbin <= rbin_nxt; rgray <= rgray_nxt; end

    // Empty when next read-gray equals synchronized write-gray
    wire rempty_nxt = (rgray_nxt == rq2);
    always @(posedge rclk or negedge rrst_n)
        if (!rrst_n) rempty <= 1'b1;
        else         rempty <= rempty_nxt;

    assign rdata = mem[rbin[AW-1:0]];
endmodule

// ----------------------------- testbench -----------------------------
module tb_async_fifo;
    localparam DW = 8, AW = 2;        // depth = 4

    reg          wclk = 0, rclk = 0;
    reg          wrst_n = 0, rrst_n = 0;
    reg          wr_en = 0, rd_en = 0;
    reg  [DW-1:0] wdata = 8'hA0;
    wire [DW-1:0] rdata;
    wire          wfull, rempty;
    integer       i;

    async_fifo #(.DW(DW), .AW(AW)) dut (
        .wclk(wclk), .wrst_n(wrst_n), .wr_en(wr_en), .wdata(wdata), .wfull(wfull),
        .rclk(rclk), .rrst_n(rrst_n), .rd_en(rd_en), .rdata(rdata), .rempty(rempty)
    );

    always #5 wclk = ~wclk;    // 100 MHz write clock
    always #8 rclk = ~rclk;    // slower read clock

    // WRITE SIDE: keep offering data; next value only after a real write
    initial begin
        repeat (2) @(negedge wclk);
        wrst_n = 1;
        for (i = 0; i < 24; i = i + 1) begin
            @(negedge wclk);
            if (!wfull && wdata < 8'hA6) begin
                wr_en <= 1'b1;
                $display("T=%0t  WRITE 0x%02h  (wfull=%b)", $time, wdata, wfull);
            end else begin
                wr_en <= 1'b0;
            end
        end
        @(negedge wclk) wr_en <= 1'b0;
    end

    // advance wdata one step after each accepted write
    always @(posedge wclk)
        if (wr_en && !wfull) wdata <= wdata + 1'b1;

    // report flag RISING edges so full/empty are visible without flooding the log
    reg wfull_d, rempty_d;
    always @(posedge wclk) begin
        wfull_d <= wfull;
        if (wfull & ~wfull_d) $display("T=%0t  >> wfull asserted (writes stall)", $time);
    end
    always @(posedge rclk) begin
        rempty_d <= rempty;
        if (rempty & ~rempty_d) $display("T=%0t  >> rempty asserted (reads stall)", $time);
    end

    // READ SIDE: let a few words build up, then drain
    initial begin
        repeat (3) @(negedge rclk);
        rrst_n = 1;
        repeat (4) @(negedge rclk);     // allow FIFO to fill a little
        for (i = 0; i < 24; i = i + 1) begin
            @(negedge rclk);
            if (!rempty) begin
                rd_en <= 1'b1;
                $display("T=%0t  READ  0x%02h  (rempty=%b)", $time, rdata, rempty);
            end else begin
                rd_en <= 1'b0;
            end
        end
        @(negedge rclk) rd_en <= 1'b0;
    end

    initial begin
        #700;
        $display("DONE  final wfull=%b rempty=%b", wfull, rempty);
        $finish;
    end
endmodule
T=30  WRITE 0xa0  (wfull=0)
T=40  WRITE 0xa1  (wfull=0)
T=50  WRITE 0xa2  (wfull=0)
T=60  WRITE 0xa3  (wfull=0)
T=75  >> wfull asserted (writes stall)
T=128  READ  0xa0  (rempty=0)
T=144  READ  0xa1  (rempty=0)
T=160  READ  0xa2  (rempty=0)
T=170  WRITE 0xa4  (wfull=0)
T=176  READ  0xa3  (rempty=0)
T=180  WRITE 0xa5  (wfull=0)
T=195  >> wfull asserted (writes stall)
T=200  >> rempty asserted (reads stall)
T=224  READ  0xa4  (rempty=0)
T=240  READ  0xa5  (rempty=0)
T=264  >> rempty asserted (reads stall)
DONE  final wfull=0 rempty=1
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpoo15amez/async_fifo.v:144: $finish called at 700 (1s)

Walking through the code

Pointers are one bit wider than the address

With depth 4 you need a 2-bit address, but the pointers are 3 bits (AW+1). The extra most-significant bit is a wrap flag. When the read and write pointers are equal in all bits the FIFO is empty; when they are equal in the low bits but the wrap bit differs, the FIFO is full. This is what lets a single comparator tell full and empty apart.

Generating full and empty

The read pointer is converted to Gray, synchronized into the write clock as wq2, and wfull asserts when the next write-Gray equals that synchronized read-Gray with its top two bits inverted. The write pointer makes the same journey into the read clock, and rempty asserts when the next read-Gray matches the synchronized write-Gray exactly. Because each flag is computed from a next-state value, it is registered and glitch-free.

Memory writes and reads

Data is written on wclk only when wr_en is high and the FIFO is not full, using the low address bits of the binary write pointer. The read data is a continuous assignment from the memory at the read pointer, so the current head word is always presented and is consumed on the next rclk edge when rd_en is high and the FIFO is not empty.

Two separate reset trees

Notice that the write logic resets on wrst_n and the read logic on rrst_n. In real silicon each reset should be asserted asynchronously but released synchronously to its own clock, so that flip-flops in a domain all leave reset on the same edge. The testbench keeps the two resets independent to model this. On reset the FIFO powers up empty (rempty high, wfull low), which is the only safe initial state.

Reading the simulation log

In the log, bytes 0xA0 through 0xA3 are written back-to-back, the fourth write fills the buffer, and wfull asserts so the writer pauses. As the reader pops 0xA0, 0xA1 and 0xA2, space frees and 0xA4 and 0xA5 slip in. Every byte comes out in the same order it went in, across two clocks that never share an edge — which is exactly the guarantee an asynchronous FIFO exists to provide.

Common mistakes

  • Synchronizing binary pointers. Passing a raw binary count across the boundary re-introduces the multi-bit-change hazard. Always convert to Gray before the synchronizer.
  • Using a single flop instead of two. One register is not enough time for a metastable value to resolve. Use at least two flops in series in the destination domain.
  • Forgetting the extra pointer bit. If the pointers are only as wide as the address, full and empty look identical and the FIFO silently corrupts data.
  • Combinational full/empty. Deriving the flags combinationally from synchronized pointers can glitch. Register them from the computed next-state value, as shown.
  • Separate reset domains ignored. Each clock domain needs its own asynchronously-asserted, synchronously-deasserted reset; sharing one raw reset across both clocks defeats the whole CDC effort.

Quick check

Why are the pointers one bit wider than the memory address?

The extra top bit acts as a wrap indicator. Equal pointers in every bit mean empty; equal low bits with a differing wrap bit mean full, so one comparator distinguishes the two states.

Why Gray code instead of binary for the pointers?

Gray code changes only one bit per increment, so a sampling clock in the other domain can only ever read the old or the new value, never a corrupted multi-bit transition.

What does the two-flop synchronizer actually buy you?

It gives any metastable bit a full destination-clock cycle to settle before the comparison logic consumes it, cutting the probability of propagating a bad value to a negligible level.

How is wfull generated without a subtractor?

The next write-Gray value is compared to the synchronized read-Gray with its two most-significant bits inverted. Equality means the write pointer has wrapped onto the read pointer, i.e. the FIFO is full.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD