Reset: assert, release, and what RTL cannot show
Tell an asynchronous reset from a synchronous one by what they do between clock edges, see why the release is the dangerous moment, and add the reset synchronizer that makes every flop leave reset in the same cycle.
- Lesson
- R2.2
- Time
- 30 min
- Runs on
- Your browser
- Check
- 5 questions
A block came out of reset one cycle later than its neighbour, and a handshake that expected both to be ready in the same cycle hung. The reset wire was released just before a clock edge: one flop saw the release before that edge, the other after it. Every RTL simulation had passed, because a simulator releases the reset at one exact instant for every flop.
- A sees the release in time
The release reaches A well before edge k. A leaves reset at edge k.
Follow the calls, values and waits
- data release: rst_n pin → Flop A
- B sees it inside its window
The release reaches B a moment before edge k, inside its recovery window: the short time before an edge in which a flop cannot take a reset change cleanly. B may leave reset at edge k or k+1, or sit metastable, hovering between 0 and 1, for a while.
Follow the calls, values and waits
- data release: rst_n pin → Flop B
- The handshake hangs
A is ready at edge k and B at edge k+1. Logic that expected both in the same cycle waits for an event that already passed.
Follow the calls, values and waits
- data ready: Flop A → Handshake
- data ready: Flop B → Handshake
Read the diagram as text
- rst_n pin: released near edge k
- clk: edge k, then k+1
- Flop A: near the pin
- Flop B: far end of the wire
- Handshake: needs A and B ready together
- rst_n pin → Flop A: short wire
- rst_n pin → Flop B: long wire
- clk → Flop A
- clk → Flop B
- Flop A → Handshake: ready
- Flop B → Handshake: ready
Drawn from the timing rules, not from a simulation. A zero-delay RTL simulation releases every flop at the same instant.
Assertion: at once or at the next edge
A reset has two moments. Assertion puts every register into its reset state. Release (also called removal) lets the registers follow their inputs again. The two reset styles differ in when assertion acts.
Asynchronous reset
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) a_q <= 4'd0;
else a_q <= a_q + 4'd1;
rst_n is in the event list, so the block runs the moment rst_n falls, with or without a clock. Cell: $_DFF_PN0_.
Synchronous reset
always_ff @(posedge clk)
if (!rst_n) s_q <= 4'd0;
else s_q <= s_q + 4'd1;
The block runs only at a clock edge, so rst_n is just another data input. Cell: $_SDFF_PN0_.
An asynchronous reset works before the clock runs, which is what a chip needs at power-up. A synchronous reset needs edges, and it needs reset to be low at one of them. The lab bench shows what happens when it is not.
- Cycle 2
The power-on reset was released 1 ns after edge 1. Both counters count from edge 2.
- Cycle 5
Both counters are at 4. Four nanoseconds later rst_n falls.
- Cycle 6
During the pulse the bench printed
a_q=0 s_q=4: the asynchronous counter cleared at once. The pulse ended before edge 6, sos_qnever saw it and counted on to 5. - Cycle 10
The two counters now disagree for good: 5 against 9. The per-edge samples of rst_n never show it low.
Measured on the lab bench. Verilator's lint also flags the mix: %Warning-SYNCASYNCNET: Signal flopped as both synchronous and async.
Release: the moment simulation cannot show
Assertion is safe in both styles. Release is where an asynchronous reset gets dangerous. A flop with an asynchronous reset has a recovery time: the release must arrive at least that long before the next rising edge. It also has a removal time: the release must not come sooner than that after the edge. These are the reset-side names for setup and hold.
Release inside that window and the flop may leave reset a cycle early or late, or go metastable: its output hovers between 0 and 1 for a while before settling. Two flops of one block can land on different sides of the window, as in the hook.
The cure is the reset synchronizer. The pin still asserts the domain's reset at once. Its release goes through two flops clocked by the domain's clock, so it reaches every register just after an edge, a whole period before the next one.
- Assertion
rst_n_in falls. Both flops clear at once, with no clock, and the domain is in reset immediately. In the lab this is the
e5+5nsline,a_q=0.Follow the calls, values and waits
- data reset (0): rst_n_in → rst_n_out
- data reset (0): rst_n_out → a_q, s_q
- First edge after release
q1 captures the 1. If the release fell inside q1's window, q1 may be late or metastable; it has a whole period to settle before anything reads it.
Follow the calls, values and waits
- data release (1): 1'b1 → q1
- Second edge
rst_n_out takes the 1 from q1. It changes just after an edge, far from every window in the domain.
Follow the calls, values and waits
- data release (1): q1 → rst_n_out
- Third edge
Every flop in the domain sees the release at the same edge and counts. In the lab both counters count from edge 4 after power-on, and from edge 8 after the pulse.
Follow the calls, values and waits
- data release (1): rst_n_out → a_q, s_q
Read the diagram as text
- rst_n_in: from the pin
- 1'b1: constant
- q1: first flop
- rst_n_out: second flop
- a_q, s_q: every flop of the domain
- rst_n_in → q1: async clear
- rst_n_in → rst_n_out: async clear
- 1'b1 → q1: D
- q1 → rst_n_out: D
- rst_n_out → a_q, s_q: rst_n_sync
The synchronizer is two $_DFF_PN0_ flops on this flow. Its first flop can still go metastable; the second gives it a period to settle.
| Measured on the lab bench | No synchronizer | With reset_sync |
|---|---|---|
| First counting edge after the power-on release | edge 2 | edge 4 |
| Counters at edge 6, after the 5 ns pulse | a_q=1, s_q=5 | a_q=0, s_q=0 |
| First counting edge after the pulse | edge 6, a_q from 0 and s_q from 4 | edge 8, both from 0 |
| Synthesis on this flow | 20 cells, 8 flops, 3 levels of logic | 22 cells, 10 flops, 3 levels of logic |
The synchronizer costs two flops and two edges of release delay. It also stretches the short pulse, so the synchronous counter sees it too.
Three rules for reset code
Reset first, alone
The reset test is the first
ifof the block, and everything else sits in itselse.Reset every register of the block
A register left out of the reset branch is unknown until something loads it. Leave one out only when the brief says so, and give it its own block.
Never treat the reset as data
Do not read rst_n in the non-reset branch, and do not gate or derive it with logic inside the domain.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | from the reset pin, active-low, may change at any time |
| a_q | output | [3:0] | free-running counter with an asynchronous reset, plus one at every rising edge |
| s_q | output | [3:0] | free-running counter with a synchronous reset, plus one at every rising edge |
- Reset
a_qasynchronous,s_qsynchronous, both through onereset_sync- Latency
- after rst_n rises, the counters count from the third rising edge
- Parameters
- Memory: 8 counter flops plus 2 synchronizer flops
- Corner cases
- a reset pulse shorter than a clock period; a release just before an edge
The lab ships without the synchronizer, and reset_sync.sv sits beside it, read-only. Run it and find the pulse that cleared one counter only. Then put the synchronizer in and run again.
Asked in interviews
Synchronous or asynchronous reset: what are the trade-offs?
Asynchronous resets act without a clock and keep the reset off the data path, but their release must be synchronized. Synchronous resets are plain data inputs that filter short glitches, but they need a running clock and a pulse that spans an edge.
What are recovery and removal time?
They are the reset-side setup and hold of a flop. The release must come at least the recovery time before the next edge, and no sooner than the removal time after the previous one. Violate them and the flop may leave reset in the wrong cycle or go metastable.
Why synchronize the release of an asynchronous reset, and can RTL simulation check it?
So every flop of the domain leaves reset at the same edge, with the only metastable risk confined to the synchronizer's first flop. RTL simulation cannot check it: it has no recovery window and no metastability, so release timing needs the synchronizer by rule and static timing.
Try it
Without a synchronizer, both counters count from edge 2. The pulse after edge 5 clears a_q at once and misses s_q, so from edge 6 the counters disagree. Synthesize: 20 cells, four $_DFF_PN0_ flops for a_q and four $_SDFF_PN0_ for s_q.
Replace the assign line with reset_sync u_rs (.clk(clk), .rst_n_in(rst_n), .rst_n_out(rst_n_sync)); and run again. Both counters now count from edge 4. The pulse still clears a_q at once, sync stays low through edge 6, and both counters count together from edge 8. Synthesis grows to 22 cells and 10 flops.
Then delete the reset from the s_q block, leaving s_q <= s_q + 4'd1;. Every line prints s_q=x: an unknown value plus one is still unknown.
Synthesize the design to measure its cells and inspect the mapped gates.
e0 rst_n=0 sync=0 a_q=0 s_q=0 e1 rst_n=0 sync=0 a_q=0 s_q=0 e2 rst_n=1 sync=1 a_q=1 s_q=1 e3 rst_n=1 sync=1 a_q=2 s_q=2 e4 rst_n=1 sync=1 a_q=3 s_q=3 e5 rst_n=1 sync=1 a_q=4 s_q=4 e5+5ns rst_n=0 sync=0 a_q=0 s_q=4 e6 rst_n=1 sync=1 a_q=1 s_q=5 e7 rst_n=1 sync=1 a_q=2 s_q=6 e8 rst_n=1 sync=1 a_q=3 s_q=7 e9 rst_n=1 sync=1 a_q=4 s_q=8 e10 rst_n=1 sync=1 a_q=5 s_q=9Original design · 20 cells · 8 flip-flops · 0 latches · 3 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: reset_sync.sv
// reset_sync.sv: asynchronous assertion, synchronous release (read-only).
// rst_n_in may fall at any time and the output falls with it; when rst_n_in rises, the output rises
// two rising edges later, so every flop in the domain leaves reset in the same cycle.
module reset_sync (input logic clk, input logic rst_n_in, output logic rst_n_out);
logic q1;
always_ff @(posedge clk or negedge rst_n_in)
if (!rst_n_in) begin q1 <= 1'b0; rst_n_out <= 1'b0; end
else begin q1 <= 1'b1; rst_n_out <= q1; end
endmodule
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic rst_n = 1;
logic [3:0] a_q, s_q;
rst_demo dut (.clk(clk), .rst_n(rst_n), .a_q(a_q), .s_q(s_q));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
$display("e%0d rst_n=%b sync=%b a_q=%0d s_q=%0d", edge_n, rst_n, dut.rst_n_sync, a_q, s_q);
endtask
initial begin
#1 rst_n = 0; // power-on reset, asserted before edge 0
tick(); tick(); rst_n = 1; // released 1 ns after edge 1
repeat (4) tick(); // e2 .. e5
#3 rst_n = 0; // a 5 ns reset pulse: falls 4 ns after edge 5 ...
#1 $display("e5+5ns rst_n=%b sync=%b a_q=%0d s_q=%0d", rst_n, dut.rst_n_sync, a_q, s_q);
#4 rst_n = 1; // ... and rises 1 ns before edge 6
repeat (5) tick(); // e6 .. e10
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- An asynchronous reset acts the moment it is asserted, without a clock; a synchronous reset acts only at a rising edge, so a pulse between edges can miss it.
- The release is the dangerous moment: inside a flop's recovery or removal window it may leave reset a cycle early or late, or go metastable.
- A zero-delay RTL simulation cannot show recovery, removal or metastability; the reset synchronizer is added by rule.
- reset_sync asserts at once and releases two edges later, so every flop of the domain leaves reset in the same cycle.
- Put the reset test first, reset every register of the block, and never use the reset as data.