What Happens Without a Reset Synchronizer
A large RTL design has thousands of flip-flops, all sharing the same active-low reset net rst_n. When power comes on — or when a fault triggers reset — the POR cell asserts rst_n=0 asynchronously. So far, so good.
The danger is on the way out of reset. When rst_n goes back to 1, every flip-flop in the design samples this transition. If the transition happens near a clock rising edge — within the setup or hold window of any FF — some FFs may capture the new value one clock cycle before or after their neighbours. A design that was supposed to start with all registers at zero instead starts with a mix of 0s and Xs, and the bugs that follow are intermittent, temperature-sensitive, and nearly impossible to reproduce in simulation.
Reset Deassertion Near a Clock Edge
If rst_n rises within the setup window of a clock edge, the FF may go metastable — neither 0 nor 1. Its neighbour, whose setup window opens a few picoseconds later, samples a clean 1. The two FFs now disagree on the initial state.
Inconsistent Initial State
State machines with multiple FFs may enter an illegal encoded state. Counters start at non-zero values. Handshake logic that requires both sides to begin in "idle" state finds one side already in "active" — protocol violation from cycle 1.
Invisible in RTL Simulation
RTL simulation assigns reset synchronously and deterministically — no FF ever samples reset near a clock edge unless you explicitly force it. The bug only appears at speed, in real silicon, when process variation shifts the exact edge alignment into the danger zone.
The 2-FF Reset Synchronizer Circuit
The fix is elegant: pass reset through two flip-flops clocked by the destination domain. Both FFs have asynchronous reset from the raw input — so assertion is still immediate. But deassertion can only propagate through the chain on clock edges — two cycles after the raw reset releases.
Key insight: Both FFs have their async reset connected to RST_N_IN. When reset asserts (0), both FFs reset immediately — no waiting for a clock edge. When reset deasserts (1), FF1's D input is tied to VDD, so on the first clock edge after release, FF1_Q=1. On the second clock edge, FF2 captures FF1_Q=1, and RST_N_SYNC goes high. Clean, glitch-free, two cycles of latency.
Why Async Assert Is Safe
Reset assertion (driving reset active) is safe to do asynchronously because it only drives flip-flops into their known reset state. There is no ambiguity: every FF responds identically to a low RST_N — output goes to 0, regardless of the clock.
More importantly, reset assertion must be asynchronous. Consider a chip experiencing a power fault or brownout. The clock may itself be unreliable or stopped. Waiting for a clock edge to propagate reset would mean the chip stays in an unknown state until a clock arrives — exactly when it is most dangerous. Async assert gives you guaranteed, immediate protection.
Rule: Assert reset asynchronously. Always. The clock cannot be trusted during fault events.
Why Sync Deassert Is Critical
Reset deassertion is the mirror image — and the dangerous one. When rst_n goes from 0 to 1, every flip-flop in the design is released from reset on the next sampling event. If different FFs sample this release at different clock edges — because the release edge landed in some FFs' setup window but not others' — the chip wakes up with inconsistent register contents.
rst_n releases near clock edge N• FF_A (setup window open): samples at edge N, Q=0→1
• FF_B (setup window closed): samples at edge N+1, Q=0→1
• One cycle skew between two "synchronous" registers
• State machine may enter illegal encoded state
• Handshake may fire prematurely
rst_n releases at any time• FF1 samples release on clock edge N or N+1
• FF2 samples FF1_Q=1 on edge N+1 or N+2
• RST_SYNC goes high cleanly on one specific edge
• ALL downstream FFs using RST_SYNC deassert on the same edge
• Consistent, deterministic initial state guaranteed
The latency tradeoff: Sync deassert adds 2 clock cycles of latency to reset release. For most designs this is negligible. At 1 GHz that is 2 ns — the chip spent milliseconds in assertion already. Only designs with sub-nanosecond reset requirements (rare) would need a different approach.
Glitch Filtering — A Free Bonus
A reset glitch is an unintended brief pulse on the reset line — from power supply noise, an ESD event, a board-level ringing, or a metastable reset controller output. Without synchronization, a glitch as short as one gate delay can momentarily reset flip-flops and corrupt state mid-operation.
The 2-FF synchronizer filters glitches automatically. Because FF1 only captures its input on clock edges, a glitch that arrives and disappears between two clock edges is never sampled. The synchronizer behaves like a minimum-pulse-width filter: only a reset deassertion that stays high for at least one full clock period — long enough to be captured at a clock edge — propagates through.
Minimum capturable pulse width ≈ one full clock period. At 500 MHz (2 ns period), glitches shorter than ~2 ns are filtered. At 100 MHz (10 ns period), glitches shorter than ~10 ns are filtered. Higher frequency = tighter glitch filter. Use the "Inject Glitch" button in the lab above to see this in action.
Verilog — From Wrong to Right
The wrong pattern fans out raw rst_n directly to every flip-flop. The correct pattern puts a synchronizer at the entry point of each clock domain.
// WRONG: rst_n_in fans out directly to thousands of flip-flops. // If rst_n_in deasserts near a clock edge, some FFs exit reset // one cycle before others → inconsistent initial state. module bad_design ( input clk, rst_n_in, output [7:0] data_out ); reg [7:0] counter; reg fsm_idle; // ← rst_n_in used raw — no synchronizer! always @(posedge clk or negedge rst_n_in) if (!rst_n_in) counter <= 8'h00; else counter <= counter + 1; // ← same raw rst_n_in — may deassert one cycle earlier than counter always @(posedge clk or negedge rst_n_in) if (!rst_n_in) fsm_idle <= 1'b1; else fsm_idle <= (counter == 8'hFF); endmodule
// 2-FF reset synchronizer: async assert, sync deassert. // Instantiate once per clock domain at the domain entry point. // DONT_TOUCH prevents synthesis from optimising away the chain. module rst_sync #(parameter STAGES = 2) ( input clk, // destination domain clock — must be UNGATED input rst_n_in, // raw async reset (active-low) output rst_n_sync // synchronized reset for all logic in this domain ); (* DONT_TOUCH = "true", ASYNC_REG = "true" *) reg [STAGES-1:0] chain; always @(posedge clk or negedge rst_n_in) if (!rst_n_in) chain <= {STAGES{1'b0}}; // async assert: all 0 else chain <= {chain[STAGES-2:0], 1'b1}; // sync deassert: shift in 1s assign rst_n_sync = chain[STAGES-1]; endmodule // Usage — top-level wires one synchronizer per domain: wire rst_n_core; rst_sync #(.STAGES(2)) u_rst_core ( .clk (clk_core), .rst_n_in (por_rst_n), // from POR cell or pin .rst_n_sync(rst_n_core) ); // All flip-flops in clk_core domain use rst_n_core — never por_rst_n directly.
// Each clock domain gets its own rst_sync instance. // NEVER share a synchronized reset across two different clock domains. module reset_dist ( input por_rst_n, // raw power-on reset from POR cell input clk_core, // 1 GHz core domain input clk_io, // 200 MHz IO domain input clk_usb, // 480 MHz USB domain output rst_n_core, output rst_n_io, output rst_n_usb ); // Each domain: independent sync chain, independent clock rst_sync #(2) u_core (.clk(clk_core), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_core)); rst_sync #(2) u_io (.clk(clk_io), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_io)); rst_sync #(3) u_usb (.clk(clk_usb), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_usb)); // USB uses 3 stages: 480 MHz leaves only 2 ns per stage — extra margin helps endmodule
// Brown-out: VDD dips below FF min operating voltage. // POR cell asserts rst_n whenever VDD < threshold. // Synchronizer FFs MUST use the free-running (ungated) clock. // If clock is gated off during brown-out, sync chain never propagates release. module por_reset_sync ( input clk_free, // ungated oscillator — always running input vdd_good, // from analog POR: 1 when VDD > threshold output rst_n_sync ); (* DONT_TOUCH = "true", ASYNC_REG = "true" *) reg [1:0] ff; always @(posedge clk_free or negedge vdd_good) if (!vdd_good) ff <= 2'b00; // VDD dip → instant assert else ff <= {ff[0], 1'b1}; // VDD OK → sync release assign rst_n_sync = ff[1]; endmodule // ⚠ Constraint required — tell STA the sync chain is intentional: // set_false_path -from [get_ports vdd_good] -to [get_pins ff_reg[0]/D] // set_max_delay -datapath_only 2 -from [get_pins ff_reg[0]/Q] \ // -to [get_pins ff_reg[1]/D]
The Five Reset Synchronizer Rules
One Synchronizer Per Domain
Never share a synchronized reset across two clock domains. Each domain must synchronize reset independently using its own clock. A reset synchronized to CLK_A is not safe in CLK_B.
Use the Ungated Clock
The synchronizer FFs must connect to the free-running (ungated) clock. If the clock is gated off — which can happen during power-down or debug — the synchronizer can never propagate reset deassertion.
No Logic Between Stages
Insert zero combinational logic between the flip-flop stages. Any gate introduces delay that can cause a hold violation within the sync chain itself — defeating the purpose of the synchronizer.
DONT_TOUCH Attribute
Mark the synchronizer with (* DONT_TOUCH = "true" *) or set_dont_touch. Without this, synthesis may merge, retime, or remove stages it considers redundant.
False Path on Input
Constrain the crossing from rst_n_in to FF1's D-pin as a false path in SDC. The raw reset is asynchronous — STA cannot meaningfully analyse a timing path from it to a synchronous FF.
Questions Engineers Actually Get Asked
Deassert (going inactive): releases FFs to capture new data on the next clock edge. If deassertion happens within the setup or hold window of a clock edge, some FFs may capture the released value one cycle before their neighbours — creating inconsistent initial conditions that corrupt state machine encoding, counter values, and handshake logic from the first operational cycle.
The result: two FFs that were both in reset (Q=0) one cycle ago now have Q=0 and Q=1 — one cycle out of sync with each other. A 4-bit encoded state machine that needs to start in state
4'b0000 may start in 4'b0001 or 4'b0010, which may be an illegal encoding. The corruption is deterministic given a specific silicon die and operating conditions, but varies die-to-die — making it appear random in production.
Three stages are sometimes used for high-reliability applications (automotive, space), at advanced nodes below 7 nm where τ is smaller (less resolution time per stage), or at very high clock rates (>2 GHz) where the resolution window per stage narrows. The reset synchronizer's MTBF calculation follows the same formula as any 2-FF CDC synchronizer:
MTBF = exp(T_res/τ) / (f_clk × f_change × T_w).
Always connect the synchronizer to the free-running root clock of the domain — the clock that runs continuously, before any ICG (Integrated Clock Gate) cells. In clock-gating-heavy designs this usually means connecting to the clock buffer output before the first gate in the clock tree.
1.
set_false_path -from [get_ports rst_n_in] — the raw async reset is not a synchronous signal; STA should not analyze a timing path from it to FF1's D-pin.2.
set_max_delay -datapath_only [period] -from [get_pins chain_reg[0]/Q] -to [get_pins chain_reg[1]/D] — within the synchronizer chain, ensure the inter-stage path meets hold without being over-constrained by clock uncertainty.3. Mark the synchronizer with
set_dont_touch or the (* DONT_TOUCH = "true" *) attribute to prevent synthesis from optimizing or merging the chain stages.
• ResetSync: a reset signal that reaches a flip-flop in a different clock domain without a proper synchronizer
• GatedClockReset: a synchronizer FF detected on a gated (non-free-running) clock path
• ResetDivergence: a reset that is synchronized into domain A and then directly used in domain B without re-synchronization — the same structural bug as CDC divergence, applied to resets
These checks require the user to define clock domain intent via
define_domain and define_reset pragmas or via a domain intent file (DIF). Without domain intent, SpyGlass cannot distinguish intentional from unintentional crossings.