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:
// 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.
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 |
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).
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.
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).
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.
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.
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.
Depends on synchronizer depth (usually 2-FF) and settling time constants. Typical: 100+ years for reasonable frequencies. Always calculate or verify with library characterization.