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.
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.
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."
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 | Dir | Width | Description |
|---|---|---|---|
| clk_dst | IN | 1 | Destination clock domain clock |
| rst_dst | IN | 1 | Synchronous reset in destination domain |
| sig_src | IN | 1 | Asynchronous input from source domain (or external pin) |
| sig_dst | OUT | 1 | Synchronised output, safe to use in destination domain |
// 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
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 | Dir | Width | Description |
|---|---|---|---|
| clk_src | IN | 1 | Source clock domain clock |
| clk_dst | IN | 1 | Destination clock domain clock |
| rst_src | IN | 1 | Reset in source domain |
| rst_dst | IN | 1 | Reset in destination domain |
| pulse_src | IN | 1 | One-cycle pulse in source domain to transfer |
| pulse_dst | OUT | 1 | Reconstructed one-cycle pulse in destination domain |
// 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
// 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
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)
| Technique | Use case | Latency | Limitation |
|---|---|---|---|
| 2-flop synchroniser | Single-bit level signals | 2 dst clocks | Pulse may be missed if shorter than 1 dst period |
| Toggle synchroniser | Single-bit pulses of any width | 3 dst clocks | Cannot handle back-to-back pulses faster than dst clock |
| Handshake (req/ack) | Multi-bit data, infrequent | 4–6 clocks | Low throughput |
| Async FIFO | Multi-bit data, high throughput | Variable | Complex to implement correctly |
| Gray-code counter | Pointer crossing in async FIFO | 2 clocks | Only for binary counters |
(* ASYNC_REG = "TRUE" *) to both flops so the tool places them adjacent and suppresses false pathsMetastability 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.
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 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.