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.
Share your question in comments or talk to our mentor team for batch guidance.
Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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