RTL Design Patterns · Chapter 2 · Registers & Sequential Building Blocks
Reset Strategy in Practice
A pure synchronous reset only takes effect on a clock edge, so it cannot seed a chip at power-up while the clock is still dead. A pure asynchronous reset asserts the instant its wire goes active, but its asynchronous release can land inside a flop's recovery or removal window and go metastable, so part of the design leaves reset a cycle late and comes up in a corrupt, inconsistent state. The industry answer is to assert asynchronously and deassert synchronously. Drive the reset in immediately, clock-free, but release it through a two-flop reset synchronizer so every flop in the domain sees one clean, metastability-resolved deassert edge. This lesson builds that synchronizer, a register that consumes it, and the buggy pure-async release beside the fix, verified in SystemVerilog, Verilog, and VHDL.
Foundation16 min readRTL Design PatternsReset SynchronizerAsync Assert Sync DeassertRecovery RemovalMetastabilityReset Strategy
Chapter 2 · Section 2.5 · Registers & Sequential Building Blocks
1. The Engineering Problem
Your chip has just been powered. The supply is ramping, the PLL has not locked, and the main clock is either dead or jittering through frequencies that mean nothing yet. Every flip-flop on the die is holding an unknown value — X in simulation, a genuine coin-flip in silicon — and a datapath seeded with X computes only more X. Before the design can behave predictably for even one cycle, it must be forced into a known state. That is the whole job of reset, and by now you have built it two ways.
You could use a synchronous reset (2.3): sample the reset line on the clock edge and, if it is asserted, load the reset value. It times like ordinary datapath logic — no separate reset tree, no special timing check — and it is the cleanest thing in the world once the clock is running. But look again at the power-up scenario: there is no clock yet. A synchronous reset that never sees an edge never takes effect, so the very flops that most need to be seeded — the ones that come up X at power-on — stay X until the clock arrives, and if any of that X has already leaked into a state machine or a counter enable, the design can be wedged before its first real cycle. Synchronous reset cannot seed a chip that has no clock, and at power-up that is exactly the chip you have.
So you reach for an asynchronous reset (2.4): connect the reset to the flop's async clear so it forces the known value the instant the wire asserts, clock or no clock. That solves power-up perfectly — assertion is immediate and free. But an asynchronous reset asserts and de-asserts asynchronously, and the de-assertion is where it bites. When the reset wire releases, it releases at some arbitrary moment relative to the clock. If that release edge lands too close to a flop's active clock edge — inside the recovery/removal window, the async-reset analogue of setup/hold — that flop's output is not guaranteed to settle to a defined 0 or 1 by the next edge. It can go metastable and resolve late. Now here is the killer: a real reset fans out to thousands of flops through a distribution tree with skew, so the single asynchronous release edge arrives at slightly different times at different flops. Some resolve cleanly and leave reset on cycle N; a few catch the recovery/removal window and resolve on cycle N+1. Part of the chip leaves reset one cycle later than the rest.
That one-cycle split is a silicon nightmare precisely because it is intermittent. The design comes up correct almost every time, and corrupt occasionally, and whether it corrupts depends on the exact phase between the release edge and the clock — which drifts with temperature, supply voltage, process corner, and the specific board. It passes on the bench, passes in the lab, and fails one board in the field at 70°C. Simulation with zero-delay reset never shows it. The requirement that falls out of all this is sharp and non-negotiable: you need a reset that asserts without a clock (so it seeds at power-up) and releases on a clean, synchronized edge (so no flop leaves reset late). Assert like an async reset; release like a synchronous one. This page is the pattern that delivers exactly that.
// PURE SYNCHRONOUS reset: perfect timing, but...
always @(posedge clk)
if (rst) q <= RESET_VAL; // needs a live CLK; at power-up there is none => q stays X
else q <= d;
// PURE ASYNCHRONOUS reset: seeds at power-up, but the RELEASE is the hazard...
always @(posedge clk or negedge arst_n)
if (!arst_n) q <= RESET_VAL; // assert: immediate, clock-free -- GOOD
else q <= d; // deassert of arst_n is ASYNC vs clk:
// if it lands in recovery/removal => q may go METASTABLE
// and resolve a cycle late => part of chip leaves reset late.
// The requirement: assert async (seed with no clock) AND release on a clean,
// synchronized edge (no flop leaves reset late). That is the reset synchronizer.2. Mental Model
3. Pattern Anatomy
The structure has one job — take a raw asynchronous reset in and hand out a reset whose assertion is still asynchronous but whose de-assertion is synchronous to the destination clock — and it is astonishingly small: two flip-flops.
The reset synchronizer (active-low, rst_n convention). The raw board reset ARST_N (active-low: 0 means "in reset") drives the asynchronous clear of two chained flops that are clocked by the destination clock CLK. The D input of the first flop is tied to the inactive level (constant 1). While ARST_N is low, both flops are async-cleared to 0 immediately — that is the asynchronous assert, clock-free, exactly like a pure async reset. When ARST_N releases (goes high), the constant 1 now walks through the chain one clock at a time: after the first rising edge the first flop is 1, after the second edge the second flop is 1, and that second-flop output is RST_SYNC_N, the reset handed to the whole design. Because RST_SYNC_N only rises on a clock edge, its de-assertion is synchronous — a clean edge every flop sees together.
Why exactly two flops on the deassert. The first flop is the one exposed to danger: ARST_N releases asynchronously, so its rising edge can fall inside flop 1's recovery/removal window and drive flop 1 metastable. Flop 1 then has a full clock period to resolve to a stable 0 or 1 before flop 2 samples it. Flop 2 therefore captures a settled value and produces a clean RST_SYNC_N. Two flops is the standard because it buys one full period of settling; a higher metastability budget (very high clock, tight MTBF target) adds a third flop, at the cost of one more cycle of reset-release latency. One flop is not a synchronizer — it just moves the metastability onto the reset net.
Assert path vs release path — asymmetric by design. Trace the two directions and the asymmetry is the point. On assert, ARST_N low hits the async clears directly: no clock needed, RST_SYNC_N goes low immediately — power-up-safe. On release, ARST_N high does not touch RST_SYNC_N directly; it only lifts the async clears and lets the constant 1 clock through the two flops, so RST_SYNC_N rises only on a clean, resolved edge. Assert is combinational and free; release is clocked and safe.
Distribution and per-domain instances. RST_SYNC_N is then buffered through a reset distribution tree — a low-skew network much like a clock tree — so it arrives at all the destination flops with minimal skew, keeping them leaving reset on the same edge. And because the resolving clock defines which domain a synchronizer serves, a multi-clock chip instantiates one reset synchronizer per clock domain, each clocked by its own clock, all fed from the same raw ARST_N.
The reset synchronizer — async assert into the clears, sync release through 2 flops
data flowThe second visual is the one that makes the bug tangible: what the release looks like on a waveform, pure-async versus synchronized. The pure-async release rises at an arbitrary point and, if it grazes an active edge, some flops resolve a cycle late; the synchronized release always rises on a clock edge, so the whole domain leaves reset together.
Reset RELEASE on a timeline — pure-async (hazard) vs synchronized (clean)
data flow4. Real RTL Implementation
This is the core of the page. We build three things — (a) the reset synchronizer itself (async-assert / sync-deassert, two-flop deassert, active-low rst_n), (b) a register that consumes the synchronized reset, and (c) the buggy pure-async unsynchronized deassert beside the fixed synchronizer — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The reasoning is identical in all three; only the syntax differs.
One idiom runs through every RTL block: the sensitivity list posedge clk or negedge arst_n (VHDL: rising_edge(clk) plus a guard on the async reset). That double-edge list is what makes the assert asynchronous (the negedge arst_n term fires with no clock) while the update stays synchronous (the posedge clk term). It is the same flop template as the pure async reset of 2.4 — the synchronizer just wires its D to a constant and chains two of them.
4a. The reset synchronizer — async-assert / sync-deassert, three ways
The synchronizer is two flops whose async clear is the raw reset and whose data path walks a constant inactive level in on the clock. Note the width: the reset is a single control bit per stage, and the parameter here is the number of synchronizer stages (STAGES = 2 by default), not a datapath width — a rare case where the "width" you parameterize is the metastability budget.
module reset_sync #(
parameter int STAGES = 2 // deassert-synchronizer depth (2 = 1 period of settling)
)(
input logic clk, // destination-domain clock
input logic arst_n, // RAW async reset in, active-low (0 = in reset)
output logic rst_sync_n // async-assert, SYNC-deassert reset out
);
logic [STAGES-1:0] sync_q; // the synchronizer chain (control bits, not data)
// posedge clk OR negedge arst_n: the negedge term makes ASSERT asynchronous
// (fires with no running clock); the posedge term makes RELEASE synchronous.
always_ff @(posedge clk or negedge arst_n) begin
if (!arst_n)
sync_q <= '0; // ASSERT: async-clear the whole chain -> reset out low, clock-free
else
sync_q <= {sync_q[STAGES-2:0], 1'b1}; // RELEASE: walk a constant 1 in, one edge per stage
end
assign rst_sync_n = sync_q[STAGES-1]; // last stage = clean, resolved, clock-aligned deassert
endmoduleThe clocked testbench must prove two distinct properties, because they are the two halves of the pattern: (1) assert works with no clock — hold the clock still, drop ARST_N, and confirm RST_SYNC_N goes low anyway; and (2) deassert is synchronized — release ARST_N and confirm RST_SYNC_N rises only after STAGES clock edges, on an edge, never combinationally.
module reset_sync_tb;
localparam int STAGES = 2;
logic clk = 1'b0, arst_n, rst_sync_n;
int errors = 0;
reset_sync #(.STAGES(STAGES)) dut (.clk(clk), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
always #5 clk = ~clk; // 10ns clock (started AFTER the clock-free assert check)
initial begin
// (1) ASSERT WITH NO CLOCK: freeze clk, assert reset, expect rst_sync_n low anyway.
clk = 1'b0; arst_n = 1'b1; #2;
arst_n = 1'b0; #2; // no edge has occurred, yet:
assert (rst_sync_n === 1'b0)
else begin $error("assert failed clock-free: rst_sync_n=%b", rst_sync_n); errors++; end
// (2) SYNCHRONIZED DEASSERT: start the clock, release reset on a known edge,
// expect rst_sync_n to STAY low until STAGES edges have passed, then rise.
@(negedge clk); arst_n = 1'b1; // release cleanly (away from the active edge)
for (int e = 0; e < STAGES; e++) begin
@(posedge clk); #1;
if (e < STAGES-1)
assert (rst_sync_n === 1'b0)
else begin $error("deassert too early at edge %0d", e); errors++; end
end
assert (rst_sync_n === 1'b1) // now, and only now, reset has been released
else begin $error("deassert did not complete after %0d edges", STAGES); errors++; end
if (errors == 0) $display("PASS: async assert (clock-free) + sync deassert after %0d edges", STAGES);
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog form is the same structure with reg/wire typing and always @(posedge clk or negedge arst_n); the chain is a simple shift of a constant 1.
module reset_sync #(
parameter STAGES = 2
)(
input wire clk,
input wire arst_n, // active-low raw async reset
output wire rst_sync_n
);
reg [STAGES-1:0] sync_q;
always @(posedge clk or negedge arst_n) begin
if (!arst_n)
sync_q <= {STAGES{1'b0}}; // ASSERT: async clear, clock-free
else
sync_q <= {sync_q[STAGES-2:0], 1'b1}; // RELEASE: shift a constant 1 through the chain
end
assign rst_sync_n = sync_q[STAGES-1]; // synchronized deassert
endmodule module reset_sync_tb;
parameter STAGES = 2;
reg clk, arst_n;
wire rst_sync_n;
integer e, errors;
reset_sync #(.STAGES(STAGES)) dut (.clk(clk), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
always #5 clk = ~clk;
initial begin
errors = 0; clk = 1'b0; arst_n = 1'b1; #2;
// (1) ASSERT WITHOUT A CLOCK EDGE:
arst_n = 1'b0; #2;
if (rst_sync_n !== 1'b0) begin
$display("FAIL: assert not clock-free, rst_sync_n=%b", rst_sync_n);
errors = errors + 1;
end
// (2) SYNCHRONIZED DEASSERT: release, then require STAGES edges before it rises.
@(negedge clk); arst_n = 1'b1;
for (e = 0; e < STAGES; e = e + 1) begin
@(posedge clk); #1;
if (e < STAGES-1 && rst_sync_n !== 1'b0) begin
$display("FAIL: deassert too early at edge %0d", e);
errors = errors + 1;
end
end
if (rst_sync_n !== 1'b1) begin
$display("FAIL: deassert not complete after %0d edges", STAGES);
errors = errors + 1;
end
if (errors == 0) $display("PASS: async assert + sync deassert after %0d edges", STAGES);
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the same template is a process (clk, arst_n) whose async branch tests the reset first and whose rising_edge(clk) branch walks the constant '1' in. arst_n = '0' outside rising_edge is what makes the assert asynchronous.
library ieee;
use ieee.std_logic_1164.all;
entity reset_sync is
generic ( STAGES : positive := 2 ); -- deassert-synchronizer depth
port (
clk : in std_logic; -- destination-domain clock
arst_n : in std_logic; -- raw async reset, active-low
rst_sync_n : out std_logic -- async assert, sync deassert
);
end entity;
architecture rtl of reset_sync is
signal sync_q : std_logic_vector(STAGES-1 downto 0);
begin
-- arst_n in the sensitivity list AND tested first => ASSERT is asynchronous.
-- rising_edge(clk) branch => RELEASE walks a constant '1' in, one edge per stage.
process (clk, arst_n)
begin
if arst_n = '0' then
sync_q <= (others => '0'); -- ASSERT: async clear, clock-free
elsif rising_edge(clk) then
sync_q <= sync_q(STAGES-2 downto 0) & '1'; -- RELEASE: shift a '1' through the chain
end if;
end process;
rst_sync_n <= sync_q(STAGES-1); -- synchronized deassert
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity reset_sync_tb is
end entity;
architecture sim of reset_sync_tb is
constant STAGES : positive := 2;
signal clk : std_logic := '0';
signal arst_n : std_logic := '1';
signal rst_sync_n : std_logic;
signal run_clk : boolean := false; -- gate the clock so we can test clock-free assert
begin
dut : entity work.reset_sync
generic map (STAGES => STAGES)
port map (clk => clk, arst_n => arst_n, rst_sync_n => rst_sync_n);
clkgen : process
begin
wait until run_clk; -- clock only starts after the clock-free check
while run_clk loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
end process;
stim : process
begin
-- (1) ASSERT WITHOUT A CLOCK: clock is stopped; assert reset; expect low anyway.
arst_n <= '1'; wait for 2 ns;
arst_n <= '0'; wait for 2 ns;
assert rst_sync_n = '0'
report "assert not clock-free" severity error;
-- (2) SYNCHRONIZED DEASSERT: start clock, release, require STAGES edges before it rises.
run_clk <= true;
wait until falling_edge(clk);
arst_n <= '1'; -- clean release
for e in 0 to STAGES-1 loop
wait until rising_edge(clk); wait for 1 ns;
if e < STAGES-1 then
assert rst_sync_n = '0'
report "deassert too early" severity error;
end if;
end loop;
assert rst_sync_n = '1'
report "deassert did not complete after STAGES edges" severity error;
report "reset_sync self-check complete" severity note;
run_clk <= false;
wait;
end process;
end architecture;4b. A register that consumes the synchronized reset
The synchronizer is upstream infrastructure; downstream, every flop in the domain uses RST_SYNC_N exactly the way it would use a pure async reset — asserted asynchronously (from the synchronizer's async assert), released synchronously (because RST_SYNC_N only deasserts on an edge). The register template is unchanged from 2.4; the safety comes entirely from what drives its reset, not from a change to the register.
module reg_with_sync_reset #(
parameter int WIDTH = 8,
parameter logic [WIDTH-1:0] RESET_VAL = '0
)(
input logic clk,
input logic rst_sync_n, // from reset_sync: async-assert, SYNC-deassert
input logic [WIDTH-1:0] d,
output logic [WIDTH-1:0] q
);
// Same async-reset flop as 2.4. Because rst_sync_n DEASSERTS synchronously, this
// flop leaves reset on the same clean edge as every other flop in the domain.
always_ff @(posedge clk or negedge rst_sync_n) begin
if (!rst_sync_n) q <= RESET_VAL; // assert path: async, immediate
else q <= d; // normal update
end
endmodule module reg_with_sync_reset_tb;
localparam int WIDTH = 8;
localparam logic [WIDTH-1:0] RESET_VAL = 8'hA5;
logic clk = 1'b0, rst_sync_n;
logic [WIDTH-1:0] d, q;
int errors = 0;
reg_with_sync_reset #(.WIDTH(WIDTH), .RESET_VAL(RESET_VAL)) dut
(.clk(clk), .rst_sync_n(rst_sync_n), .d(d), .q(q));
always #5 clk = ~clk;
initial begin
rst_sync_n = 1'b0; d = 8'h3C; // hold in reset
repeat (2) @(posedge clk); #1;
assert (q === RESET_VAL) // in reset -> q is the reset value
else begin $error("in-reset q=%h exp=%h", q, RESET_VAL); errors++; end
@(negedge clk); rst_sync_n = 1'b1; // clean synchronized release
@(posedge clk); #1;
d = 8'h77; @(posedge clk); #1;
assert (q === 8'h77) // out of reset -> q tracks d
else begin $error("post-reset q=%h exp=77", q); errors++; end
if (errors == 0) $display("PASS: holds RESET_VAL in reset, tracks d after clean release");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule module reg_with_sync_reset #(
parameter WIDTH = 8,
parameter RESET_VAL = 8'h00
)(
input wire clk,
input wire rst_sync_n, // async-assert, SYNC-deassert
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk or negedge rst_sync_n) begin
if (!rst_sync_n) q <= RESET_VAL; // async assert
else q <= d; // normal update
end
endmodule module reg_with_sync_reset_tb;
parameter WIDTH = 8;
parameter RESET_VAL = 8'hA5;
reg clk, rst_sync_n;
reg [WIDTH-1:0] d;
wire [WIDTH-1:0] q;
integer errors;
reg_with_sync_reset #(.WIDTH(WIDTH), .RESET_VAL(RESET_VAL)) dut
(.clk(clk), .rst_sync_n(rst_sync_n), .d(d), .q(q));
always #5 clk = ~clk;
initial begin
errors = 0; clk = 1'b0; rst_sync_n = 1'b0; d = 8'h3C;
repeat (2) @(posedge clk); #1;
if (q !== RESET_VAL) begin
$display("FAIL: in-reset q=%h exp=%h", q, RESET_VAL); errors = errors + 1;
end
@(negedge clk); rst_sync_n = 1'b1; // synchronized release
@(posedge clk); #1;
d = 8'h77; @(posedge clk); #1;
if (q !== 8'h77) begin
$display("FAIL: post-reset q=%h exp=77", q); errors = errors + 1;
end
if (errors == 0) $display("PASS: holds RESET_VAL in reset, tracks d after release");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
entity reg_with_sync_reset is
generic (
WIDTH : positive := 8;
RESET_VAL : std_logic_vector := "10100101" -- 0xA5
);
port (
clk : in std_logic;
rst_sync_n : in std_logic; -- async-assert, SYNC-deassert
d : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of reg_with_sync_reset is
begin
process (clk, rst_sync_n)
begin
if rst_sync_n = '0' then
q <= RESET_VAL; -- async assert
elsif rising_edge(clk) then
q <= d; -- normal update
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity reg_with_sync_reset_tb is
end entity;
architecture sim of reg_with_sync_reset_tb is
constant WIDTH : positive := 8;
constant RESET_VAL : std_logic_vector(WIDTH-1 downto 0) := "10100101"; -- 0xA5
signal clk : std_logic := '0';
signal rst_sync_n : std_logic := '0';
signal d : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.reg_with_sync_reset
generic map (WIDTH => WIDTH, RESET_VAL => RESET_VAL)
port map (clk => clk, rst_sync_n => rst_sync_n, d => d, q => q);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process;
stim : process
begin
rst_sync_n <= '0'; d <= x"3C"; -- hold in reset
wait until rising_edge(clk); wait until rising_edge(clk); wait for 1 ns;
assert q = RESET_VAL
report "in-reset q /= RESET_VAL" severity error;
wait until falling_edge(clk);
rst_sync_n <= '1'; -- synchronized release
wait until rising_edge(clk); wait for 1 ns;
d <= x"77";
wait until rising_edge(clk); wait for 1 ns;
assert q = x"77"
report "post-reset q /= d" severity error;
report "reg_with_sync_reset self-check complete" severity note;
wait;
end process;
end architecture;4c. The signature bug — pure-async unsynchronized deassert vs the fixed synchronizer
Pattern (c) is the flagship failure, shown here as buggy vs fixed RTL in each language and dramatized in §7. The buggy design drives the raw asynchronous ARST_N straight into the design's flops with no synchronizer — assertion is fine, but the release is unsynchronized, so a release near the active edge violates recovery/removal and lets a flop resolve late. The fix is to route the release through the 4a synchronizer and feed the design RST_SYNC_N instead. RTL cannot make a flop metastable in a zero-delay sim, so the comment marks where the hazard lives; §5 covers how gate-level and CDC tools catch it.
// BUGGY: the design flops take the RAW async reset directly. Assert is fine, but
// ARST_N deasserts ASYNCHRONOUSLY vs clk -> a release inside recovery/removal
// can drive a flop metastable -> it leaves reset a cycle LATE. Intermittent.
module core_bad (
input logic clk,
input logic arst_n, // RAW async reset straight from the pad
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk or negedge arst_n) begin
if (!arst_n) q <= 8'h00; // async assert: OK
else q <= d; // release of arst_n is UNSYNCHRONIZED -> hazard
end
endmodule
// FIXED: synchronize the RELEASE first (async assert preserved), then feed rst_sync_n
// to every design flop, so the whole domain leaves reset on one clean edge.
module core_good (
input logic clk,
input logic arst_n,
input logic [7:0] d,
output logic [7:0] q
);
logic rst_sync_n;
reset_sync #(.STAGES(2)) u_rsync (.clk(clk), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
always_ff @(posedge clk or negedge rst_sync_n) begin
if (!rst_sync_n) q <= 8'h00; // async assert (from the synchronizer)
else q <= d; // release is now SYNCHRONIZED -> no late flop
end
endmodule module reset_bug_tb;
logic clk = 1'b0, arst_n;
logic [7:0] d, q;
int errors = 0;
// Point at core_good to PASS cleanly. Functionally both look correct in a
// zero-delay sim; the difference is the RELEASE metastability, which needs
// gate-level timing (recovery/removal) or a CDC check to expose (see section 5).
core_good dut (.clk(clk), .arst_n(arst_n), .d(d), .q(q));
always #5 clk = ~clk;
initial begin
arst_n = 1'b0; d = 8'h5A; // in reset
repeat (2) @(posedge clk); #1;
assert (q === 8'h00)
else begin $error("in-reset q=%h exp=00", q); errors++; end
@(negedge clk); arst_n = 1'b1; // release
repeat (3) @(posedge clk); #1; // allow the synchronizer to release
d = 8'hC3; @(posedge clk); #1;
assert (q === 8'hC3)
else begin $error("post-reset q=%h exp=C3", q); errors++; end
if (errors == 0) $display("PASS: reset asserts to 00, releases cleanly, then tracks d");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule // BUGGY: raw async reset into the design flops -> unsynchronized release hazard.
module core_bad (
input wire clk,
input wire arst_n,
input wire [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge arst_n) begin
if (!arst_n) q <= 8'h00; // assert OK; release UNSYNCHRONIZED
else q <= d;
end
endmodule
// FIXED: synchronize the release, then feed rst_sync_n to the design flop.
module core_good (
input wire clk,
input wire arst_n,
input wire [7:0] d,
output reg [7:0] q
);
wire rst_sync_n;
reset_sync #(.STAGES(2)) u_rsync (.clk(clk), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
always @(posedge clk or negedge rst_sync_n) begin
if (!rst_sync_n) q <= 8'h00; // async assert (from synchronizer)
else q <= d; // SYNCHRONIZED release
end
endmodule module reset_bug_tb;
reg clk, arst_n;
reg [7:0] d;
wire [7:0] q;
integer errors;
// Bind to core_good to PASS; the release-metastability difference vs core_bad
// needs gate-level recovery/removal timing to observe (see section 5).
core_good dut (.clk(clk), .arst_n(arst_n), .d(d), .q(q));
always #5 clk = ~clk;
initial begin
errors = 0; clk = 1'b0; arst_n = 1'b0; d = 8'h5A;
repeat (2) @(posedge clk); #1;
if (q !== 8'h00) begin $display("FAIL: in-reset q=%h exp=00", q); errors = errors + 1; end
@(negedge clk); arst_n = 1'b1; // release
repeat (3) @(posedge clk); #1;
d = 8'hC3; @(posedge clk); #1;
if (q !== 8'hC3) begin $display("FAIL: post-reset q=%h exp=C3", q); errors = errors + 1; end
if (errors == 0) $display("PASS: asserts to 00, releases cleanly, then tracks d");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: raw async reset drives the design flop; the RELEASE is unsynchronized.
entity core_bad is
port (
clk : in std_logic;
arst_n : in std_logic; -- raw async reset
d : in std_logic_vector(7 downto 0);
q : out std_logic_vector(7 downto 0)
);
end entity;
architecture rtl of core_bad is
begin
process (clk, arst_n)
begin
if arst_n = '0' then
q <= (others => '0'); -- assert OK
elsif rising_edge(clk) then
q <= d; -- release UNSYNCHRONIZED -> hazard
end if;
end process;
end architecture;
-- FIXED: synchronize the release, feed rst_sync_n to the design flop.
library ieee;
use ieee.std_logic_1164.all;
entity core_good is
port (
clk : in std_logic;
arst_n : in std_logic;
d : in std_logic_vector(7 downto 0);
q : out std_logic_vector(7 downto 0)
);
end entity;
architecture rtl of core_good is
signal rst_sync_n : std_logic;
begin
u_rsync : entity work.reset_sync
generic map (STAGES => 2)
port map (clk => clk, arst_n => arst_n, rst_sync_n => rst_sync_n);
process (clk, rst_sync_n)
begin
if rst_sync_n = '0' then
q <= (others => '0'); -- async assert (from synchronizer)
elsif rising_edge(clk) then
q <= d; -- SYNCHRONIZED release
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity reset_bug_tb is
end entity;
architecture sim of reset_bug_tb is
signal clk : std_logic := '0';
signal arst_n : std_logic := '0';
signal d : std_logic_vector(7 downto 0) := (others => '0');
signal q : std_logic_vector(7 downto 0);
begin
-- Bind to core_good to PASS; the release-metastability difference vs core_bad
-- needs gate-level recovery/removal timing to observe (see section 5).
dut : entity work.core_good
port map (clk => clk, arst_n => arst_n, d => d, q => q);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process;
stim : process
begin
arst_n <= '0'; d <= x"5A"; -- in reset
wait until rising_edge(clk); wait until rising_edge(clk); wait for 1 ns;
assert q = x"00" report "in-reset q /= 00" severity error;
wait until falling_edge(clk);
arst_n <= '1'; -- release
for i in 0 to 2 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
d <= x"C3";
wait until rising_edge(clk); wait for 1 ns;
assert q = x"C3" report "post-reset q /= d" severity error;
report "reset_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: assert the reset asynchronously so it seeds without a clock, but never let the design see a raw asynchronous release — synchronize the deassert through two flops so every flop leaves reset on one clean edge.
5. Verification Strategy
Reset correctness splits into two properties that need two very different testbenches, because one is a plain functional check and the other is a timing property RTL simulation cannot fully see.
Assert must work with no clock; deassert must land on a clean, shared edge — and every flop in the domain must leave reset on the same cycle.
- Assert-is-clock-free (self-checking, RTL). Freeze the clock, assert ARST_N, and confirm the reset output goes active without any edge occurring. This is fully checkable in RTL: the §4a testbenches drop ARST_N while the clock is held and assert RST_SYNC_N low — SV
assert (rst_sync_n === 1'b0), Verilogif (rst_sync_n !== 1'b0) $display("FAIL…"), VHDLassert rst_sync_n = '0' … severity error. If the reset needed an edge to assert, this test fails, and you have accidentally built a synchronous reset that cannot seed at power-up. - Deassert-is-synchronized (self-checking, RTL). Release ARST_N and confirm the output reset stays active until
STAGESclock edges have passed, then rises — and rises on an edge, never combinationally. The §4a testbenches count edges: the output must be low for the firstSTAGES-1edges and high only after theSTAGES-th. This proves the release walks through the synchronizer rather than passing straight through, which is the whole point of the pattern. - Invariants, stated conceptually. Two properties must always hold: (i) assertion of ARST_N drives the reset active with no clock dependency (the async-assert property), and (ii) the reset output only ever de-asserts on a rising clock edge (the sync-deassert property). A third, at the system level: every flop in a domain leaves reset within the same cycle — checkable at gate level by confirming no reset-tree path violates recovery/removal.
- Recovery/removal and metastable release are gate-level / CDC concerns. This is the honest limit: a zero-delay RTL sim cannot show the release metastability, because it has no notion of the recovery/removal window and no timing on the reset net — which is exactly why the §4c buggy and fixed cores behave identically in the functional testbench. The release bug is caught by (a) gate-level simulation with SDF back-annotation, where recovery/removal timing arcs are checked and a release in the window flags a violation; (b) static timing analysis, which reports recovery/removal slack on every reset endpoint; and (c) a CDC / reset-domain checker (a lint-class tool), which structurally flags any flop taking a raw asynchronous reset release without a synchronizer. In RTL you approximate it: assert on the two synchronizer properties above, and add a real-delay note that the deassert must meet recovery/removal at gates.
- Corner cases to exercise. Reset asserting mid-operation (the flop must snap to RESET_VAL regardless of
d); reset released one clock after another (back-to-back reset pulses shorter thanSTAGES— the output must still deassert cleanly); a reset released exactly at an active edge (the case the synchronizer exists to absorb); and, for a multi-domain design, each domain's synchronizer releasing independently. - Expected waveform. On a correct run, ARST_N low drives RST_SYNC_N low immediately, with no edge in between (the async assert). After ARST_N rises, RST_SYNC_N stays low for
STAGESclock edges and then rises coincident with a rising clock edge — never in between. Every design flop'sqholds RESET_VAL until that shared edge and begins trackingdon the first edge after. The visual signature of the bug is RST released and one flop'squpdating a cycle later than its neighbours — the late-leaving flop.
6. Common Mistakes
Driving the design with a raw async reset — an unsynchronized deassert. The flagship bug (§7). Connecting the board reset straight to every flop's async clear makes the assert correct but leaves the release asynchronous; a release near an active edge violates recovery/removal, a flop resolves late, and the chip comes up with an inconsistent initial state on some boards at some temperatures. Always synchronize the deassert.
One reset synchronizer feeding multiple clock domains. A synchronizer resolves metastability with its clock, so it is only valid for the domain of that clock. Sharing one synchronizer's RST_SYNC_N across two clocks re-introduces an asynchronous release in the other domain (its release is now unrelated to its clock) — the exact bug you built the synchronizer to remove. Instantiate one synchronizer per clock domain, each clocked by its own clock, all fed from the same raw ARST_N.
Too few synchronizer stages. A single flop is not a synchronizer — it catches the async release, may go metastable, and passes that metastability straight onto the reset net. Two flops is the minimum, because it gives one full clock period for the first flop to settle before the second samples. At very high clock rates or with a tight MTBF target, use three; never use one.
Gating or deriving the reset combinationally. Feeding the design a reset that is a combinational function of other signals (rst_n & enable, a decoded reset, a reset ANDed with a mode bit) creates glitches and un-synchronized edges on the reset net, and can momentarily reset flops you did not mean to. Reset should be driven from the synchronizer output directly; if a block needs a conditional reset, gate the condition into the synchronizer's input, not the output.
Forgetting per-domain reset synchronization entirely. In a single-clock design it is tempting to think one synchronizer covers everything — and it does, for that one domain. The moment a second clock appears (a slow always-on domain, a PLL-multiplied core clock, an I/O clock), every new domain silently ships with an unsynchronized reset release unless you add its own synchronizer. Reset is per-domain, and the count of synchronizers should equal the count of asynchronous clock domains.
Assuming the RTL sim proving it. Because a zero-delay RTL simulation cannot model recovery/removal, a design with an unsynchronized reset release passes every functional test and still fails in silicon. Never conclude "reset is fine" from RTL sim alone — the release bug lives at the gate/timing level (STA recovery/removal, gate-level SDF, CDC lint), so those checks are part of signing off reset, not optional extras.
7. DebugLab
The board that boots 99 times and corrupts the 100th — a metastable reset release
The engineering lesson: a reset's assertion is free and never the bug; its de-assertion is the entire hazard, because an asynchronous release edge that grazes a flop's recovery/removal window drives it metastable and lets it leave reset a cycle late — an inconsistent, temperature- and board-dependent power-up corruption that RTL simulation cannot see. The tell is intermittency that tracks the physical environment: a boot bug whose rate moves with temperature, voltage, or which board you hold is a timing-window bug, not a logic bug. Two rules make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: assert asynchronously but synchronize every reset de-assert through a two-flop synchronizer clocked by the destination clock, and instantiate one synchronizer per clock domain so no domain ever sees a release edge unrelated to its own clock. And sign reset off at the gate/timing level (STA recovery/removal, gate-level SDF, CDC lint) — never from RTL sim alone.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the async-assert / sync-deassert habit and the timing reasoning behind it.
Exercise 1 — Design and justify a reset synchronizer
Draw a two-flop reset synchronizer for an active-low reset in a single clock domain. Label the async-clear connection, the constant tied to the first flop's D, the clock on each flop, and the output. Then answer in one line each: (a) what makes the assert asynchronous; (b) what makes the de-assert synchronous; (c) why the first flop can go metastable but RST_SYNC_N is still clean; (d) what changes if you make it three flops, and what you pay for it.
Exercise 2 — Diagnose the intermittent boot
A colleague's board boots correctly about 99 times in 100 and comes up with a corrupt state machine the other time; the failure rate rises with temperature and differs between two physically identical boards, and it never reproduces in RTL simulation. State the single most likely root cause, the precise timing mechanism behind it (name the window), why it is intermittent and environment-dependent, why RTL sim misses it, and the exact RTL change that fixes it. Then name the two sign-off checks that would have caught it before tape-out.
Exercise 3 — Count the synchronizers
A chip has four clocks: a 600 MHz core clock, a 200 MHz bus clock, a 50 MHz I/O clock, and a 32 kHz always-on clock, all reset from one board-level active-low ARST_N. How many reset synchronizers does the design need and why? For each domain, state which clock its synchronizer is clocked by and what its output must feed. Then explain what specifically goes wrong if a junior engineer reuses the core-clock synchronizer's output for the always-on domain to save flops.
Exercise 4 — Choose the reset style for the target
For each target and block, choose synchronous, pure-asynchronous, or async-assert/sync-deassert reset, and justify in one line: (a) an ASIC datapath with thousands of flops and a real reset tree; (b) an FPGA design whose block RAMs and DSP slices support only synchronous reset; (c) a single flop that must be forced to a known value at power-up before any clock is guaranteed running; (d) a multi-clock ASIC where every domain must come up deterministically. Note where the release must be synchronized regardless of the style chosen.
10. Related Tutorials
Continue in RTL Design Patterns (sibling and forward links unlock as they ship):
- Asynchronous Reset — Chapter 2.4; the pure-async half this page completes — where the immediate, clock-free assert comes from, and where the recovery/removal release hazard is introduced.
- Shift Registers — Chapter 2.6; the same "walk a value through a chain of flops one edge at a time" structure the reset synchronizer is built from.
- Metastability — Chapter 11.1; why a flop clocked near a violating edge can settle late — the physics the reset synchronizer's second flop is designed to absorb.
- Two-Flop Synchronizer — Chapter 11.2; the general CDC synchronizer this reset synchronizer is a specialization of — data crossing clocks instead of a reset releasing.
- CDC Design Rules — Chapter 11.6; the domain-crossing discipline (one synchronizer per domain, no combinational gating) this page previews for reset.
Backward / in-track dependencies:
- Synchronous Reset — Chapter 2.3; the clean-timing half — why a sampled-on-the-edge reset cannot seed a chip at power-up.
- The Register Pattern (D-FF) — Chapter 2.1; the flop this reset seeds, and the reset-first priority inside the clocked process.
- Register with Enable / Load — Chapter 2.2; the hold-vs-update mux that sits under reset in the flop's priority order (reset beats enable beats hold).
- The RTL Design Mindset — Chapter 0.2; the State and Verification lenses this page applies — reset is how a design acquires a known state before it can behave.
- What Is an RTL Design Pattern? — Chapter 0.1; why async-assert/sync-deassert earns the name "pattern" — a recurring problem, a reusable form, a synthesis reality, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Blocking and Non-Blocking Assignments — why the synchronizer and every reset flop use non-blocking (
<=) in their clocked process. - Initial and Always Blocks — the
always @(posedge clk or negedge arst_n)block template the async-assert flop is built on, and the clocked testbench structure. - Level-Sensitive Timing — the setup/hold reasoning that generalizes to the recovery/removal window governing the reset release.
11. Summary
- Neither pure reset style is enough alone. A synchronous reset (2.3) times cleanly but needs a running clock, so it cannot seed a chip at power-up; a pure asynchronous reset (2.4) seeds fine but de-asserts asynchronously, and a release in the recovery/removal window can drive a flop metastable so part of the chip leaves reset a cycle late — an intermittent, board- and temperature-dependent corrupt initial state.
- The strategy is assert asynchronously, deassert synchronously. Drive the reset in asynchronously (immediate, clock-free — the power-up seed) but release it through a two-flop reset synchronizer clocked by the destination clock, so every flop in the domain sees one clean, resolved de-assert edge. Assert is free; the release is the entire hazard, and the synchronizer is what tames it.
- The synchronizer is two flops. ARST_N async-clears both (the assert); a constant inactive level walks through them one edge at a time on release (the sync deassert); the first flop absorbs any metastability with a full period to settle, the second produces a clean RST_SYNC_N. Two flops is standard; three for a higher metastability budget; one is never a synchronizer.
- Reset is per clock domain. Because metastability is resolved by a clock, each asynchronous clock domain needs its own reset synchronizer, all fed from the same raw ARST_N; sharing one across domains re-creates the unsynchronized-release bug in the others. Never gate or derive the reset combinationally.
- The release bug is a timing bug RTL sim cannot see. An unsynchronized reset release passes every zero-delay RTL simulation and still fails in silicon, because recovery/removal and metastability only exist with real timing. Sign reset off at the gate/timing level — STA recovery/removal slack, gate-level SDF, and CDC/reset lint — and verify the two RTL-checkable properties: assert works clock-free, and the deassert lands only after
STAGESedges, on a clean shared edge. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.