The reset reaches every flop through a tree of buffers, and the arrival times differ. If the spread is small compared with the recovery slack of the latest flop and the removal slack of the earliest, every flop takes the same first clock edge after the release. A small spread that straddles an edge minus the recovery time still splits the release: some flops leave reset one cycle before others, and the design starts in an inconsistent state.
import math
rec = 0.20
arrivals = [2.25, 2.60, 2.95, 3.20, 3.55] # release arrival at five flops (ns after the launch edge)
def first_edges(T):
return sorted({math.ceil((a + rec) / T) * T for a in arrivals})
safe = first_edges(10.0)
tight = first_edges(3.0)
assert safe == [10.0]
assert tight == [3.0, 6.0]
spread = max(arrivals) - min(arrivals)
assert abs(spread - 1.30) < 1e-9
print("PASS: spread", round(spread, 2), "ns; 10 ns clock first edges", safe, "; 3 ns clock first edges", tight)The five flops see the release between 2.25 and 3.55 ns. With a 10 ns period they all take the edge at 10 ns. With a 3 ns period the two earliest take the edge at 3 ns, while the other three miss it (their release plus recovery time is after 3 ns) and take the edge at 6 ns: the flops come out of reset one cycle apart. A counter that depends on all of them would start in a wrong state.
The reset network has a skew budget relative to the clock period; the spread of release arrival times across a domain is reported.
Release is synchronised per clock domain, so the tree only has to be balanced against one clock.
Reset buffers are in the timing analysis, not treated as ideal, because their delay is the arrival being checked.
The earliest flop needs an edge at or after 2.45 ns (edge at 4 ns). The latest needs an edge at or after 3.75 ns (also 4 ns). Both take the 4 ns edge, so the release is not split, but the latest flop has only 0.25 ns of recovery slack.