Skip to content

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.

the-need.v — why neither pure style is enough on its own
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 flow
The reset synchronizer — async assert into the clears, sync release through 2 flopsasync clear1 walks inARST_N (raw)active-low async reset inFF1 (D = 1)clk; async clear = ARST_NFF2 (D = FF1)clk; async clear = ARST_NRST_SYNC_Nasync assert, SYNC deassertreset treelow-skew fan-out to the design
ARST_N drives the async CLEAR of both flops, so asserting reset is immediate and clock-free (power-up-safe). The first flop's D is tied to the inactive level (constant 1); when ARST_N releases, that 1 walks through the two flops one clock edge at a time. FF1 may go metastable on the release but has a full clock period to settle before FF2 samples it, so RST_SYNC_N deasserts on a clean, resolved, clock-aligned edge. One synchronizer per clock domain, all fed from the same raw ARST_N. This structure is identical in SystemVerilog, Verilog, and VHDL.

The 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 flow
Reset RELEASE on a timeline — pure-async (hazard) vs synchronized (clean)phase?unsync2-FFCLK edgesthe active edges reset must line up toARST_N risesasynchronous — arbitrary phaserecovery/removalrelease near an edge => metastablepure-async resultsome flops leave reset a cycle LATEsynchronizedresultRST_SYNC_N rises ON an edge => together
A pure asynchronous reset releases at a phase unrelated to CLK. When that rising edge falls in a flop's recovery/removal window it can drive the flop metastable and it resolves a cycle late — so across a skewed reset tree, some flops leave reset on cycle N and others on N+1: an inconsistent initial state that depends on temperature, voltage, and the board. Route the release through the two-flop synchronizer and RST_SYNC_N only ever rises ON a clock edge, so the whole domain leaves reset on the same cycle. The assert path (not shown) is immediate in both cases — the release is the entire story.

4. 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.

reset_sync.sv — async assert, 2-FF synchronized deassert (active-low)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

reset_sync_tb.sv — clocked self-check: assert clock-free, deassert after STAGES edges
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

reset_sync.v — reset synchronizer in Verilog (2-FF deassert)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reset_sync_tb.v — self-checking; assert clock-free then count edges to deassert
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

reset_sync.vhd — reset synchronizer in VHDL (async assert, rising_edge deassert)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
reset_sync_tb.vhd — self-checking; assert clock-free, deassert after STAGES edges
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

reg_with_sync_reset.sv — a normal async-reset register, but fed RST_SYNC_N
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reg_with_sync_reset_tb.sv — clocked: q holds RESET_VAL through reset, tracks d after
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reg_with_sync_reset.v — async-reset register fed the synchronized reset (Verilog)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reg_with_sync_reset_tb.v — self-checking Verilog; check in-reset and post-release
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reg_with_sync_reset.vhd — async-reset register fed the synchronized reset (VHDL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
reg_with_sync_reset_tb.vhd — self-checking VHDL; in-reset value then post-release tracking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

reset_bug.sv — BUGGY (raw async release) vs FIXED (synchronized release)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
reset_bug_tb.sv — clocked functional check (both come up at RESET_VAL, track d after)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reset_bug.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
reset_bug_tb.v — self-checking Verilog functional check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
reset_bug.vhd — BUGGY (raw async release) vs FIXED (synchronized) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
reset_bug_tb.vhd — self-checking VHDL functional check (bind to core_good)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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), Verilog if (rst_sync_n !== 1'b0) $display("FAIL…"), VHDL assert 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 STAGES clock edges have passed, then rises — and rises on an edge, never combinationally. The §4a testbenches count edges: the output must be low for the first STAGES-1 edges and high only after the STAGES-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 than STAGES — 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 STAGES clock edges and then rises coincident with a rising clock edge — never in between. Every design flop's q holds RESET_VAL until that shared edge and begins tracking d on the first edge after. The visual signature of the bug is RST released and one flop's q updating 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.

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 STAGES edges, on a clean shared edge. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.