When the timeline is not met, the lab needs a way to find out why. NanoSoC v1 has no debug port and no trace block: its block list has a UART, an SPI master, 16 GPIO pins, a reset pin and a crystal. If the CPU is stuck, no register can be read from outside. Its one job is the honest inventory of what can be seen, the triage that turns a silent pin into a stage and a short list of suspects, and the gaps worth raising while the design can still change.
Observability on this chip comes from three places. Pins: the UART transmit line (pad P0) carries the banner and nothing else, because the shipped ROM prints only the banner; the SPI lines (pads P2 to P5) show the load; the GPIO pads P6 to P15 could show a status code, but the boot flow never writes the GPIO block, so on v1 they stay low; and the reset pin and crystal are inputs the lab controls. Registers read by software: RST_CAUSE, PLL_STATUS, PWR_STATUS, the INTC RAW, PENDING and TOP registers, and the DMA and SBR STATUS and COUNT registers, all readable only by code that is running. Timing: the timeline of the previous lesson, because a stage that stops leaves its successors silent.
That inventory has gaps a review should state plainly. The boot flow table lists only success steps; it does not say what the ROM does when the self-test or the checksum fails. Without a defined failure path (print a code, drive a GPIO pattern, stop in a known loop), a failed self-test and a hang look the same on the pins. And with no debug access, a CPU stalled at 0x0000_0000 (planted bug 2) can be told apart from other early failures only by the absence of the banner. These are review findings, not claims about the shipped chip: they must be closed in the ROM content, which is final at tape-out, or accepted knowingly. Any added hardware access (a debug port with security gating, a status register that mirrors stage numbers) is a design proposal for a later version, not part of NanoSoC v1; how such a port would be designed, and what it would cost in pads, is the subject of the debug-infrastructure and test-pin lessons linked below.
A per-step code in the ROM is the cheap in-ROM hook, and it has a price that has to be written next to the proposal. A UART character is 10 bits at 115,384.6 baud, 86.7 us each, so a code printed at each of five self-test steps adds 433 us and moves every later event of the timeline. A GPIO status write is one clk_per register access, 281.25 ns. The pads P6 to P15 are GPIO-only, so no peripheral shares them, but GPIO.DIR resets to 0 (all inputs), so the ROM must write DIR first: six writes in all, about 1.7 us. Either is a ROM content change, final at tape-out, and the timeline of the previous lesson then has to include it.
Pin observation | Stage that stopped | First suspects (planted bugs where they apply) | Next probe |
|---|---|---|---|
No UART activity at all | Before UART init: reset, clocks, first fetch, stack clear | Crystal or reset pin; PLL lock; CPU released early, stalled at PC 0 (bug 2) | Reset pin and crystal on the scope; compare with the reset-release timeline |
Chip-select (pad P5) low from power-on, banner timing normal | A pad fault, not a boot stage | Pad bug P1: pad P5 given a pull-down; the flash was selected before the first clock | Open-pin level of P5 with the chip in reset; compare with the reset-state table |
Banner printed, SPI chip-select never falls (or falls only sometimes) | DMA self-test | Interrupt on the wrong input (3), byte-offset wiring (4), NS default (7), LEN mismatch (8), overlap (1); an intermittent banner-then-silence also fits the unsynchronised crossing (5) | Only a per-step code or GPIO pattern, if the ROM had one (a proposal), would separate them; otherwise repeat the run to tell the intermittent bug 5 from the rest |
Chip-select falls and never rises | SPI load | Flash not responding, SPI wiring or clock divisor, wrong image size | SPI clock and data lines against the 6 Mb/s timeline |
Chip-select rises, then silence from the image | Checksum or jump | Wrong image, corrupted load, image does not print | Compare the stored checksum; image's own first output |
The banner appears again at random, long after boot | Reset, not a boot stage | Pad bug P3: RESET_N without a pull-up, so noise holds it low for more than 666.7 ns (RST_CAUSE bit 1 set, SRAM0 cleared) | RESET_N on the scope; add the board pull-up and capacitor |
UART framing errors only when software drives all 16 GPIO pads | Pads and supply, not a boot stage | Pad bug P2: both vdd_io pairs placed side by side, 13 outputs on one pair against a limit of 8 | Scope vdd_io while GPIO DATA_OUT toggles 0xFFFF; check the pair per half of the ring |
# Pin-observation triage: the stage each planted bug stops the boot at comes from
# running it in simulation (the table of stages below); the pin pattern comes from the
# stage, and suspects are derived from both. NanoSoC teaching values; codes are ILLUSTRATIVE.
# Pin pattern after each stage: (banner, cs_fell, cs_rose, image_output).
PATTERN = {"before-uart": (0, 0, 0, 0), "dma-self-test": (1, 0, 0, 0),
"spi-load": (1, 1, 0, 0), "checksum-or-jump": (1, 1, 1, 0), "booted": (1, 1, 1, 1)}
STAGE_OF = {2: "before-uart", 1: "dma-self-test", 3: "dma-self-test", 4: "dma-self-test",
5: "dma-self-test", 7: "dma-self-test", 8: "dma-self-test"} # 5: intermittent
def triage(banner, cs_fell, cs_rose, image_output):
stage = next((s for s, p in PATTERN.items() if p == (banner, cs_fell, cs_rose, image_output)), None)
return stage, {b for b, st in STAGE_OF.items() if st == stage}
for stage, pat in PATTERN.items():
assert triage(*pat)[0] == stage
assert triage(0, 0, 0, 0)[1] == {2}
assert triage(1, 0, 0, 0)[1] == {1, 3, 4, 5, 7, 8} # six suspects behind one pin pattern
# separability is computed, not asserted: group the bugs by what the pins (and codes) show
def groups(key):
out = {}
for b in STAGE_OF:
out.setdefault(key(b), set()).add(b)
return sorted(out.values(), key=lambda g: (-len(g), sorted(g)))
pins_only = groups(lambda b: PATTERN[STAGE_OF[b]])
assert pins_only[0] == {1, 3, 4, 5, 7, 8} # not separable by pins alone
# a code per self-test step (what the bug was waiting for); bug 5 waits for the interrupt, like 3 and 7
STEP_OF = {4: "reg-values", 3: "irq-wait", 5: "irq-wait", 7: "irq-wait", 1: "readback", 8: "count-check"}
by_step = groups(lambda b: (PATTERN[STAGE_OF[b]], STEP_OF.get(b)))
assert by_step[0] == {3, 5, 7} and len(by_step) == 5 # splits to one group of three
# plus the DMA STATUS bits at the timeout: DONE for 3 and 5, BUSY with COUNT 0 for 7
STATUS_OF = {4: "pslverr", 3: "done", 5: "done", 7: "busy", 1: "done", 8: "done"}
by_status = groups(lambda b: (PATTERN[STAGE_OF[b]], STEP_OF.get(b), STATUS_OF.get(b)))
assert {3, 5} in by_status and all(len(g) <= 2 for g in by_status) # 3 and 5 only differ by repeating the run
# the price of the hook: a UART character is 10 bits at 115,384.6 baud, a GPIO write one clk_per access
char_us = 10 / 115384.6 * 1e6
steps = 5
uart_cost_us = steps * char_us
gpio_cost_ns = (1 + steps) * 281.25 # GPIO.DIR resets to 0, so one DIR write first
assert abs(char_us - 86.67) < 0.01 and abs(uart_cost_us - 433.3) < 0.1 and abs(gpio_cost_ns - 1687.5) < 1e-9
print(f"six suspects behind one pin pattern; a step code leaves {len(by_step)} groups; UART code {uart_cost_us:.0f} us, GPIO code {gpio_cost_ns / 1000:.2f} us")
print("PASS: pin patterns map to stages; one pattern hides six suspects until a code is added, and the code has a price")A written inventory of every observable (pins and registers) with the instrument that reads it, and a statement of what cannot be seen at all.
A defined failure behaviour for every boot stage: what the ROM prints or drives on the pins, and where it stops, so no failed stage looks like a hang.
For each gap, an owner and a decision: close it in the ROM content before tape-out, add hardware in a later version, or accept it with the consequence written down.
Prove the observability in simulation, where everything is visible: run each planted bug in the boot test and write down what the pins show for it, with the pad pulls modelled, since the RTL has none. If two bugs give the same pin pattern, the readiness review has found a place where silicon will be harder to debug than simulation, and a cheap hook (a GPIO pattern per step, 281.25 ns a write, rather than a printed character at 86.7 us) is worth adding to the ROM now, because the ROM cannot be changed afterwards. For the debug method itself, follow the Hardware Debug links.
The interrupt on spare input 6 (bug 3) and the SBR's non-secure attribute against secure-only SRAM1 (bug 7) both leave the self-test waiting. In simulation the bench can read STATUS and separate them: with bug 7 the DMA stays BUSY with COUNT 0 and SBR STATUS.ERR is set; with bug 3 the DMA reads DONE = 1 and only the interrupt never reaches the controller. On silicon only a ROM variant that drives that status onto the GPIO pads would do the same, which is a proposal, not v1.