HomeFPGA from ScratchDay 17
DAY 17 · CLOCKING

Clock Domain Crossing on FPGA

By EcrioniX · Updated Jun 11, 2026

Real FPGA designs use multiple clocks — an audio clock, a video pixel clock, a USB 60 MHz clock, a 100 MHz system clock. Whenever a signal crosses between these domains, metastability is the silent killer. This lesson shows you the two canonical CDC circuits every FPGA designer must know: the 2-flop synchroniser and the toggle-based pulse synchroniser.

1. What is metastability?

Every flip-flop has a setup window around its clock edge during which the input must not change. If data changes inside this window, the flip-flop enters metastability — its output is neither 0 nor 1 but an intermediate analogue voltage. It will eventually resolve, but the time it takes is probabilistic. During that time, any logic fed by the output may see different values — catastrophic for state machines and data paths.

The only safe rule for CDC

Never connect a signal driven in clock domain A directly to flip-flop logic in clock domain B. Always pass it through a synchroniser. There are no exceptions — not even for "slow" signals or "it worked in simulation."

2. Two-flop synchroniser

The 2-flop synchroniser is the simplest and most common CDC technique. It gives metastability one full clock cycle to resolve before the signal propagates further. For a 100 MHz receiving clock, mean-time-between-failures (MTBF) exceeds millions of years for typical FPGA flip-flops.

Port table — sync_2ff

PortDirWidthDescription
clk_dstIN1Destination clock domain clock
rst_dstIN1Synchronous reset in destination domain
sig_srcIN1Asynchronous input from source domain (or external pin)
sig_dstOUT1Synchronised output, safe to use in destination domain
sync_2ff.v
// sync_2ff.v — 2-Flop Synchroniser for single-bit CDC
// Place ASYNC_REG attribute on both flip-flops to:
//   1. Tell the tool to place them adjacent (minimum inter-FF routing)
//   2. Suppress false timing paths between the two flops
// Always use for: external inputs, push buttons, single-bit flags crossing domains.

module sync_2ff (
    input  wire clk_dst,
    input  wire rst_dst,
    input  wire sig_src,    // async input (from another clock domain)
    output wire sig_dst     // synchronised output
);

// (* ASYNC_REG = "TRUE" *) tells Vivado to apply ASYNC_REG property
(* ASYNC_REG = "TRUE" *) reg ff1, ff2;

always @(posedge clk_dst) begin
    if (rst_dst) begin
        ff1 <= 1'b0;
        ff2 <= 1'b0;
    end else begin
        ff1 <= sig_src;   // first flop: may go metastable
        ff2 <= ff1;       // second flop: resolves before use
    end
end

assign sig_dst = ff2;

endmodule

3. Toggle-based pulse synchroniser

If the source domain sends a one-cycle pulse and the destination clock is slower, that pulse might be missed by the 2-flop synchroniser. The toggle synchroniser solves this: the sender toggles a register on each pulse event. The receiver detects the edge (XOR of current and previous synchronised value) and generates a local pulse. Every toggle is reliably detected regardless of clock ratio.

Port table — pulse_sync

PortDirWidthDescription
clk_srcIN1Source clock domain clock
clk_dstIN1Destination clock domain clock
rst_srcIN1Reset in source domain
rst_dstIN1Reset in destination domain
pulse_srcIN1One-cycle pulse in source domain to transfer
pulse_dstOUT1Reconstructed one-cycle pulse in destination domain
pulse_sync.v
// pulse_sync.v — Toggle-based pulse synchroniser
// Safely crosses a single-cycle pulse from clk_src to clk_dst
// Works even when clk_dst is slower than clk_src.

module pulse_sync (
    input  wire clk_src,
    input  wire clk_dst,
    input  wire rst_src,
    input  wire rst_dst,
    input  wire pulse_src,   // 1-cycle pulse in source domain
    output wire pulse_dst    // 1-cycle pulse in destination domain
);

// ---- Source domain: toggle register on each pulse ----
reg toggle_src;
always @(posedge clk_src) begin
    if (rst_src)
        toggle_src <= 1'b0;
    else if (pulse_src)
        toggle_src <= ~toggle_src;   // toggle on each event
end

// ---- Cross the toggle signal with a 2-flop synchroniser ----
(* ASYNC_REG = "TRUE" *) reg sync1, sync2, sync3;
always @(posedge clk_dst) begin
    if (rst_dst) begin
        sync1 <= 0; sync2 <= 0; sync3 <= 0;
    end else begin
        sync1 <= toggle_src;
        sync2 <= sync1;
        sync3 <= sync2;
    end
end

// ---- Detect edge in destination domain = reconstruct pulse ----
assign pulse_dst = sync2 ^ sync3;   // XOR detects any toggle

