HomeCDC GuideDay 5
DAY 5 · CDC FUNDAMENTALS

Dual-Clock FIFOs — The Cornerstone Pattern

By EcrioniX · Updated Jul 31, 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:

Write domain full flag:

⚠️ A tempting mistake

Checking write_ptr == read_ptr_synced for plain equality looks reasonable but is wrong — that condition is what EMPTY looks like, not full. With the read pointer parked, an 8-deep FIFO built that way won't block writes until the write pointer has wrapped all the way past 15, silently allowing far more than 8 unread writes through and corrupting data. FULL means "lapped by exactly one DEPTH," which requires computing the actual occupancy, not just comparing the two pointers for equality.

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:

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

🎯 Day 5 takeaways

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: occupancy, computed as (write_ptr − read_ptr_synced), equals DEPTH — not a plain pointer-equality check, which is actually the empty condition. 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.

Previous
← Day 4: Pulse Synchronizers