Test needs pins and pins are scarce: scan-in, scan-out, test clock, test mode and test reset compete with functional I/O, and a test pin that floats at reset can put the chip into test or hold it there. The entry sequence is the other half of the danger: switching clocks and isolation while the chip runs corrupts state the mission never recovers. NanoSoC's user pins are the 16 GPIO pads (the UART and SPI share six of them), plus the reset pin and the crystal, so a test extension has to say which of them it borrows and when. The reset-state table and the ordered sequence below are what keep the mission safe.
Each test pin has a defined reset state that is safe for mission: test mode pulled to inactive so a floating pin can never enter test, test clock gated off, scan outputs tristated or parked. Shared pins are allowed only with a documented priority: at reset the mission function owns the pin until the entry sequence claims it, and the test-mode pin is sampled only after the reset pin's debounce (16 clk_per cycles, 666.7 ns) has completed. Entry runs in order: halt the clocks cleanly, assert resets, switch muxes to test sources, configure isolation for the test configuration, then release test reset and start shifting. Exit reverses the order and ends with a full mission reset release through the normal sequence, because no mission state survives test and pretending it does is how bricked bring-up starts.
# Entry and exit order. The preconditions live in their own table, so a
# reordered sequence can be caught; the check does not trust the list itself.
ENTRY = ["halt clocks", "assert resets", "switch to test sources",
"configure isolation", "release test reset", "start shifting"]
EXIT = ["stop shifting", "assert resets", "restore mission sources",
"restore isolation", "mission reset release"]
NEEDS_ENTRY = {"assert resets": "halt clocks",
"switch to test sources": "assert resets",
"configure isolation": "switch to test sources",
"release test reset": "configure isolation",
"start shifting": "release test reset"}
NEEDS_EXIT = {"assert resets": "stop shifting",
"restore mission sources": "assert resets",
"restore isolation": "restore mission sources",
"mission reset release": "restore isolation"}
def check(seq, needs):
for i, step in enumerate(seq):
need = needs.get(step)
assert need is None or need in seq[:i], f"{step} before {need}"
return True
assert check(ENTRY, NEEDS_ENTRY) and check(EXIT, NEEDS_EXIT)
bad = ENTRY[:3] + ["release test reset", "configure isolation"] + ENTRY[5:]
try:
check(bad, NEEDS_ENTRY)
raise SystemExit("reordered entry was not caught")
except AssertionError as e:
assert "release test reset before configure isolation" in str(e)
# Reset-state model: what a pin resolves to when nobody drives it.
PIN_AT_RESET = {"test_mode": "pull-down", "test_clk": "pull-down",
"scan_in": "none"}
def resolve(pin, driven):
if driven is not None:
return driven
return {"pull-down": 0, "pull-up": 1, "none": "X"}[PIN_AT_RESET[pin]]
assert resolve("test_mode", None) == 0 # floating pin cannot enter test
assert resolve("scan_in", None) == "X" # unpulled pin is flagged, not trusted
assert resolve("test_mode", 1) == 1 # an explicit drive still wins
print("PASS: reordered entry rejected; floating test_mode resolves to 0; unpulled pin is X")The reset-state table for every test and shared pin, with pulls specified and the floating-pin case argued explicitly.
The entry and exit sequences above, simulated in both directions including refusal of illegal transitions.
The test-mode pin sampled only after the reset debounce, and the shared-pin priority written down.
Simulate entry from a running mission: the switch must leave no half-muxed clock and no unisolated crossing, checked with assertions on every mux select during the sequence. On the bench, confirm the reset state of each pin before the first entry attempt; a pin that floats toward test will eventually get there on a noisy board. If exit leaves the mission misbehaving, stop debugging the mission and rerun the mission reset release: exit without a full release is not exit.
Refuse and stay in mission: the capture clocks come from the PLL, so an unlocked PLL gives undefined capture timing. The refusal must be clean, with mission clocks and resets untouched. Entry into a shift-only configuration on the tester's clock does not depend on the PLL.