Reset Domain Crossing: The Bugs Your CDC Tool Misses
Why a path inside one clock domain can still go metastable, how to build reset synchronizers, an RDC fix in SystemVerilog, and a sign-off checklist.
Your CDC report is clean. Every signal that crosses between clocks goes through a synchronizer or an asynchronous FIFO. Then a block gets a soft reset in the lab, and its neighbor, on the same clock, locks up or reads garbage.
That is a reset domain crossing (RDC) bug. It is easy to miss because the path never leaves its clock domain, so an analysis that only looks at clocks has nothing to report.
CDC vs. RDC in one paragraph
A flop goes metastable when its input changes too close to its capturing clock edge. In CDC, the input changes because it is launched by a different clock. In RDC, the input changes because the source flop is asynchronously reset at a moment unrelated to the clock. If flop A is in reset domain RST_A and flop B, on the same clock, is in RST_B, then asserting RST_A can change A's output at any time relative to B's clock edge. If B is not held in reset or otherwise frozen at that moment, B can capture a changing value and go metastable, or capture a value that is valid as a bit but meaningless as state.
Reset synchronizers done right
The basic rule: assert asynchronously, deassert synchronously, with one synchronizer per clock domain the reset reaches.
module reset_sync #(parameter int STAGES = 2) ( // STAGES >= 2
input logic clk,
input logic arst_n, // asynchronous, active-low
output logic rst_n // async assert, sync deassert
);
logic [STAGES-1:0] sync_q;
always_ff @(posedge clk or negedge arst_n)
if (!arst_n) sync_q <= '0;
else sync_q <= {sync_q[STAGES-2:0], 1'b1};
assign rst_n = sync_q[STAGES-1];
endmodule
When arst_n falls, every stage clears immediately, with or without a clock. When it rises, the constant 1 walks through the chain, so rst_n rises on the STAGES-th clock edge. The first stage may go metastable if arst_n rises near an edge; the remaining stages give it time to resolve. Keep the stages placed close together, and on FPGAs mark them as synchronizer flops (for example the ASYNC_REG attribute in Vivado).
Common mistakes:
- One synchronizer shared across clock domains. Deassertion is then synchronous to one clock and asynchronous to every other clock it reaches.
- Logic on the reset path after the synchronizer. A gate that combines
rst_nwith a software reset bit can glitch, and it brings back an asynchronous deassertion. Combine the sources before the synchronizer, and drive them from flops, not decoded logic. - Fanning out a synchronized reset without thinking about timing. The deassertion edge is a timed path (recovery and removal checks). If it can't meet timing, pipeline it per region, and make sure the regions leave reset in an order that is safe.
A correct synchronizer only fixes deassertion. Assertion is still asynchronous by design, and that is exactly where RDC bugs live.
An RDC hazard in ten lines
Here is the pattern in its smallest form. A configuration register belongs to a block that software can soft-reset. A datapath accumulator in a neighboring block, on the same clock, is reset only at power-on.
// Source: config register, reset by the block's soft reset
always_ff @(posedge clk or negedge blk_rst_n)
if (!blk_rst_n) step_q <= '0;
else if (cfg_we) step_q <= cfg_wdata;
// Destination: same clock, reset only at power-on
always_ff @(posedge clk or negedge por_rst_n)
if (!por_rst_n) acc_q <= '0;
else acc_q <= acc_q + step_q;
When blk_rst_n falls, step_q drops to zero at a time unrelated to clk. The adder output changes, carries ripple, and if that happens inside acc_q's setup/hold window, some bits of acc_q can resolve to the old sum and some to the new one, or go metastable. Even if timing were luckily met, the accumulator keeps running with a step it never agreed to. A CDC tool sees one clock and reports nothing.
The fix: isolate, then reset
The destination must be frozen before the source's reset asserts and stay frozen until that reset has fully deasserted. A small controller sequences it:
module soft_reset_ctrl (
input logic clk,
input logic por_rst_n, // power-on reset, synchronized to clk
input logic sw_rst_req, // level from a control register
input logic blk_rst_n, // block reset, after its reset_sync (fed back)
output logic iso, // freeze consumers of the block's outputs
output logic blk_rst_req // reset request into the block's synchronizer
);
// State bits are the outputs: {releasing, blk_rst_req, iso}. No decode glitches.
localparam logic [2:0] RUN = 3'b000, ISOLATE = 3'b001,
ASSERT = 3'b011, RELEASE = 3'b101;
logic [2:0] state_q;
always_ff @(posedge clk or negedge por_rst_n)
if (!por_rst_n) state_q <= RUN;
else case (state_q)
RUN: if (sw_rst_req) state_q <= ISOLATE;
ISOLATE: state_q <= ASSERT;
ASSERT: if (!sw_rst_req) state_q <= RELEASE;
RELEASE: if (blk_rst_n) state_q <= RUN;
default: state_q <= RUN;
endcase
assign iso = state_q[0];
assign blk_rst_req = state_q[1];
endmodule
Hook-up:
reset_sync u_blk_rs (.clk, .arst_n(por_arst_n & ~blk_rst_req), .rst_n(blk_rst_n));
always_ff @(posedge clk or negedge por_rst_n)
if (!por_rst_n) acc_q <= '0;
else if (!iso) acc_q <= acc_q + step_q; // frozen for the whole reset window
iso rises one clock before blk_rst_req, so the accumulator has already stopped sampling when step_q changes. The block's reset is still asserted asynchronously (through the synchronizer's clear) and released synchronously, and iso drops only after the controller has seen blk_rst_n high. The sources are combined before the synchronizer, and both come from flops.
I checked these modules with a self-checking Icarus Verilog testbench that issues soft resets at randomized times and verifies that the accumulator never changes while isolated, that isolation always leads assertion and trails deassertion, and that rst_n asserts without a clock and deasserts on exactly the STAGES-th edge.
Classic RDC failures in real systems
1. Soft reset of a bus master while the interconnect runs. A DMA engine is reset by a register write. Its AXI VALID drops asynchronously in the middle of a transfer. The slave captures a half-formed beat, or VALID falls before READY, which breaks the rule that VALID must stay asserted until accepted. Fix: quiesce before reset. Stop new requests, wait for outstanding transactions to drain, then isolate and reset. Many SoCs formalize this as a per-block request/acknowledge reset handshake.
2. FIFO pointers reset on only one side. An asynchronous FIFO's write side is reset, its read side is not. The Gray-coded write pointer jumps from its current value to zero, which can change several bits at once. That breaks the one-bit-per-step assumption the pointer synchronizer relies on, so the read side can sample a pointer that was never valid, and empty becomes wrong. Data is lost or replayed.
Fix: reset both sides together, each through its own clock's synchronizer, and hold the interface idle until both sides report they are out of reset.
3. Configuration register feeding a power-on-only datapath. This is the hazard above. Resetting the register changes the datapath's control mid-operation. Fix: reset the consumer at the same time, or isolate it for the whole reset window.
4. Reset release order. Domain B leaves reset and starts issuing requests before domain A, its target, is out of reset. Requests are dropped or land in uninitialized state. Fix: define a release sequence and enforce it in hardware (a small reset controller), not with "software will wait long enough".
What to tell the tool, and what never to waive
RDC tools, and the RDC modes of CDC tools, need to know your reset domains, which resets are always asserted together, and which paths are isolated during reset. Legitimate waivers usually look like this:
- The destination is guaranteed to be in reset whenever the source is, by a reset ordering you can point to in the RTL.
- The destination is isolated (enable held off, clamped, or clock-gated) for the whole reset window, and an assertion proves it.
Do not waive:
- Crossings into state machines, counters or FIFO pointers "because software will never reset that block while traffic is running". Somebody's driver will.
- Anything justified by a timing relationship between two resets that hardware does not enforce.
Assertions that catch RDC problems in simulation
Simulation won't show metastability, but it will show the protocol violations and sequencing errors that RDC bugs cause:
// Isolation leads reset assertion: iso was already high on the previous edge.
assert property (@(posedge clk) disable iff (!por_rst_n)
$fell(blk_rst_n) |-> $past(iso));
// Isolation trails deassertion: iso drops only after the block is out of reset.
assert property (@(posedge clk) disable iff (!por_rst_n)
$fell(iso) |-> $past(blk_rst_n));
// The consumer holds its state while isolated.
assert property (@(posedge clk) disable iff (!por_rst_n)
$past(iso) |-> $stable(acc_q));
// VALID must not drop before it is accepted, even across a soft reset of the master.
assert property (@(posedge clk) disable iff (!por_rst_n)
(m_valid && !m_ready) |=> m_valid);
Then add a directed test that asserts each soft reset in the middle of traffic, at randomized times. That single test finds more RDC bugs than any review.
RDC sign-off checklist
- [ ] Every reset source and reset domain is listed, with the clocks each reset reaches.
- [ ] One reset synchronizer per (reset, clock) pair: asynchronous assertion, synchronous deassertion.
- [ ] No logic on a synchronized reset; sources are combined before the synchronizer and come from flops.
- [ ] Deassertion paths meet recovery/removal timing; large fan-outs are pipelined per region.
- [ ] Every crossing from a resettable source into a differently reset destination is either reset together or isolated for the full window.
- [ ] Independently resettable blocks quiesce and drain before reset.
- [ ] Asynchronous FIFOs reset both sides together and stay idle until both are ready.
- [ ] Domains that depend on each other leave reset in a hardware-enforced order.
- [ ] The RDC tool runs with reset domains defined; every waiver has a design reason and, where possible, an assertion.
- [ ] Simulation includes soft resets asserted mid-traffic at random times.
Where this comes from
Reset bugs show up at block boundaries, which is where I have spent much of my career: RTL design, integration and CPU work at Intel, iPhone camera hardware at Samsung, and an FPGA AI accelerator built from the ground up. More on that kind of work is on the chip design and FPGA development pages.
Planning a reset scheme or staring at an RDC report you don't trust? Send a short description through the contact page, and I'll reply with how I would approach it.