endmodule

4. Testbench — tb_sync_2ff.v

tb_sync_2ff.v
// tb_sync_2ff.v — self-checking testbench for sync_2ff
// Drives async signal, verifies it appears synchronised after 2 clocks
`timescale 1ns/1ps

module tb_sync_2ff;

reg clk_dst = 0;
reg rst_dst = 1;
reg sig_src = 0;
wire sig_dst;

sync_2ff dut(.clk_dst(clk_dst),.rst_dst(rst_dst),.sig_src(sig_src),.sig_dst(sig_dst));

always #5 clk_dst = ~clk_dst;  // 100 MHz dst clock

integer pass_cnt = 0, fail_cnt = 0;

task check;
    input expected;
    begin
        if (sig_dst === expected) begin
            $display("PASS: sig_dst=%b (expected %b)", sig_dst, expected);
            pass_cnt = pass_cnt + 1;
        end else begin
            $display("FAIL: sig_dst=%b (expected %b)", sig_dst, expected);
            fail_cnt = fail_cnt + 1;
        end
    end
endtask

initial begin
    $dumpfile("tb_sync_2ff.vcd");
    $dumpvars(0, tb_sync_2ff);

    // Reset
    repeat(4) @(posedge clk_dst);
    rst_dst = 0;

    // Initially sig_src=0, sig_dst should be 0
    repeat(4) @(posedge clk_dst);
    check(1'b0);

    // Assert sig_src asynchronously (between clock edges)
    #3; sig_src = 1;

    // After 1 clock: ff1 captures, ff2 still 0
    @(posedge clk_dst); #1;
    $display("INFO: After 1 clk, sig_dst=%b (ff2 may still be 0)", sig_dst);

    // After 2 clocks: ff2 should be 1
    @(posedge clk_dst); #1;
    check(1'b1);

    // Deassert sig_src
    #2; sig_src = 0;
    @(posedge clk_dst); #1;
    @(posedge clk_dst); #1;
    check(1'b0);

    // Rapid toggling: sig_src pulses for 3 ns (less than 1 clock period)
    // 2-flop may or may not catch it — this is expected behaviour
    #2; sig_src = 1; #3; sig_src = 0;
    repeat(4) @(posedge clk_dst); #1;
    $display("INFO: After short glitch, sig_dst=%b (may be 0 or 1 — both valid)", sig_dst);
    pass_cnt = pass_cnt + 1;  // not an error — CDC expected behaviour

    if (fail_cnt == 0)
        $display("\nALL TESTS PASSED (%0d/%0d)", pass_cnt, pass_cnt+fail_cnt);
    else
        $display("\nFAILED: %0d passed, %0d failed", pass_cnt, fail_cnt);

    $finish;
end

initial #2000 begin $display("TIMEOUT"); $finish; end

endmodule

5. Expected output

PASS: sig_dst=0 (expected 0)
INFO: After 1 clk, sig_dst=0 (ff2 may still be 0)
PASS: sig_dst=1 (expected 1)
PASS: sig_dst=0 (expected 0)
INFO: After short glitch, sig_dst=0 (may be 0 or 1 — both valid)

ALL TESTS PASSED (4/4)

6. CDC techniques comparison

TechniqueUse caseLatencyLimitation
2-flop synchroniserSingle-bit level signals2 dst clocksPulse may be missed if shorter than 1 dst period
Toggle synchroniserSingle-bit pulses of any width3 dst clocksCannot handle back-to-back pulses faster than dst clock
Handshake (req/ack)Multi-bit data, infrequent4–6 clocksLow throughput
Async FIFOMulti-bit data, high throughputVariableComplex to implement correctly
Gray-code counterPointer crossing in async FIFO2 clocksOnly for binary counters

Key Takeaways

Frequently Asked Questions

What is metastability and why is it dangerous?

Metastability occurs when a flip-flop's setup or hold time is violated — the output gets stuck at an intermediate voltage. It eventually resolves unpredictably. The danger is that different downstream flip-flops may see different values, corrupting state machines. A two-flop synchroniser gives the metastability time to resolve before it reaches logic.

Why can't you directly connect signals between clock domains?

Signals can change at any time relative to the receiving clock. If the transition falls near the clock edge, the receiving flip-flop enters metastability. This causes random intermittent failures nearly impossible to debug. Always use a synchroniser — 2-flop for level signals, toggle-based for pulses, async FIFO for data buses.

When should I use a toggle synchroniser?

When the source clock is faster than the destination or the pulse is shorter than one destination clock period. The sender toggles a register on each event; the receiver detects any edge. Every toggle is captured even if the destination clock is 10× slower.

← Previous
Day 16: Timing Constraints