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:
Write domain full flag:
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.
// 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: 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.
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.