This lesson has one job: teach the pad table as the single source of truth for what each pad does. NanoSoC's sixteen GPIO pads are shared: P0 and P1 carry the UART, P2 to P5 carry the SPI flash bus, and P6 to P15 are GPIO only. The two peripherals never compete for a pad, but the GPIO block does compete with each of them, and a table that is wrong in one place puts two drivers on one board net. This is the classic pin-mux conflict, caught by a checker run against the ball-out and fixed by a single pad database that generates the RTL mux, the software header and the board netlist.
The table has one row per pad and, for each shared pad, one peripheral function. There is no pad-control register and no function-select field: NanoSoC has none in its register map. Instead the rule is one sentence. If the peripheral's CTRL.EN bit is 1, the pad belongs to the peripheral; otherwise it is a GPIO, with direction from GPIO.DIR, value from DATA_OUT and level read back in DATA_IN. At reset every EN is 0, so every pad is a GPIO input. The pulls are fixed in the pad cell, not programmable. Because the select is the enable bit software writes anyway, the mux needs no extra programming step, but it also means the select can change at any time the driver writes CTRL, which the next lesson turns into a discipline.
Two classes of pad bypass the mux entirely. RESET_N and the two crystal pads are dedicated: a muxed reset is a boot risk, and a muxed crystal is an oscillator that never starts. The power and ground pads are not signals at all. Everything else is the table below. The UART and SPI signals are counted once, as pads: the 19 signal pads are 16 GPIO pads, the reset and the crystal pair, with no extra pad for any peripheral.
Pad | Reset function (EN = 0) | Function when the peripheral is enabled | Direction when enabled | Owner bit |
|---|---|---|---|---|
P0 | GPIO0 | uart_tx | output | UART.CTRL.EN |
P1 | GPIO1 | uart_rx | input | UART.CTRL.EN |
P2 | GPIO2 | spi_sck | output | SPI.CTRL.EN |
P3 | GPIO3 | spi_mosi | output | SPI.CTRL.EN |
P4 | GPIO4 | spi_miso | input | SPI.CTRL.EN |
P5 | GPIO5 | spi_cs_n (the inverse of SPI.CTRL.CS) | output | SPI.CTRL.EN |
P6 to P15 | GPIO6 to GPIO15 | none: GPIO only | per GPIO.DIR | none |
NanoSoC has no JTAG, TEST_MODE or BOOT_MODE pins and no boot straps: boot is over SPI only, so there is no debug tap on any pad and no pad function other than the ones in the table. The shape of the table, one row per pad with reset function, enabled function and the owner bit, is the transferable part. The pad control logic sits in the always-on domain like the peripherals, so pad outputs keep their state in SLEEP.
// NanoSoC teaching values: the mux on one shared pad (P0 to P5).
// en is the owning peripheral's CTRL.EN bit; there is no select register.
// Outputs: the peripheral drives the pad when en = 1, the GPIO block otherwise.
module nano_pad_mux_cell (
input logic en, // UART.CTRL.EN or SPI.CTRL.EN
input logic per_is_out, // 1 for uart_tx, spi_sck, spi_mosi, spi_cs_n; 0 for the inputs
input logic per_out, // value the peripheral drives
input logic per_idle, // idle level seen by a peripheral input while en = 0
input logic gpio_dir, // GPIO.DIR bit, 1 = output
input logic gpio_out, // GPIO.DATA_OUT bit
input logic pad_in, // pad level after the pad cell
output logic pad_out,
output logic pad_oe,
output logic gpio_in, // GPIO.DATA_IN bit
output logic per_in // peripheral input (uart_rx, spi_miso), unsynchronised
);
assign pad_out = en ? per_out : gpio_out;
assign pad_oe = en ? per_is_out : gpio_dir;
// The GPIO block still reads the pad level (teaching choice: DATA_IN is honest).
assign gpio_in = pad_in;
// A disabled peripheral sees its idle level, never a live pad.
assign per_in = en ? pad_in : per_idle;
endmoduleThe script below encodes what each pad offers and a candidate table, then checks the rules that catch every conflict: no pad carries two peripheral functions, every function the board needs (the UART pair and the four SPI signals) is placed exactly once, and every pad is given only a function it offers. It also applies the EN-follows rule so you can see the owner of every pad in three situations. Then it proves it is awake by injecting a double placement, a missing SPI signal and an unoffered function.
# What each pad offers beyond GPIO (None = GPIO only), and who owns it.
OFFER = {"P0": "uart_tx", "P1": "uart_rx", "P2": "spi_sck",
"P3": "spi_mosi", "P4": "spi_miso", "P5": "spi_cs_n"}
OWNER = {"uart_tx": "UART", "uart_rx": "UART", "spi_sck": "SPI",
"spi_mosi": "SPI", "spi_cs_n": "SPI", "spi_miso": "SPI"}
REQUIRED = sorted(OWNER)
PLAN = {f"P{i}": OFFER.get(f"P{i}") for i in range(16)} # the NanoSoC table
def check(plan):
# plan maps a pad to a list of peripheral functions placed on it
for f in REQUIRED:
users = [p for p, fs in plan.items() if f in fs]
assert len(users) == 1, f"{f} placed {len(users)} times"
for p, fs in plan.items():
assert len(fs) <= 1, f"{p} carries {len(fs)} functions"
for f in fs:
assert OFFER.get(p) == f, f"{p} does not offer {f}"
def listed(plan):
return {p: ([f] if f else []) for p, f in plan.items()}
def owner_of(pad, en):
# the EN-follows rule: no register, only the peripheral's enable bit
f = OFFER.get(pad)
return f if f and en[OWNER[f]] else "GPIO"
check(listed(PLAN))
assert len(PLAN) == 16 and sum(1 for f in PLAN.values() if f) == 6
reset = {"UART": 0, "SPI": 0}
assert all(owner_of(p, reset) == "GPIO" for p in PLAN)
assert owner_of("P0", {"UART": 1, "SPI": 0}) == "uart_tx"
assert owner_of("P2", {"UART": 1, "SPI": 0}) == "GPIO" # SPI off: P2 stays a GPIO
assert owner_of("P5", {"UART": 0, "SPI": 1}) == "spi_cs_n"
assert owner_of("P9", {"UART": 1, "SPI": 1}) == "GPIO" # P6 to P15 never change
print("16 pads, 6 shared; every pad is GPIO at reset; the owner follows EN only")
bad = listed(PLAN); bad["P6"] = ["uart_tx"] # a second place for uart_tx
try:
check(bad); raise SystemExit("checker missed the double placement")
except AssertionError as e:
assert str(e) == "uart_tx placed 2 times", str(e); print(f"caught: {e}")
bad = listed(PLAN); bad["P4"] = [] # spi_miso has no pad
try:
check(bad); raise SystemExit("checker missed the missing function")
except AssertionError as e:
assert str(e) == "spi_miso placed 0 times", str(e); print(f"caught: {e}")
bad = listed(PLAN); bad["P3"] = ["spi_cs_n"]; bad["P5"] = [] # spi_cs_n on a pad that offers spi_mosi
try:
check(bad); raise SystemExit("checker missed the unoffered function")
except AssertionError as e:
assert "placed 0 times" in str(e) or "does not offer" in str(e); print(f"caught: {e}")
print("PASS: NanoSoC table conflict-free with six functions placed once; three injected faults caught")One pad database generates the RTL mux, the software header and the board netlist; the review quotes the database version.
No pad carries two peripheral functions, and a function that is absent from the table is a missing function, not a don't care: the checker output is attached.
The EN-follows rule is written once, in the programming guide, with the idle level of each shared pad; a design that adds a pad-control register has changed the register map and the review must say so.
A reader of the table can see that RESET_N and the crystal pads are dedicated, and that no pad is a JTAG, test or boot pin.
Generate all three consumers from the table and diff them against the checked-in files: any hand edit shows up as a diff. On the bench, enable each peripheral in turn and walk its pads with a scope: a pad that moves under two drivers, or a peripheral signal that does not appear on its pad, is a table error, not a board error. If the UART works and the SPI is dead, check that SPI.CTRL.EN is really 1 and that the board wiring follows P2 to P5, not a different pad order.
Neither block is wrong: the board netlist was built from the wrong pad table. NanoSoC has one pad plan, so the fix is the board netlist, regenerated from the pad database, not a patch to the RTL or a second pad plan; and a table generated from one source cannot disagree with the netlist.