Reset is the single most overlooked CDC problem in chip design. Most engineers understand that data signals need synchronizers — but reset? Reset looks global, looks simple, and hides a dangerous trap: asynchronous resets must be asserted immediately but released synchronously to every clock domain. Get this wrong and your chip initializes inconsistently, produces corrupt state after power-on, and ships bugs that only appear during reset sequences.
Most engineers treat reset as a simple global signal — connect it everywhere and it just works. That intuition breaks down in any design with multiple independent clocks.
Here is the problem. Your chip has a power-on reset (POR) signal, rst_n, generated by an always-on domain or a power management controller. This signal is asynchronous with respect to every clock domain in the chip. When reset de-asserts — when it goes from 0 back to 1 and flip-flops are released — that transition must be seen by each flip-flop in a controlled, synchronous way relative to its own clock.
If the reset release is not synchronized to the clock, each flip-flop in the domain may exit reset at a slightly different absolute time. Whether they all exit on the same clock edge depends entirely on the net routing delay from the reset source to each flip-flop — and that delay is non-deterministic across process, voltage, and temperature corners. A state machine whose flip-flops release in different clock cycles is in an undefined, inconsistent state from the very first cycle of operation.
This is not theoretical. Designs that share a raw POR across multiple clock domains without synchronizers regularly exhibit initialization failures in silicon — the chip boots correctly 95% of the time and randomly hangs the other 5%, depending on power ramp rate, clock startup timing, and PVT conditions. These bugs are extremely difficult to debug because they do not reproduce in simulation.
Asserting reset (going low) is safe to do asynchronously — flip-flops' async reset pins are designed for it, and forcing every flip-flop to 0 immediately is exactly what you want during a power event. De-asserting reset (going from 0 to 1) is the CDC hazard. If de-assertion travels through combinational logic to reach flip-flops at different times relative to their clock edges, some flip-flops exit reset one cycle earlier than others. The reset synchronizer eliminates this race by re-timing the release through a 2-FF chain clocked by the destination domain.
The industry-standard rule for reset handling in multi-clock designs is captured in one sentence: assert asynchronously, de-assert synchronously.
rst_n_in goes low, all flip-flops in the domain are forced to their reset state. No waiting for a clock edge. This is critical for power-down events and glitch recovery — you want the domain to stop as fast as physically possible.rst_n_in goes high, do not release the reset output immediately. Instead, re-time the release through a 2-FF chain clocked by the destination domain's clock. The domain exits reset after exactly two clock edges following the release of rst_n_in.Why does de-assertion through combinational logic cause problems? Because every flip-flop has an asynchronous reset pin. When that pin changes state (de-asserts), the flip-flop transitions based on its own internal timing and the exact moment the pin voltage crosses the logic threshold. If the de-assertion propagates through a long net with non-uniform delay, two flip-flops in the same domain may see the reset pin go high in adjacent — or even the same — clock cycle, with no guarantee they all release on the same rising edge. The 2-FF synchronizer forces all downstream flip-flops to see a clean, stable de-assertion exactly two cycles after the input goes high.
The circuit is a 2-flip-flop shift register where both flip-flops carry the incoming reset as their asynchronous reset and are clocked by the destination domain clock. The data input of FF1 is always tied to logic 1. This is the key insight: in steady state (no reset), the chain is always shifting in 1s. When reset asserts, the chain is immediately cleared to 0s. When reset de-asserts, the 1s propagate through the chain over two clock edges.
// Basic 2-FF reset synchronizer
// rst_n_in : asynchronous input reset (active-low)
// clk : destination domain clock
// rst_n_sync: synchronized reset output (active-low)
module reset_sync_basic (
input wire clk,
input wire rst_n_in,
output wire rst_n_sync
);
reg q1, q2;
always @(posedge clk or negedge rst_n_in) begin
if (!rst_n_in)
{q2, q1} <= 2'b00; // assert: both FFs reset immediately (async)
else
{q2, q1} <= {q1, 1'b1}; // de-assert: shift 1 through chain over 2 cycles
end
assign rst_n_sync = q2; // output de-asserts 2 cycles after rst_n_in goes high
endmodule
Trace through the behavior step by step:
negedge rst_n_in sensitivity fires immediately, regardless of clock. Both q1 and q2 are forced to 0. rst_n_sync goes low immediately. All downstream flip-flops using rst_n_sync as their async reset are also forced to 0 immediately. Fast, clean, fully asynchronous.q1 shifts in 1'b1, q2 stays 0 (it shifts in the old value of q1, which was 0). rst_n_sync remains low — domain still in reset.q2 now shifts in the 1 that is sitting in q1. rst_n_sync goes high. The domain exits reset exactly two full clock cycles after rst_n_in de-asserted.The first flip-flop (FF1) absorbs any metastability caused by the rst_n_in de-assertion edge arriving near a clock edge. By the time FF1's output propagates to FF2's D-input, it has a full clock period to resolve to a valid 0 or 1. FF2's output is guaranteed stable. Two stages is the industry minimum; critical designs at aggressive nodes (sub-5nm, ultra-high frequency) sometimes use three stages for extra MTBF margin.
A real chip with three clock domains needs three independent reset synchronizers — one per domain. The same rst_n_in feeds all three, but each synchronizer uses its own clock. This means each domain exits reset independently, on its own clock boundary, with no coupling between domains.
// Three independent reset synchronizers for three clock domains
// Each uses its own clock. All share the same POR input.
// Never share one synchronized reset output across two different domains.
module multi_domain_reset (
input wire clk_sys, // system clock (e.g. 200 MHz)
input wire clk_ddr, // DDR ctrl clock (e.g. 333 MHz)
input wire clk_pcie, // PCIe clock (e.g. 250 MHz)
input wire por_rst_n, // power-on reset (active-low, async source)
output wire rst_n_sys, // synchronized reset for sys domain
output wire rst_n_ddr, // synchronized reset for ddr domain
output wire rst_n_pcie // synchronized reset for pcie domain
);
// Each synchronizer is fully independent — uses its own clock
reset_sync_basic u_sys_rst (.clk(clk_sys), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_sys));
reset_sync_basic u_ddr_rst (.clk(clk_ddr), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_ddr));
reset_sync_basic u_pcie_rst (.clk(clk_pcie), .rst_n_in(por_rst_n), .rst_n_sync(rst_n_pcie));
// Flip-flops in clk_sys domain --> use rst_n_sys
// Flip-flops in clk_ddr domain --> use rst_n_ddr
// Flip-flops in clk_pcie domain --> use rst_n_pcie
// NEVER cross-connect: e.g., clk_sys FF with rst_n_ddr is a CDC violation
endmodule
The three rst_n_xxx outputs are not guaranteed to de-assert at exactly the same absolute time, because the three clocks are independent. rst_n_sys de-asserts two clk_sys cycles after por_rst_n goes high; rst_n_ddr de-asserts two clk_ddr cycles after. In wall-clock time these differ by nanoseconds. That is fine — as long as logic in each domain only uses its own synchronized reset, there is no cross-domain inconsistency.
In designs where domains depend on each other, the order in which domains exit reset matters. A downstream block that relies on an upstream block's initialization must not exit reset first.
Classic example: a CPU domain that fetches instructions from an SRAM domain. If the CPU's reset de-asserts before the SRAM domain is fully initialized, the CPU issues fetch requests to an SRAM that has not yet completed its reset sequence. The SRAM may return garbage values or enter an illegal state.
The correct ordering for a typical SoC:
This sequencing is enforced either by the power management unit (PMU) — which gates the reset synchronizer input of domain N until domain N-1 signals initialization complete — or through UPF power intent. In UPF-based flows, reset sequencing is specified explicitly using set_domain_supply_net and add_power_state commands, and the formal verification tool checks that no domain violates the sequence.
| Bug | Root Cause | Symptom |
|---|---|---|
| Shared reset net across multiple domains | rst_n connected directly to flip-flops in clk_a and clk_b domains without per-domain synchronizers |
Intermittent initialization failure; state machine starts in invalid encoding |
| Missing synchronizer for new clock domain | Engineer adds a new IP block during integration but omits a reset synchronizer for its clock | Domain occasionally misses reset; partial initialization; rare field failures |
| Reset glitch propagation | External reset pin bounces; glitch shorter than one clock period partially resets some flip-flops | Random register corruption without full re-initialization; hard to reproduce |
| Active-high vs active-low mismatch | Synchronizer coded active-low, connected to active-high reset source without inversion | Domain stays in reset forever, or never resets at all |
| Synchronizer clocked by gated clock | Reset synchronizer clocked by a gated version of the domain clock that is off during reset | Domain never exits reset because the clock is not running to shift in de-assertion |
The reset synchronizer must use the free-running version of the domain clock, not a clock gated by logic that depends on reset state. If the clock is disabled when reset de-asserts, the synchronizer can never shift in the de-assertion — the domain is permanently stuck in reset. Connect the synchronizer to the ungated, always-running version of the clock, before any clock gate in the path.
External reset pins (test points, board-level reset buttons, hot-plug connectors) are notorious for producing glitches — brief spurious pulses caused by contact bounce or inductive coupling on PCB traces. A glitch shorter than one clock period can partially reset some flip-flops while leaving others untouched, causing subtle corruption that looks like a software bug but originates in hardware.
The solution is a reset glitch filter: a counter-based debounce circuit that only accepts a reset assertion after it has been stable for N consecutive clock cycles. Short glitches are rejected; only a sustained reset signal passes through.
// Reset glitch filter — debounce external reset_n input
// Requires rst_n_ext to be asserted (low) for FILTER_CYCLES consecutive
// always-on clock cycles before passing the reset through.
// De-assertion (going high) passes immediately without delay.
module reset_glitch_filter #(
parameter FILTER_CYCLES = 8
)(
input wire clk_ao, // always-on reference clock (32 kHz or divided sysclk)
input wire rst_n_ext, // raw external reset (may have glitches)
output reg rst_n_filtered // debounced reset output
);
localparam CNT_W = $clog2(FILTER_CYCLES + 1);
reg [CNT_W-1:0] cnt;
always @(posedge clk_ao) begin
if (!rst_n_ext) begin
// External reset asserted: increment debounce counter
if (cnt < FILTER_CYCLES)
cnt <= cnt + 1'b1;
// Latch reset output only after counter reaches threshold
if (cnt == (FILTER_CYCLES - 1))
rst_n_filtered <= 1'b0;
end else begin
// External reset released: clear counter and output immediately
cnt <= {CNT_W{1'b0}};
rst_n_filtered <= 1'b1;
end
end
endmodule
The filtered output is then fed into the reset synchronizer chain for each clock domain. The debounce counter itself is clocked by an always-on reference clock — the RTC oscillator or the top-level clock divided down — that is guaranteed to be running regardless of the reset state of any other domain.
Asynchronous resets create a specific problem for scan-based DFT: because the reset signal is asynchronous, it can override scan shift operations at any clock edge. During scan shift, flip-flops must hold their state to propagate patterns correctly — an asynchronous reset asserting during scan would corrupt the entire scan chain and invalidate all test patterns.
The standard solution is to gate the asynchronous reset with scan_enable using an OR gate: when scan is active, the OR forces the reset pin high (inactive), blocking it from the synchronizer. In functional mode (scan_en=0), the gate is transparent and the circuit behaves normally.
// DFT-aware reset synchronizer
// During scan shift (scan_en=1), reset is blocked to prevent
// corruption of scan chain patterns during shift or capture.
module reset_sync_dft (
input wire clk,
input wire rst_n_in, // async reset input (active-low)
input wire scan_en, // scan enable from ATPG controller
output wire rst_n_sync
);
// Gate: when scan_en=1 force reset inactive (high) to block async reset
// When scan_en=0 (functional mode), gate is transparent
wire rst_n_gated = rst_n_in | scan_en;
reg q1, q2;
always @(posedge clk or negedge rst_n_gated) begin
if (!rst_n_gated)
{q2, q1} <= 2'b00;
else
{q2, q1} <= {q1, 1'b1};
end
assign rst_n_sync = q2;
endmodule
// Synthesis note: the OR gate must be placed before the FF async reset pin.
// CDC tools will flag the OR as a reset-domain crossing — add a waiver with
// justification that scan_en is a DFT-controlled, glitch-free signal.
Implementation requirements for the DFT reset gate:
scan_en must be clean and glitch-free; it comes from the ATPG controller and is explicitly constrained as a primary input in DFT modescan_en is fully driven and controllable during all test modesPutting it all together, a production chip's reset architecture follows a clear pipeline from board-level input to domain-local synchronized reset output.
The five stages in order:
The following is a production-ready parameterized reset synchronizer with configurable number of synchronization stages and configurable reset polarity. For most designs N=2 is correct. Safety-critical or sub-5nm high-frequency designs may use N=3 for greater MTBF margin.
// Parameterized reset synchronizer
// Async assert, synchronous de-assert — production ready
//
// Parameters:
// N : number of synchronization stages (default 2, use 3 for safety-critical)
// RST_ACTIVE : 0 = active-low reset, 1 = active-high reset
module reset_sync #(
parameter N = 2,
parameter RST_ACTIVE = 0
)(
input wire clk,
input wire rst_in, // raw async reset input
input wire scan_en, // DFT scan enable (tie to 0 if unused)
output wire rst_sync // synchronized reset output (same polarity as rst_in)
);
// Normalize to active-low internally
wire rst_n_raw = (RST_ACTIVE == 0) ? rst_in : ~rst_in;
// Block reset during scan shift to protect scan chain integrity
wire rst_n_gated = rst_n_raw | scan_en;
// Synchronizer chain — keep FFs physically adjacent for best MTBF
// Xilinx/Vivado : (* ASYNC_REG = "TRUE" *)
// Synopsys DC : set_false_path -to [get_cells *sync_reg*]; set_dont_touch
(* ASYNC_REG = "TRUE" *) reg [N-1:0] sync_reg;
always @(posedge clk or negedge rst_n_gated) begin
if (!rst_n_gated)
sync_reg <= {N{1'b0}}; // async assert: clear all stages
else
sync_reg <= {sync_reg[N-2:0], 1'b1}; // shift 1 through chain
end
// Output de-asserts only after N clock cycles following rst_n_in de-assertion
wire rst_n_sync_out = sync_reg[N-1];
// Convert output back to requested polarity
assign rst_sync = (RST_ACTIVE == 0) ? rst_n_sync_out : ~rst_n_sync_out;
endmodule
Synthesis and implementation notes:
(* ASYNC_REG = "TRUE" *) attribute instructs the synthesizer and place-and-route tool to keep the synchronizer flip-flops physically adjacent, minimizing skew between them and reducing the MTBF impact of routing delaysset_false_path -to [get_cells *sync_reg*] for the reset path — this is a standard CDC constraint that tells the tool the path timing is controlled by the synchronizer structure, not by normal setup/hold analysis{sync_reg[N-2:0], 1'b1} works correctly for any N >= 2; for N=1 it degenerates to a single-FF synchronizer which is insufficient for MTBF requirementsrst_in with a task that asserts it for at least 2*N clock cycles to ensure the entire chain is properly cleared before releasingFlip-flop reset pins are asynchronous by design — de-assertion (going high) travels through combinational logic and may reach different flip-flops in different clock cycles. If two flip-flops in a state machine exit reset in different clock cycles, the machine starts in an inconsistent, undefined state. A reset synchronizer ensures every flip-flop in the domain exits reset on the same clock edge, eliminating the race condition.
A 2-flip-flop chain where both flip-flops have their asynchronous reset inputs tied to the incoming reset signal and are clocked by the destination domain's clock. Assert (going low) is immediate — both FFs are forced to 0 asynchronously, with no clock required. De-assert (going high) shifts a 1 through the chain over two clock cycles before the output goes high. This prevents metastability at downstream flip-flop reset pins during the de-assertion transition.
Each reset synchronizer uses its own domain's clock to re-time the reset release. If one synchronized reset output was shared across two different clock domains, it would be synchronous to one domain's clock but asynchronous to the other's — defeating the entire purpose of synchronization. Each domain gets its own synchronizer so the reset de-assertion is always aligned to that specific domain's clock boundary.