Clock domain crossing and the two-flop synchronizer
Most real designs have more than one clock. The moment a signal generated in one clock domain is read in another, you have a clock domain crossing (CDC), and nothing guarantees that signal changes at a polite time relative to the destination clock. Sooner or later it will change inside the destination flip-flop’s setup/hold window, and the flop’s output goes metastable: not a clean 0, not a clean 1, but hovering in between while the flop’s internal feedback fights it out.
Metastability cannot be prevented. It resolves on its own, and the time it needs is random: usually well under a nanosecond, occasionally longer. So the design question is not “how do I stop it” but “how do I make sure nobody looks at the signal until it has settled”. That reframing is the whole topic, and it’s why this is one of the most reliable interview questions in digital design.
The standard answer
Two flip-flops in series, clocked by the destination domain:
always @(posedge clk_dst) begin
meta <= async_in; // may go metastable: give it a full cycle to settle
sync <= meta; // safe to use in the clk_dst domain
end
The first flop takes the hit. The second flop samples it a full clock period later, by which time the odds of it still being unsettled are astronomically small. Here it is live (edit and press Run):
Interactive schematic by RapidRTL
One rule makes the whole thing work: nothing else may read meta. Its
entire job is to be possibly-wrong for one cycle in private.
Why isn’t one flop enough?
With a single flop, the possibly-still-settling value feeds your logic directly. Two downstream gates can then read the in-between voltage differently, one seeing 0 and the other 1, and your state machine takes a transition that exists in no state diagram. The second flop exists purely to buy time: a full destination clock period for the first flop to resolve.
How safe is “a full period”? The probability that a flop is still metastable decays exponentially with the time you give it, which is why the mean time between failures for a two-flop synchronizer at ordinary clock rates is typically measured in years to millennia, and why each extra flop multiplies it further. Very fast clocks and safety-critical designs sometimes use three flops. Two is the convention because it is almost always enough, and you should be able to say why rather than just recite it.
As a complete module
Interactive schematic by RapidRTL
Three habits worth copying from real codebases:
- Name the flops so reviewers can spot them (
meta/sync, or a_meta/_syncsuffix convention). A synchronizer that doesn’t look like one will eventually be “optimized” by a colleague. - Tell the tools. Vendor attributes (Xilinx’s
ASYNC_REG, for example) keep the two flops placed next to each other, so routing delay doesn’t eat the settling time you just paid a cycle for, and stop retiming from moving logic between them. - In SystemVerilog, use
always_fffor the same reasonalways_combexists: it tells the tools (and readers) the block is sequential on purpose. And the<=assignments are load-bearing: written with=, the two stages collapse into one, as the blocking vs nonblocking guide shows on this exact circuit.
The follow-up trap: what about buses?
The classic next question: “great, now cross this 8-bit counter value”. The
tempting answer, eight synchronizers in parallel, is wrong. Each bit gets its
own tiny wire delay, so when the counter steps from 0111 to 1000 (all
four bits change), the destination can capture any mixture of old and new
bits: 1111, 0000, anything. Every bit arrives safely; the word is
garbage.
The honest answer is that you don’t synchronize a bus bit-by-bit. You cross something that is safe to cross, and there are three standard shapes:
- Gray code, for counters: consecutive values differ in exactly one bit, so the captured word is always either the old or the new value. This is how asynchronous FIFO pointers work.
- A handshake: hold the data steady in the source domain, synchronize a single request bit, and let the destination acknowledge back. Data itself never races because it isn’t sampled until it’s stable.
- An asynchronous FIFO, which packages both ideas and is the standard answer for streaming data.
The VHDL spelling
Same structure, same rules:
process (clk_dst) is
begin
if rising_edge(clk_dst) then
meta <= async_in; -- may go metastable; never used elsewhere
sync <= meta; -- safe in the clk_dst domain
end if;
end process;
The pitfalls checklist
- Never use the middle flop. Its value is allowed to be wrong.
- Synchronize a signal in exactly one place. Two synchronizers fed by the same source can disagree for a cycle, which splits your control logic into two realities.
- Register the signal in the source domain first. Combinational logic glitches, and a synchronizer happily captures a glitch as a real event.
- Buses go via Gray code, a handshake, or a FIFO. Never bit-by-bit.
- Resets cross domains too. Asynchronous assertion with synchronized de-assertion is its own classic, and worth reading up on once this page feels comfortable.
If you’re building up an interview loop, the priority arbiter guide pairs well with this one: between them they cover the two most common “design this on the whiteboard” prompts.