A sequencer that waits forever is a hang with a nicer name: power-good never arrives because the board supply failed, the PLL never locks, the busy bit never clears because firmware crashed mid-job. Each wait needs a number after which the machine stops waiting and reports, and each number needs a reason. This lesson sizes the three hardware timeouts (ramp, power-good, relock) plus the one software timeout (quiesce), says what fires them, and says honestly what no timeout on the chip can catch.
Timeouts are sized at four times the nominal wait, wide enough to absorb slow silicon and cold rails, tight enough to fail fast on a dead board. The ramp timeout covers the 64-clock switch ramp; the power-good wait covers the ramp plus the 8-clock margin; the relock wait covers the 256-clock PLL lock. The quiesce wait is software's job, not the sequencer's: the driver polls DMA and SBR idle before writing SW_OFF_REQ, with a software-side timeout of four times the longest job, 4 x 72 clk_sys = 288 clk_sys (3.0 µs, which is 72 clk_per), before it declares the DMA hung. On a hardware timeout the machine stops in the one state that is safe without a usable rail, SLEEP's state (switch open, isolation held, PD_AON running), and records the event in a sticky status bit. This is an illustrative extension: the NanoSoC PWR_IRQ register defines only LOCK_EVT and UP_DONE, so a real chip would add a timeout event bit. What no timeout can catch is a dead crystal: the sequencer and its counters run on that crystal, so with no clock nothing counts. Detect it outside the chip, with a supervisor or a watchdog on a separate clock (NanoSoC has none), or by the board-level check that por_n is released but rst_per_n never is.
# Sequencer timeout sizing, NanoSoC teaching values (NanoSoC clock and power sequences).
T_PER_NS = 41.6667 # clk_per period in ns
T_SYS_NS = 10.4167 # clk_sys period in ns
FACTOR = 4 # every timeout is four times nominal
nominal = {"ramp": 64, "pgood": 64 + 8, "relock": 256} # clk_per
timeouts = {k: FACTOR * v for k, v in nominal.items()}
assert timeouts == {"ramp": 256, "pgood": 288, "relock": 1024}, timeouts
# Software quiesce timeout: longest DMA job is 72 clk_sys, also x4.
quiesce_sys = FACTOR * 72
quiesce_per = quiesce_sys * T_SYS_NS / T_PER_NS
assert quiesce_sys == 288 and round(quiesce_per) == 72
assert abs(quiesce_sys * T_SYS_NS / 1000.0 - 3.0) < 0.01 # 3.0 us
# Each timeout must exceed its nominal wait, and the relock one is the longest.
assert all(timeouts[k] > nominal[k] for k in nominal)
worst_us = timeouts["relock"] * T_PER_NS / 1000.0
assert abs(worst_us - 42.667) < 0.01, worst_us
# A PLL that locks late (300 clk_per) still passes; one that never locks trips.
def relock_outcome(lock_after):
return "locked" if lock_after <= timeouts["relock"] else "timeout"
assert relock_outcome(300) == "locked" and relock_outcome(10**9) == "timeout"
print("PASS: HW timeouts ramp/256 pgood/288 relock/1024 clk_per; SW quiesce 288 clk_sys (3.0 us)")Each wait state names its timeout in cycles and the multiple of nominal it represents; no wait is unbounded.
Timeout expiry always lands in a defined safe state (switch open and isolation held, or a fully powered state), never half-way through the ramp.
Each timeout sets a sticky status bit that firmware and the lab can both read; the dead-crystal case is listed as outside the sequencer's reach.
Test each timeout by breaking its wait: hold power-good low, kill the lock signal, stall the ramp model, and (for the software timeout) hold the DMA busy. Each hardware timeout must end in its safe state with its status bit set within its cycle budget. If a timeout never fires, the counter is clocked by a clock that the state itself gates; sequencer counters run on the always-on peripheral clock for exactly this reason.
Every cold boot would pay it or trip it: the reset controller already waits for lock on its own counter (step R3 of the cold-start timeline), so a second timeout in series either duplicates the wait or false-fires. Keep the two lock waits distinct and qualify both on the lock signal.