HomeCDC GuideDay 5
DAY 5 · CDC FUNDAMENTALS

Dual-Clock FIFOs — The Cornerstone Pattern

By EcrioniX · Updated Jun 13, 2026

The dual-clock FIFO is the gold standard for safe data transfer between clock domains. It combines everything you've learned: Gray code pointers, 2-FF synchronizers, and empty/full flag generation. A dual-clock FIFO decouples two clock domains, allowing data to flow freely without timing violations or metastability corruption.

1. Architecture overview

A dual-clock FIFO has:

Key insight: pointers never cross clock domains directly. Only Gray code versions cross, synced with 2-FF. This prevents invalid intermediate states.

✅ The dual-clock FIFO pattern

Write domain: Maintain write_ptr (binary) → convert to Gray → sync to read domain. Read domain: Compare synced Gray write pointer (converted back to binary) with read_ptr to detect empty. Same in reverse: read_ptr crosses to write domain as Gray.

2. Empty and Full flag generation

Flags must be generated in their respective clock domains using synced pointers:

Write domain empty flag:

Read domain full flag:

This ensures flags are generated from synchronized pointers, preventing false positives/negatives due to metastability.

3. Complete Verilog implementation

dual_clock_fifo.sv (complete)
// Dual-Clock FIFO with Gray Code Pointer Synchronization
module dual_clock_fifo #(
  parameter DATA_WIDTH = 8,
  parameter ADDR_WIDTH = 4   // 2^ADDR_WIDTH depth
) (
  // Write clock domain
  input clk_w, rst_n_w,
  input wr_en,
  input [DATA_WIDTH-1:0] data_in,
  output wr_full,

  // Read clock domain
  input clk_r, rst_n_r,
  input rd_en,
  output [DATA_WIDTH-1:0] data_out,
  output rd_empty
);

  // Write-side: binary counter and Gray conversion
  reg [ADDR_WIDTH:0] wr_ptr, wr_ptr_gray;
  wire [ADDR_WIDTH:0] rd_ptr_gray_sync, rd_ptr_sync;

  always @(posedge clk_w or negedge rst_n_w) begin
    if (!rst_n_w) begin
      wr_ptr <= 0;
      wr_ptr_gray <= 0;
    end else if (wr_en && !wr_full) begin
      wr_ptr <= wr_ptr + 1;
      wr_ptr_gray <= (wr_ptr + 1) ^ ((wr_ptr + 1) >> 1);
    end else begin
      wr_ptr_gray <= wr_ptr ^ (wr_ptr >> 1);
    end
  end

  // Read-side: binary counter and Gray conversion
  reg [ADDR_WIDTH:0] rd_ptr, rd_ptr_gray;
  wire [ADDR_WIDTH:0] wr_ptr_gray_sync, wr_ptr_sync;

  always @(posedge clk_r or negedge rst_n_r) begin
    if (!rst_n_r) begin
      rd_ptr <= 0;
      rd_ptr_gray <= 0;
    end else if (rd_en && !rd_empty) begin
      rd_ptr <= rd_ptr + 1;
      rd_ptr_gray <= (rd_ptr + 1) ^ ((rd_ptr + 1) >> 1);
    end else begin
      rd_ptr_gray <= rd_ptr ^ (rd_ptr >> 1);
    end
  end

  // Synchronize Gray pointers across domains (2-FF sync)
  reg [ADDR_WIDTH:0] wr_ptr_gray_ff1, wr_ptr_gray_ff2;
  reg [ADDR_WIDTH:0] rd_ptr_gray_ff1, rd_ptr_gray_ff2;

  always @(posedge clk_r or negedge rst_n_r) begin
    if (!rst_n_r) begin
      wr_ptr_gray_ff1 <= 0;
      wr_ptr_gray_ff2 <= 0;
    end else begin
      wr_ptr_gray_ff1 <= wr_ptr_gray;
      wr_ptr_gray_ff2 <= wr_ptr_gray_ff1;
    end
  end

  always @(posedge clk_w or negedge rst_n_w) begin
    if (!rst_n_w) begin
      rd_ptr_gray_ff1 <= 0;
      rd_ptr_gray_ff2 <= 0;
    end else begin
      rd_ptr_gray_ff1 <= rd_ptr_gray;
      rd_ptr_gray_ff2 <= rd_ptr_gray_ff1;
    end
  end

  assign wr_ptr_gray_sync = wr_ptr_gray_ff2;
  assign rd_ptr_gray_sync = rd_ptr_gray_ff2;

  // Gray to Binary converters (in respective domains)
  function [ADDR_WIDTH:0] gray_to_binary(input [ADDR_WIDTH:0] gray);
    integer i;
    begin
      gray_to_binary = gray[ADDR_WIDTH];
      for (i = ADDR_WIDTH-1; i >= 0; i=i-1)
        gray_to_binary[i] = gray_to_binary[i+1] ^ gray[i];
    end
  endfunction

  assign wr_ptr_sync = gray_to_binary(wr_ptr_gray_sync);
  assign rd_ptr_sync = gray_to_binary(rd_ptr_gray_sync);

  // Memory
  reg [DATA_WIDTH-1:0] mem [0:(1<This is a complete, working dual-clock FIFO. The key points:

  • Parametrized: WIDTH and ADDR_WIDTH set at instantiation
  • Gray code pointers: Only Gray versions cross domains
  • 2-FF sync: Each Gray pointer gets FF1 and FF2 in receiving domain
  • Binary conversion: Synced Gray pointers converted back in respective domains
  • Full/empty logic: Generated from synced pointers, preventing race conditions

4. Testbench: stress testing with different frequencies

tb_dual_clock_fifo.sv
// Testbench: Dual-Clock FIFO with different clock frequencies
module tb_dual_clock_fifo;
  parameter DATA_WIDTH = 8;
  parameter ADDR_WIDTH = 3;  // 8-entry FIFO

  reg clk_w, rst_n_w;
  reg clk_r, rst_n_r;
  reg wr_en, rd_en;
  reg [DATA_WIDTH-1:0] data_in;
  wire [DATA_WIDTH-1:0] data_out;
  wire wr_full, rd_empty;

  dual_clock_fifo #(.DATA_WIDTH(DATA_WIDTH), .ADDR_WIDTH(ADDR_WIDTH))
    dut (.*);

  // Write clock: 10ns period (100 MHz)
  always begin
    #5 clk_w = ~clk_w;
  end

  // Read clock: 7ns period (~142 MHz) - different frequency!
  always begin
    #3.5 clk_r = ~clk_r;
  end

  initial begin
    clk_w = 0;
    clk_r = 0;
    rst_n_w = 0;
    rst_n_r = 0;
    wr_en = 0;
    rd_en = 0;

    // Reset
    #50 rst_n_w = 1; rst_n_r = 1;
    $display("@%0t: Resets released", $time);

    // Test 1: Write some data
    repeat(5) begin
      #10;
      if (!wr_full) begin
        wr_en = 1;
        data_in = $random % 256;
        $display("@%0t: Write %d, full=%b", $time, data_in, wr_full);
      end else begin
        wr_en = 0;
        $display("@%0t: FIFO full, stalling writes", $time);
      end
    end
    wr_en = 0;

    // Test 2: Read data
    #50;
    repeat(8) begin
      #7;
      if (!rd_empty) begin
        rd_en = 1;
        $display("@%0t: Read %d, empty=%b", $time, data_out, rd_empty);
      end else begin
        rd_en = 0;
        $display("@%0t: FIFO empty", $time);
      end
    end
    rd_en = 0;

    #100 $finish;
  end

  initial begin
    $dumpfile("tb_dual_clock_fifo.vcd");
    $dumpvars(0, tb_dual_clock_fifo);
  end
endmodule

This testbench demonstrates the FIFO with different clock frequencies (100 MHz write, 142 MHz read). The FIFO correctly handles bursts, prevents overflow/underflow, and reliably transfers data despite frequency mismatch.

5. Synchronizer selection table

By now you've seen all the key CDC patterns. Here's when to use each:

PatternUse CaseProsCons
2-FF SyncSingle-bit, stable signalsSimple, low latency, small areaLoses pulses, fixed 2-cycle latency
Gray CodeMulti-bit counters, pointersSafe, no invalid intermediate statesUnidirectional, must be sequential
Pulse/Toggle SyncEvents, narrow pulsesHandles any pulse widthSingle-bit only
Handshake (req-ack)Data + flow controlAtomic, bidirectionalHigher latency, more logic
Dual-Clock FIFOStreaming data, frequency decouplingMost flexible, buffering includedMore area, higher latency than simple sync

✅ Decision flowchart

Is it a single bit? → Use 2-FF sync or pulse sync (if pulse). Is it a multi-bit counter/pointer? → Use Gray code. Is it streaming data? → Use dual-clock FIFO or valid-ready handshake. Is it a request+data? → Use req-ack handshake or data+handshake pattern (Day 6).

6. Advanced considerations

  • Initialization: Both pointers start at 0. Synced pointers start at 0. FIFO starts empty in both domains.
  • Overflow/underflow: Set data_in to 'x (don't care) if FIFO is full. Don't read if rd_empty. The hardware prevents corruption automatically.
  • One-hot vs binary pointers: This example uses binary pointers. Some designs use one-hot encoding (one bit per entry) for simpler full/empty logic, but this limits FIFO size.
  • Asynchronous reset: Resets should be synchronous on each side. If async, reset both pointer and synced_pointer to prevent inconsistency.
  • MTBF calculation: Use the formula from Day 2. Typical dual-clock FIFO: MTBF > 100 years for reasonable frequencies.

🎯 Day 5 takeaways

  • Dual-clock FIFO combines: Gray code pointers, 2-FF synchronizers, empty/full logic
  • Pointers never cross raw: Only Gray code versions cross, synced with 2-FF
  • Full flag: Write domain: wr_ptr_next == rd_ptr_synced
  • Empty flag: Read domain: rd_ptr == wr_ptr_synced
  • FIFO benefits: Decouples clock domains, enables frequency independence, handles bursts
  • Production use: Use library FIFO macros (foundry PDK, Synopsys DesignWare) for certified implementations
  • This is Phase 1 foundation: Days 6–15 cover advanced patterns building on these blocks

FAQ

What is a dual-clock FIFO?

A FIFO buffer with separate write and read clocks. Data is written in one clock domain, read in another, with pointers synchronized using Gray code + 2-FF to prevent invalid states.

Why use Gray code for FIFO pointers?

FIFO pointers are strictly incrementing counters. Gray code ensures only 1 bit changes per increment. Even if that bit is delayed, the result is always valid (no invalid intermediate states).

How do full/empty flags work?

Full: write_ptr_next == read_ptr_synced (both in Gray or binary). Empty: read_ptr == write_ptr_synced. Both comparisons happen in their respective clock domains using synced pointers.

What if I read when FIFO is empty or write when full?

Reading empty: data_out is undefined, no error. Writing full: data is lost, no error. Always check empty/full flags before Read. Always check full before write to prevent data loss.

Can I use a regular single-clock FIFO across clock domains?

No. A single-clock FIFO's pointer logic is not CDC-safe. You must use a dual-clock FIFO or equivalent CDC-safe design. Using a single-clock FIFO will cause data corruption.

What's the MTBF of a dual-clock FIFO?

Depends on synchronizer depth (usually 2-FF) and settling time constants. Typical: 100+ years for reasonable frequencies. Always calculate or verify with library characterization.