The safe sequence works when both clocks run and the analysis knows about the mux. Real chips violate both assumptions: a source dies, a constraint tells the tools the mux output is one clock when it is the other, or firmware raises the frequency before the voltage arrives. Each failure has the same shape, an assumption the sequence needed that nobody checked. This lesson lists the three with their checks. Its one job is the failure set; the correct sequence belongs to the previous lesson.
First, the dead clock. The handshake waits for an edge on each source, so either clock being dead hangs it. A dead old source leaves its enable stuck on, because the drop can never be synchronised. A dead new source leaves both enables off after the old one dropped, and no clock at all. The abort path is a timeout that forces the enable flops, through their asynchronous reset, to the known-good source, and accepts that one truncated pulse is possible at that moment: the clock is declared failed, so downstream logic is held in reset while it happens. NanoSoC keeps the crystal path alive for exactly this reason: the always-on domains never need a switch to a clock that can be dead.
Second, the wrong timing exception. The mux output carries two clocks in different modes, and the analysis must constrain each mode to its own clock. A false path drawn across the mux to quiet a report, or a case analysis that pins the test select while the chip runs functional, signs off timing that was never analysed. Every exception on a clock mux needs evidence attached: which select value it covers and which modes it applies to.
Third, the ratio sweep. When voltage and frequency change together the order is fixed: raise voltage first, then frequency; lower frequency first, then voltage. Reversing it runs fast logic on a low rail. As documented in the NanoSoC specification, NanoSoC runs at one operating point, so its crossing assumptions never change; the power architecture section adds DVFS as a chip-level extension with illustrative lower gears and reuses this lesson's rule: every crossing is re-verified at every ratio.
# Synchroniser latency across ratios, in source cycles: k * (f_src / f_dst).
# A 2-flop sync out of the fast domain costs more fast cycles than out of
# the slow one; a DVFS extension would move every number in this table per operating point.
RATIOS = {"cpu": 192e6, "sys": 96e6, "per": 24e6}
K = 2
def sync_cost_in_src(src, dst):
return K * (RATIOS[src] / RATIOS[dst])
table = {(s, d): sync_cost_in_src(s, d) for s in RATIOS for d in RATIOS if s != d}
assert table[("cpu", "sys")] == 4.0
assert table[("sys", "cpu")] == 1.0
assert table[("sys", "per")] == 8.0
assert table[("per", "sys")] == 0.5
print("PASS: 2-flop latency cpu->sys 4 cpu cycles, sys->per 8 sys cycles")The dead-clock path is drawn beside the happy path with its timeout or bypass, and both are verified.
Every timing exception touching a clock mux names its select value and its modes, with evidence, not blanket waivers.
If frequencies can change, every crossing is verified at every ratio; on NanoSoC the single operating point is stated as the reason this collapses to one check.
Kill each source in simulation, old and new in turn, and attempt the switch: the controller must land somewhere safe, never with both enables high and never hung. Audit the exception list for any line that mentions a mux output without a mode guard. A failure that appears only at one operating point, or only with the test select forced, points at this lesson before it points at the logic.
The synchroniser latencies, the pulse widths and the crossing margins are single numbers. With DVFS each becomes a table over operating points, and every entry needs its own verification.