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.
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.
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.
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.
// 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:
Pattern Use Case Pros Cons
2-FF Sync Single-bit, stable signals Simple, low latency, small area Loses pulses, fixed 2-cycle latency
Gray Code Multi-bit counters, pointers Safe, no invalid intermediate states Unidirectional, must be sequential
Pulse/Toggle Sync Events, narrow pulses Handles any pulse width Single-bit only
Handshake (req-ack) Data + flow control Atomic, bidirectional Higher latency, more logic
Dual-Clock FIFO Streaming data, frequency decoupling Most flexible, buffering included More 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.