In RTL, an uninitialised flop reads zero and the test passes; in the netlist with delays it reads X and the X spreads until the whole result is unknown. The first GLS run on any chip drowns in X, and the team must sort real reset bugs from simulation pessimism before any timing failure is visible. Its one job is that sort: where X comes from, which X is a bug, and how reset order is proved when delays apply.
X has four sources on NanoSoC, in the order to check them. Unreset flops: control flops the RTL never reset, reading zero in RTL sim and X in GLS — every one is a reset bug until proved unreachable, and the proof is a reset-value sweep in GLS, not an argument. SRAM outputs before first write: the memories power up unknown, so any read before the stack clear or the loader write is X by construction; the fix is ordering, and the boot does it right. Clock-gate enables unknown at start: a gate whose enable flop is not yet reset drives X onto its clock and the X spreads into every flop behind it; it clears once the enable is reset, and a gate whose enable never resolves is a reset bug. Synchroniser outputs at release: the two-flop cells resolve within their destination cycles; X here that clears after two cycles is the model working, X that persists is a missing synchroniser.
Reset order in the netlist adds one check RTL cannot do: release skew. The synchroniser guarantees release on a clock edge per domain, but the tree from the cell to the last flop spans the floorplan, and the last flop can release a cycle later than the first. GLS with annotation proves the bound the review needs: all endpoints of one net release within the same cycle, and the staged order — peripherals, fabric, CPU with its 8-cycle gap — still holds end to end. The script below audits the X sources the way the triage does: every X traced to one of the four bins before timing debug starts.
# X triage as a function: classify each X seen in the first GLS window from
# observable facts, then check the classifier and the release checks can fail.
# NanoSoC teaching values (reset release table); signal names are illustrative.
def classify(x):
if x["is_memory_output"]:
return "ordering" # SRAM before first write: X by construction
if x["is_gate_enable"]:
return "gate-enable" if x["clears_after_reset"] else "rtl-fix"
if x["has_reset"]:
return "sync-resolving" if x["cycles_x_after_release"] <= 2 else "rtl-fix"
return "rtl-fix" # a flop with no reset is a reset bug until proved unreachable
def X(name, **kw):
base = dict(name=name, is_memory_output=False, is_gate_enable=False,
has_reset=True, clears_after_reset=True, cycles_x_after_release=0)
base.update(kw)
return base
window = [X("u_xbar/sel_q", has_reset=False), X("u_sram0/dout", is_memory_output=True),
X("u_intc/sync_q", cycles_x_after_release=2),
X("u_dma_gate/en_q", is_gate_enable=True)]
fixes = [x["name"] for x in window if classify(x) == "rtl-fix"]
assert fixes == ["u_xbar/sel_q"], fixes
assert classify(X("u_intc/sync_q", cycles_x_after_release=5)) == "rtl-fix" # X past release
# release checks: staged order across nets, same-cycle release within a net
T_SYS_NS = 1e9 / 96e6
def staged_ok(t_sys_ns, t_cpu_ns):
return t_cpu_ns > t_sys_ns
def skew_ok(endpoint_times_ns, period_ns):
return max(endpoint_times_ns) - min(endpoint_times_ns) < period_ns
assert staged_ok(187.5, 187.5 + 93.75)
assert not staged_ok(187.5, 10.4) # bug 2 in the netlist: the CPU comes out after only its own 2 clk_cpu sync
assert not staged_ok(187.5, 187.5) # strict: released together is also a failure
assert skew_ok([187.5, 190.1, 192.0], T_SYS_NS)
assert not skew_ok([187.5, 190.1, 199.0], T_SYS_NS) # last flop a cycle late
print(f"{len(window)} Xs binned, {len(fixes)} demands an RTL fix: {fixes[0]}")
print("PASS: X triage complete, release order and skew checks can fail")The first-GLS X list with every entry binned and the unreset bin empty or fixed — no waived X without a source.
The release-skew bound per reset net, measured with annotation, inside one destination cycle.
SRAM-before-write reads named as ordering facts with the boot stage that writes first quoted.
Force each X source in turn to isolate it: initialise the suspect flops, preload the SRAM, hold the gate open, and watch which X clears. The X that survives all forcing is the real bug. Never globally initialise the netlist to zero to make GLS pass — that converts the lens back into RTL and the exercise proves nothing.
At whether the loader wrote all 8,192 bytes: SRAM before its first write is X by construction, and the checksum covers exactly the image, 2,048 words from 0x2000_0000 to 0x2000_1FFF. A loop bound or loader length one word off reads an unwritten word; RTL simulation hides it only if the memory model initialises to zero.