At power-on, before the ROM executes its first fetch, every pin is already doing something: driving, floating or held by a pull. If the SPI chip-select floats low, the flash is selected from power-up and sees the first clock edges in the middle of a command; the boot ROM's image load then fails its checksum, in a chip that simulates perfectly because RTL simulation has no pad model. The reset state of every pin is therefore a deliverable, written before tapeout and checked by a script, and this lesson builds it for NanoSoC.
Each digital pad has a function, a pull, a drive and a receiver. NanoSoC has no pad-control register: the function of a shared pad follows the peripheral's own enable bit. If the UART's EN is 1, P0 and P1 belong to the UART; otherwise they are GPIO, with direction from the GPIO direction register. The pulls are fixed in the pad cell, not programmable, so they must be right for the state before firmware has run. The rule that makes the table easy to review is this: for every shared pad, the reset pull equals the idle level of the function the pad takes when enabled. UART TX idles high, so P0 has a pull-up. SPI chip-select is active low and inactive at 1, so P5 has a pull-up. SPI clock idles low in mode 0, so P2 has a pull-down, as do MOSI and MISO. All ten plain GPIO pads, P6 to P15, are inputs at reset with a pull-down, so nothing drives and no interrupt fires.
The drive is the default for every output: an illustrative 4 mA with medium slew. A stronger, fast drive is possible but halves the number of outputs a supply pair can tolerate switching together (the next topic), so the rule is default drive everywhere and a fast drive only with a measured timing reason. RESET_N is an input with a Schmitt trigger and a weak pull-up: an open pin means run, a slow board reset edge does not double-trigger, and the 16-cycle debounce in the reset controller then filters the rest.
Pad | Direction at reset | Pull (illustrative 50 kohm) | Open pin reads | Why |
|---|---|---|---|---|
P0 (uart_tx) | Input, driver off | Pull-up | 1 | TX idles high; the far end must not see a break while the UART is disabled. |
P1 (uart_rx) | Input | Pull-up | 1 | RX idles high; a pull-down would read as a continuous break. |
P2 (spi_sck) | Input, driver off | Pull-down | 0 | Mode 0 clock idles low. |
P3 (spi_mosi) | Input, driver off | Pull-down | 0 | Quiet default. |
P4 (spi_miso) | Input | Pull-down | 0 | A missing flash reads 0, never a floating level. |
P5 (spi_cs_n) | Input, driver off | Pull-up | 1 | Active low: floating or low would select the flash. |
P6 to P15 | Input (direction register 0) | Pull-down | 0 | Quiet default, no interrupt enabled. |
RESET_N | Input, Schmitt trigger | Pull-up | 1 (not in reset) | An open pin means run; the power-on reset still resets at power-up. |
XTAL_IN, XTAL_OUT | Analog | None | n/a | The oscillator runs whenever its supply is on. |
The table holds from the moment the power-on reset releases. Before that, while rails are still ramping and perhaps only the I/O supply is up, the core-side output enable is undefined, so the pad cell itself must keep its driver off. Library pad cells provide a power-on-control input for exactly this (its name varies by library), tied to a signal that says the core supply is valid. It is the mechanism that makes "nothing drives at reset" true, and the audit needs one row for it: every digital pad's power-on-control is connected, or the library states a safe default.
The module below is the teaching version of what sits behind one bidirectional GPIO pad: a function select, a registered output enable, and the power-on-control gate. The output enable is registered so that a control write does not decode combinationally onto the pin. The function select is not registered, so firmware must change it with the output disabled: clear the enable, change the select, then set the enable again.
// NanoSoC teaching values: one bidirectional pad slice.
// pad_oe is held off by the pad's power-on control until the core is valid.
module nano_pad_slice (
input logic clk,
input logic rst_n,
input logic core_pgood, // power-on control: core supply valid
input logic func_sel, // 0 = gpio path, 1 = peripheral path
input logic gpio_out,
input logic alt_out,
input logic oe_req, // requested output enable
input logic pad_in_raw, // from the pad receiver
output logic pad_out, // to the pad driver
output logic pad_oe, // to the pad driver
output logic gpio_in,
output logic alt_in
);
logic oe_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) oe_q <= 1'b0; // reset: input, never driving
else oe_q <= oe_req;
end
assign pad_out = func_sel ? alt_out : gpio_out;
assign pad_oe = oe_q & core_pgood;
assign gpio_in = pad_in_raw;
assign alt_in = pad_in_raw;
endmoduleThe script below holds the table as data, one row per pad (31 pads), and applies three rules through one function: no pad drives at reset; every shared pad's pull equals its idle level; RESET_N is an input with a pull-up and a Schmitt trigger. A fourth rule, the power-on-control audit, requires every digital pad to be listed as connected. The function is then run on three broken tables, each reproducing a real failure, and must reject all of them: a pull-down on the SPI chip-select, a pad that drives at reset, and a RESET_N without its pull-up.
IDLE = {"P0": 1, "P1": 1, "P2": 0, "P3": 0, "P4": 0, "P5": 1} # idle level of the shared function
ROWS = {}
for i in range(16):
ROWS["P%d" % i] = ("input", "up" if IDLE.get("P%d" % i, 0) else "down", False)
ROWS["RESET_N"] = ("input", "up", True) # direction, pull, Schmitt trigger
PON = set(ROWS) # pads whose power-on control is connected
def check(rows, pon):
assert len(rows) == 17, len(rows)
for pad, (direction, pull, schmitt) in rows.items():
assert direction != "output", pad + " drives at reset"
assert pull in ("up", "down"), pad + " floats"
if pad in IDLE:
assert pull == ("up" if IDLE[pad] else "down"), pad + ": pull differs from idle level"
assert rows["RESET_N"] == ("input", "up", True), "RESET_N needs a pull-up and a Schmitt trigger"
assert pon == set(rows), "a pad has no power-on control: " + str(set(rows) - pon)
check(ROWS, PON)
bad_cs = dict(ROWS); bad_cs["P5"] = ("input", "down", False)
bad_drive = dict(ROWS); bad_drive["P7"] = ("output", "down", False)
bad_rst = dict(ROWS); bad_rst["RESET_N"] = ("input", "down", True)
for rows, pon in ((bad_cs, PON), (bad_drive, PON), (bad_rst, PON), (ROWS, PON - {"P3"})):
try:
check(rows, pon)
raise SystemExit("a bad reset-state table was accepted")
except AssertionError:
pass
print("PASS: 17 digital pads checked; pull-down CS_N, driving pad, open RESET_N and missing power-on control all rejected")The reset-state table, signed, with one row per pad and no pending entries.
Every pad shown as an input or driver-off at reset, with the RTL reset value of its output enable quoted.
Each reset pull compared with the idle level of the function the pad takes, row by row.
The power-on-control connection of every digital pad, or the library's stated safe default.
Drive choices justified per pin against a timing or noise budget, not copied from a previous chip.
Check the table against the RTL reset values: the output-enable reset of every pad slice must be zero, as in the module above, and the check is a lint over reset assignments, not a visual skim. On the bench, a contention suspect is confirmed by measuring the pin voltage at power-on: a healthy undriven pin sits at its pull level, a fighting pin sits somewhere between the rails and runs warm. The SPI chip-select deserves its own scope trace during boot, because a wrong pull there looks like a flash or software problem.
Because each pull must equal the idle level of the function the pad takes. Chip-select is active low and inactive at 1, so a pull-up keeps the flash deselected; the mode 0 clock idles low, so a pull-down keeps the clock quiet. The same fixed rule gives the UART TX pad a pull-up.