VHDL · Chapter 17.5 · FPGA-Oriented VHDL Design
Clock Domain Crossing
As soon as a design has two unrelated clocks, passing a signal between them becomes dangerous. A destination flip-flop can sample a signal from another domain mid-transition, violate setup and hold, and go metastable, hovering at an indeterminate level before resolving unpredictably. You cannot eliminate metastability, only make it astronomically unlikely to propagate. The standard fix for a single-bit crossing is the two-flip-flop synchronizer in the destination domain: the first flop may go metastable but is given a full clock to settle before the second flop samples it. This works only for single-bit or slow signals. You cannot put a synchronizer on each bit of a multi-bit bus, because the bits resolve independently and you capture an incoherent value. This lesson covers metastability, the two-flip-flop synchronizer, and why buses need Gray code or a handshake and FIFO.
Foundation15 min readVHDLFPGACDCMetastabilitySynchronizerClock Domains
1. Engineering intuition — sampling something that isn't ready
A flip-flop promises a clean 0/1 only if its input is stable around the clock edge (setup/hold). A signal
from another clock domain has no such courtesy — it can change at any time relative to the destination clock,
including exactly at the sampling edge. When that happens the flop can go metastable: balanced between 0 and
1, resolving to one or the other only after an unpredictable delay. You cannot prevent this, but you can give
the glitch time to die out before anything downstream uses it — which is what a synchronizer does: sacrifice
the first flop to metastability, then wait a clock for it to settle before the second flop reads it. The whole art
of CDC is making sure a metastable value never reaches logic that acts on it.
2. Formal explanation — metastability and the two-FF synchronizer
-- METASTABILITY: a destination FF sampling an ASYNCHRONOUS input near its edge may go metastable
-- (output indeterminate for a while). Cannot be eliminated → reduce propagation probability (MTBF).
-- TWO-FLIP-FLOP (double) SYNCHRONIZER for a SINGLE-BIT signal, in the DESTINATION domain:
signal sync_ff1, sync_ff2 : std_logic;
attribute async_reg : string;
attribute async_reg of sync_ff1, sync_ff2 : signal is "true"; -- mark as synchronizer FFs (17.4/16.8)
process (dst_clk) begin
if rising_edge(dst_clk) then
sync_ff1 <= async_in; -- FF1 may go METASTABLE (samples the async signal)
sync_ff2 <= sync_ff1; -- FF2 samples FF1 a full clock LATER → metastability has settled
end if;
end process;
sync_out <= sync_ff2; -- safe to use in the destination domain
-- RULE: ONLY for single-bit / slow-changing signals. A multi-bit BUS canNOT use parallel 2-FF
-- synchronizers (each bit resolves independently → an INCOHERENT combined value).A destination FF sampling an asynchronous input can go metastable; the two-FF synchronizer gives the first FF a full clock to settle before the second reads it, cutting propagation probability (raising MTBF). It is valid only for single-bit crossings — parallel synchronizers on a bus capture independently-resolved, incoherent bits.
3. Production usage — what to synchronize, and what not to
-- SAFE with a 2-FF synchronizer: a SINGLE control bit / flag / slow status.
-- e.g. a request pulse, an enable, a "done" flag — one bit, double-sync it in the destination domain.
-- NOT SAFE with parallel 2-FF synchronizers: a MULTI-BIT BUS (data, address, counter, pointer).
-- Each bit's synchronizer resolves on its own; on the capture edge different bits may be old/new
-- → the assembled value is GARBAGE (never existed). Use instead:
-- • GRAY CODE for pointers/counters (only ONE bit changes per step → at most one bit is uncertain)
-- • HANDSHAKE (req/ack across the boundary) to pass a held, stable bus value
-- • ASYNC FIFO (dual-clock) for streaming data (next lesson, 17.6)
-- ALWAYS: synchronize an async signal BEFORE any logic uses it. Never feed it raw into a comparator/FSM.What hardware does this become? The two-FF synchronizer is two flip-flops in series in the destination
domain (marked async_reg so the tools place them close and constrain them) — the standard single-bit CDC cell.
For buses it becomes something larger: a Gray-coded pointer crossing (used in async FIFOs), a handshake
(req/ack with the data held stable while it crosses), or a dual-clock FIFO for streams. The non-negotiable rule
in silicon: an asynchronous signal must pass through a synchronizer before any combinational logic or FSM uses
it — feeding it raw means metastability propagates into your logic and the design fails intermittently and
un-reproducibly.
4. Structural interpretation — the two-FF synchronizer across domains
5. Simulation interpretation — metastable first stage settling
Two-FF synchronizer: FF1 metastable, FF2 clean
8 cycles6. Debugging example — synchronizing a bus bit-by-bit
Expected: a multi-bit value crosses cleanly between domains. Observed: the destination occasionally reads a value that never existed on the source (some bits old, some new), causing rare, un-reproducible failures — or an async signal fed straight into an FSM makes the FSM jump to impossible states. Root cause: a multi-bit bus was crossed with parallel two-FF synchronizers (one per bit), so each bit resolved independently and the captured word mixed old and new bits; or an async signal was used by logic before being synchronized. Fix: synchronize single bits with a two-FF cell; cross buses coherently — Gray code for pointers/counters (only one bit changes per step), a handshake to hold the bus stable while it crosses, or a dual-clock FIFO (17.6); and never feed an async signal into logic before it is synchronized. Engineering takeaway: two-FF synchronizers are for single bits only — parallel-syncing a bus captures incoherent values; use Gray code or a handshake/FIFO for multi-bit crossings.
-- BUG: per-bit 2-FF sync on a bus → bits resolve independently → incoherent value.
-- for i in bus'range loop bus_s1(i) <= bus_a(i); bus_s2(i) <= bus_s1(i); end loop; -- WRONG for a bus
-- FIX: cross the bus coherently — Gray-coded pointer, or a handshake/async FIFO (17.6).
-- (single CONTROL bits use the 2-FF synchronizer; the DATA crosses via FIFO/handshake)7. Common mistakes & what to watch for
- Bit-by-bit bus synchronizing. Parallel 2-FF synchronizers on a bus give incoherent values; use Gray code or a handshake/FIFO.
- Using an async signal before syncing. Always pass it through a synchronizer first; never feed it raw to logic/FSMs.
- Single synchronizer flop. One FF is not enough; use two (or more in very high-speed/critical designs) to settle metastability.
- Not marking synchronizer FFs. Use
async_reg(and proper CDC constraints) so tools place them close and treat the path correctly. - Forgetting the source must hold. For handshake-based bus crossings, the source must keep data stable until the destination has captured it.
8. Engineering insight & continuity
Clock domain crossing is governed by metastability: a flop sampling an asynchronous input can become indeterminate, so a two-FF synchronizer gives it a clock to settle — valid for single-bit signals only, since a bus crossed bit-by-bit resolves into an incoherent value. Single control bits double-sync; multi-bit data needs Gray code or a handshake/FIFO, and nothing async touches logic before synchronization. Those coherent multi-bit mechanisms are exactly the next lesson: CDC FIFOs and Handshake Synchronizers — passing buses and streams safely between clock domains.