This lesson has one job: define what the pads do in the seconds before the first configuration write, and what they do while a peripheral is being turned on. At reset release the chip has opinions about sixteen pads and the board has opinions about the same nets, and if both drive, the symptom is physical: contention current, a failed boot or a damaged driver. The classic pad-direction bug is a bidirectional pad driving at reset while the board device also drives, and the fix is a reset-state table with input by default and defined pulls, enforced before any functional test runs.
Every GPIO pad resets to an input with its output driver off: GPIO.DIR resets to all zeros and DATA_OUT to zero, so even the first configured output starts by driving low. Every shared peripheral is disabled at reset, so every shared pad is a GPIO input. Each pad carries a weak fixed pull (illustratively 50 kilohm) chosen from the idle level of the function the pad takes when enabled. P0 uart_tx and P1 uart_rx pull up: UART lines idle high, so a pull-down on RX reads as a continuous break and a pull-down on TX shows the far end a break while the UART is disabled. P5 spi_cs_n pulls up, because chip select is active low and a pull-down would select the flash from power-up, so the flash would see the first clock edges in the middle of a command. P2 spi_sck pulls down because SPI mode 0 idles low, P3 spi_mosi pulls down as a quiet default, and P4 spi_miso pulls down so that a missing flash reads 0 and never floats. P6 to P15 pull down: quiet, and no interrupt because INT_EN resets to 0. RESET_N pulls up so that an open pin means run; the crystal pads have no pull. The rule the audit checks is one line: for every shared pad, the reset pull equals the idle level of the function the pad takes when enabled.
Because the select is the enable bit, the transition matters, and it is the one thing the pull rule buys. At reset the pad sits at its pull level. When a driver writes EN = 1, the mux hands the pad to the peripheral in the same cycle, and the peripheral drives its idle level, which is the level the pull already held: the wire does not move and the far end sees nothing. This is why the pulls are chosen from the idle levels, and it dictates the order of the driver. The first instruction a driver executes on a shared pad must not be a GPIO write: DIR for P0 to P5 stays 0 and DATA_OUT stays 0 until the peripheral is finished with. Configure the peripheral first (baud divider, mode, interrupt enables) with EN still 0, and write EN = 1 last, so the pad changes owner exactly once and with no level change. For SPI, CS resets to 0, so spi_cs_n is 1 at the moment of EN, matching the pull-up; setting CS = 1 is the deliberate act that drops the pad.
Glitches while EN toggles come in three forms. First, a GPIO driver left on the pad: if DIR bit 0 is 1 and DATA_OUT bit 0 is 0 when EN rises, P0 is driven low and then handed to a UART whose idle level is 1, so the far end sees a start edge. Second, clearing EN while the peripheral is active: the pad falls back to a GPIO input and the pull takes over, so a UART frame is cut short and then idles high, and spi_cs_n returns high through the pull-up and ends the transfer in whatever state the flash was in; stop the transfer first, then clear EN. Third, EN and the data written in the same access: for a peripheral that may start on EN, write the data after EN so the first bit is not lost. These are driver rules, and they exist because there is no pad-control register to hold the pad in a neutral state in between.
Direction discipline after reset has three further rules. Configure direction before enabling an interrupt on the pad, or an output driving its own level self-triggers, as the GPIO lesson showed. Never leave a bidirectional pad driven from both sides: the board schematic and the pad table are reviewed together, pad by pad, with one owner signing that each net has exactly one driver in every mode. And remember that NanoSoC has no boot straps and no test-mode pad: no pad is sampled as configuration, so the pulls exist for the board, not for a boot decision.
The script below encodes the reset-state table for all sixteen pads and audits the rules that prevent contention: every pad resets to an input, every pad has a defined pull, and the pull matches the idle level of the function the pad takes when enabled. It then models the EN rising edge, to show that a clean pad does not move and a pad with a leftover GPIO driver does, and proves it is awake by catching an output-at-reset, a pull-down on spi_cs_n, a pull-down on uart_tx and an EN glitch.
# Idle level of the function a shared pad takes when its peripheral is enabled.
FUNC = {"P0": ("uart_tx", 1), "P1": ("uart_rx", 1), "P2": ("spi_sck", 0),
"P3": ("spi_mosi", 0), "P4": ("spi_miso", None), "P5": ("spi_cs_n", 1)}
PULL = {"P0": "up", "P1": "up", "P2": "down", "P3": "down", "P4": "down", "P5": "up"}
def make_table():
t = []
for i in range(16):
pad = f"P{i}"
func, idle = FUNC.get(pad, ("GPIO", None))
pull = PULL.get(pad, "down")
t.append({"pad": pad, "func": func, "idle": idle, "reset_dir": "input", "pull": pull})
return t
LEVEL = {"up": 1, "down": 0}
def audit(table):
problems = []
for p in table:
if p["reset_dir"] != "input":
problems.append(f"{p['pad']} drives at reset")
if p["pull"] not in LEVEL:
problems.append(f"{p['pad']} floating: no pull")
elif p["idle"] is not None and LEVEL[p["pull"]] != p["idle"]:
problems.append(f"{p['pad']} pull {p['pull']} fights idle level of {p['func']}")
return problems
def enable_edge(p, dir_bit=0, data_out=0):
# level on an open pad just before EN rises, and just after the mux hands over
before = data_out if dir_bit else LEVEL[p["pull"]]
after = p["idle"] if p["idle"] is not None else before # MISO is an input: no change
return before, after
assert audit(make_table()) == []
t = make_table()
pulls = {p["pad"]: p["pull"] for p in t}
assert [q for q, v in pulls.items() if v == "up"] == ["P0", "P1", "P5"]
assert all(v == "down" for q, v in pulls.items() if q not in ("P0", "P1", "P5"))
for p in t:
b, a = enable_edge(p)
assert b == a, f"{p['pad']} moves when EN rises"
print("16 pads input at reset; P0, P1, P5 pull up, the other 13 pull down; EN rising moves no pad")
bad = make_table(); bad[3]["reset_dir"] = "output"
assert audit(bad) == ["P3 drives at reset"]
bad = make_table(); bad[5]["pull"] = "down" # selects the flash at reset
assert audit(bad) == ["P5 pull down fights idle level of spi_cs_n"]
bad = make_table(); bad[0]["pull"] = "down" # a break while UART.EN = 0
assert audit(bad) == ["P0 pull down fights idle level of uart_tx"]
b, a = enable_edge(t[0], dir_bit=1, data_out=0) # driver left DIR bit 0 high
assert (b, a) == (0, 1) # a falling-then-rising start edge
print("PASS: clean table passes; output-at-reset, spi_cs_n and uart_tx pull-downs and an EN glitch caught")The reset-state table lists direction, the function the pad takes when enabled and a pull that matches that function's idle level for every pad; no cell may say to be decided.
The board schematic review signs that each net has exactly one driver in every mode, with the table as the reference.
The programming guide says that EN is written last and that DIR and DATA_OUT stay at their reset values on P0 to P5 while the peripheral owns the pad.
The pull on each pad is fixed in the pad cell: the review confirms no software step is expected to set it.
On first silicon, probe every pad at reset release before loading software: inputs with pulls sit at their pull level, nothing drives. If a pad sits mid-rail, its pull is missing or fighting the board; if a pad drives, its direction default is wrong. Then enable each peripheral on a scope and watch the pad at the moment EN is written: a clean enable shows no edge at all. On NanoSoC, a flash that misbehaves from power-up points first at the spi_cs_n pull, and a flash that fails only when the driver sets EN points at DIR and DATA_OUT left over from a GPIO test. A chip that reboots at random after a quiet period points at the RESET_N pull-up.
P0 is driven low until EN rises, then the mux hands the pad to uart_tx, whose idle level is 1: the line rises, and the far end sees the low period as a break and the rise as the start of idle. The fix is in the driver, not the pad: clear DIR bit 0 and leave DATA_OUT at 0 before EN, so the pad sits at its pull level and does not move when EN rises.