Release is the dangerous edge. Assertion can be asynchronous because forcing flops into reset needs no clock, but release near a clock edge violates recovery and removal on some flops and not others, leaving the domain half reset. The fix is a synchroniser per domain: assert everywhere at once, release on a clock edge. This lesson builds that cell, proves it with a waveform, and states the timing that signs it off. Its one job is the release circuit; the order it serves is the previous lesson.
The circuit is taught in Async Assert, Sync Deassert; this lesson shows the NanoSoC instances and what the integrator signs off. NanoSoC has three reset nets, rst_per_n, rst_sys_n and rst_cpu_n, and each leaves nano_rstpwr through its own nano_rst_sync (two flops) in its own domain, so three instances on three clocks. Assertion is asynchronous and reaches every endpoint without a clock. On release the first flop may violate its own recovery and removal against arst_n: that is the metastability the second flop absorbs. The output rst_out_n always changes shortly after a clock edge, so the destination flops' recovery and removal are an ordinary timing check on the reset tree: clk-to-q plus buffering must fit inside one period, which is 5.208 ns in clk_cpu, 10.417 ns in clk_sys and 41.667 ns in clk_per (see Reset Tree Timing). The minimum assertion width of 16 domain cycles guarantees the reset is seen by the slowest clock in the domain.
// NanoSoC teaching values: per-domain reset synchroniser, async assert,
// two-flop synchronised release. One instance per reset net per domain.
module nano_rst_sync (
input logic clk, // domain clock
input logic arst_n, // raw reset in, active low, async assert
output logic rst_out_n // released reset out, active low, sync release
);
logic q1, q2;
always_ff @(posedge clk or negedge arst_n) begin
if (!arst_n) begin
q1 <= 1'b0;
q2 <= 1'b0;
end else begin
q1 <= 1'b1;
q2 <= q1;
end
end
assign rst_out_n = q2;
endmoduleThe waveform below is simulation output for the wave source of this topic: an asynchronous assertion that clears both flops at once, a release near a clock edge that emerges two cycles later, and a second assert-release pair. One slot is one domain-clock cycle.
How to read it: arst_n starts asserted and releases just before the edge at slot 3, so q1 rises in slot 3 and q2 in slot 4: the two-cycle synchronised release. arst_n falls again just before an edge and both flops clear together in slot 10, which is the asynchronous assert: no clock edge was needed. The second release puts q1 in slot 14 and q2 in slot 15, one slot apart as before, proving the cell re-arms. q2 only ever changes right after a clock edge, never in between, so every flop downstream sees the release aligned to its own clock: that is what recovery and removal need.
# Release latency of the three nano_rst_sync instances, plus the R5 hold.
# All NanoSoC NanoSoC-specification values.
T = {"cpu": 1e9 / 192e6, "sys": 1e9 / 96e6, "per": 1e9 / 24e6}
EXPECT = {"cpu": 10.417, "sys": 20.833, "per": 83.333}
for dom, period in T.items():
sync_ns = 2 * period
print(dom, f"sync release {sync_ns:.3f} ns")
assert abs(sync_ns - EXPECT[dom]) < 0.01
# R5 check: 16-cycle hold + 2-flop sync = 18 sys cycles = 187.5 ns.
assert abs(16 * T["sys"] + 2 * T["sys"] - 187.5) < 0.01
# The assertion width is 16 cycles of each domain's OWN clock: the slowest
# clock (clk_per) needs the longest time, 666.7 ns; clk_cpu only 83.3 ns.
assert abs(16 * T["per"] - 666.7) < 0.1
assert abs(16 * T["cpu"] - 83.333) < 0.01
assert max(16 * p for p in T.values()) == 16 * T["per"]
print("PASS: per-domain sync release 2 cycles; R5 hold-plus-sync 187.5 ns")One synchroniser instance per reset net per domain, three in NanoSoC, named in the netlist, never a shared release across clocks.
Recovery and removal checked on the reset tree behind each synchroniser (clk-to-q plus buffering inside one period of that domain).
Assertion width proven at 16 cycles of each domain's own clock, so the slowest clock, clk_per at 666.7 ns, sets the longest requirement.
Release reset at every phase against the domain clock in simulation, including just before the edge: the output must always move right after an edge, never in between. A domain that wakes half-reset, some flops live and some dead, points at a release that bypassed the synchroniser, often a test-mode mux that was never switched back. For the bug classes see the hardware-debug reset-domain lessons.
Marginally, at the cost of a cycle: each extra stage adds about a clock period to the resolving time and multiplies the MTBF. Two flops with met timing is the standard choice; depth beyond two needs an MTBF argument, not a feeling.