VLSI

Asynchronous Fifo Design

By CourseTron Team ·  5 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: An asynchronous FIFO safely moves data between two unrelated clock domains by using separate read and write pointers, converting those pointers to Gray code so only one bit changes per step, and passing them through two-flop synchronizers before they are compared to generate full and empty flags.

    Every real chip has more than one clock. A camera sensor streams pixels at 148.5 MHz, your SoC fabric runs at 200 MHz, and a PCIe PHY ticks at yet another rate. Whenever data crosses from one of these domains to another, you face the oldest hazard in digital design: metastability. The asynchronous FIFO is the canonical, interview-favourite structure that solves this cleanly, and getting its pointer logic exactly right separates engineers who have taped out from those who have only simulated.

    Why a naive FIFO fails across clocks

    A single-clock (synchronous) FIFO is trivial: one counter for the write address, one for the read address, and a subtraction tells you occupancy. The moment the write side and read side run on independent clocks, that subtraction becomes illegal. If you let the write clock sample a read-pointer counter that is mid-transition, you capture a value that is neither the old nor the new count—it may resolve to a random intermediate, and your empty/full comparison lies. A FIFO that reports "not full" when it is actually full will overwrite unread data; one that reports "not empty" when empty will read garbage. Both are silent, intermittent, and brutal to debug on silicon.

    A single D flip-flop sampling an asynchronous input has a non-zero probability of entering a metastable state. The fix is not to prevent it—you cannot—but to give the output time to settle, which is exactly what a two-flop synchronizer buys you (one extra clock period of settling time).

    The three pillars of a working async FIFO

    • Dual-port memory with independent write and read ports, written in the write-clock domain and read in the read-clock domain.
    • Gray-code pointers so that only one bit ever changes between consecutive addresses.
    • Two-flop synchronizers that carry each Gray-coded pointer into the opposite clock domain before any comparison happens.
    Asynchronous FIFO: two domains, one memory Write domain (wclk) Write pointer + FULL logic Dual-port RAM 2^n entries Read domain (rclk) Read pointer + EMPTY logic Gray pointers through 2-FF synchronizers CourseTron · coursetron.com
    Figure 1 — The two clock domains meet only at the dual-port RAM and through synchronized Gray pointers.

    Why Gray code, not binary

    Here is the heart of the design. Suppose the binary write pointer steps from 0111 (7) to 1000 (8). All four bits flip. When those four signals pass through a synchronizer, there is no guarantee they all arrive on the same destination clock edge—routing skew and metastable resolution can make the receiver briefly see 1111 (15) or 0000 (0). A comparison against such a corrupted value can momentarily assert a false full or empty.

    Gray code guarantees that exactly one bit changes on every increment. If that single bit is caught mid-transition and resolves either way, the synchronized value is still either the old pointer or the new one—never an illegal third value. Both outcomes are safe: at worst the flag is pessimistic (reports full/empty one cycle late), which stalls but never corrupts. Converting binary to Gray is a one-line XOR:

    // n-bit binary -> Gray
    assign gray = (bin >> 1) ^ bin;
    Pointer step 7 to 8: binary vs Gray Binary Gray 0111 1000 4 bits flip at once: receiver may see 1111 or 0000 0100 1100 1 bit flips: receiver sees old or new value only bit that changed CourseTron · coursetron.com
    Figure 2 — Stepping 7 to 8 flips four binary bits but only one Gray bit.

    Pointer width: the n+1 bit trick

    A FIFO with 2^n entries uses an (n+1)-bit pointer. The extra MSB distinguishes "full" from "empty" when the lower n address bits are identical. Empty is when the read and (synchronized) write Gray pointers are completely equal. Full is detected when the two most-significant Gray bits differ and all remaining bits match—this is the Gray-code equivalent of "write wrapped exactly one lap ahead of read."

    // empty in read domain
    assign empty = (rgray == wgray_sync);
    // full in write domain (Cummings style)
    assign full  = (wgray_next ==
        {~rgray_sync[n:n-1], rgray_sync[n-2:0]});
    Gray write pointer advancing, one bit at a time FF0 FF1 Read logic 2-flop synchronizer clocked by rclk Only 1 bit moves, so it is safe even if caught mid-edge CourseTron · coursetron.com
    Figure 3 — A single Gray bit propagates through two settling flops; a glitch resolves to old or new value, never an illegal one.

    Verilog skeleton

    A classic structure (after Clifford Cummings' widely cited style) splits the design into modules: a write-pointer/full block, a read-pointer/empty block, two synchronizer modules, and the dual-port memory. The synchronizer is deliberately minimal—no logic between the two flops:

    module sync2 #(parameter W=4)(
      input  clk, rstn,
      input  [W-1:0] d,
      output reg [W-1:0] q);
      reg [W-1:0] q1;
      always @(posedge clk or negedge rstn)
        if(!rstn) {q,q1} <= 0;
        else      {q,q1} <= {q1,d};
    endmodule

    You synchronize the Gray pointer, never the binary one, and never the occupancy count itself. Keep the two flops physically close and add a timing constraint so the tool does not insert combinational logic between them. For deeper state-machine patterns that pair well with FIFO controllers, see our walkthrough on Verilog FSM design, and for the hazard itself our primer on metastability and clock-domain crossing.

    Flag timing and MTBF

    FlagGenerated inPessimistic directionRisk if wrong
    FullWrite domainAsserts early / de-asserts lateStall (safe)
    EmptyRead domainAsserts early / de-asserts lateStall (safe)

    Both flags are intentionally conservative: the synchronizer delay makes each side see a slightly stale picture of the other pointer, so the FIFO may briefly think it has less room or fewer items than reality. That never causes data loss—only a cycle or two of lost throughput. Your constraint file must apply a set_false_path (or better, a set_max_delay -datapath_only) on the Gray buses crossing domains so static timing analysis in Synopsys PrimeTime or Cadence Tempus does not try, and fail, to meet a single-cycle path between unrelated clocks.

    For a two-flop synchronizer at 200 MHz with a typical 28 nm flop, the mean time between failures is often computed in the range of millions of years. Add a third flop for very high-frequency or safety-critical PHY crossings—each extra stage multiplies MTBF exponentially.

    Verification: where bugs actually hide

    Simulate with deliberately incommensurate clock periods (e.g. 7 ns write, 11 ns read) so edges drift through every phase relationship. Use Siemens Questa or Synopsys VCS with CDC-specific linting—SpyGlass CDC or Questa CDC—to prove every crossing has a synchronizer and that no convergence or reconvergence of multi-bit signals bypasses the Gray rule. Formal tools can prove the "never overflow, never underflow" invariant exhaustively. In the lab, a FIFO that passes RTL sim but fails on silicon almost always has a missing false-path constraint or a reset that releases asynchronously on one side. Teams building production CDC blocks often lean on dedicated VLSI design-services expertise to sign off these crossings before tapeout.

    Want to code, simulate and formally verify a complete parameterized async FIFO from scratch? Our CourseTron VLSI RTL & CDC track builds it line by line with Questa and SpyGlass labs—exactly the question you will get in a front-end interview.

    Frequently asked questions

    Why can't I just add a few flip-flops to a binary pointer?

    Because multiple binary bits change simultaneously on many increments. During synchronization those bits can arrive on different edges, yielding a value that equals neither the source's old nor new pointer—producing a false full or empty flag. Gray code removes this by limiting every transition to a single bit.

    How deep should the FIFO be?

    Depth is set by the worst-case burst rate difference between producer and consumer, plus the synchronizer latency (typically 2–3 cycles of the slower clock). Compute the maximum backlog over the longest burst and round up to the next power of two so the pointer wrap stays clean.

    Do I synchronize the binary or the Gray pointer?

    Always the Gray-coded pointer. You convert back to binary after synchronization if you need an occupancy count on that side, but the value that physically crosses the clock boundary must be Gray.

    Is two flops always enough?

    Two stages suffice for most on-chip crossings. For very high frequencies, large bit widths, or ISO 26262 / aerospace reliability targets, add a third synchronizer stage to push MTBF higher; the cost is one extra cycle of latency.

    Advertisement