ticktrace
// cookbook · gpio

M3-A: GPIO / PADS

Full RP2350 GPIO subsystem coverage: 48 user GPIOs in IO_BANK0 + 6 QSPI pins in IO_QSPI, all atomic-aliased and per-pad configurable. This is the "complete" GPIO driver; it supersedes the v0.1 single-pin LED helpers, which remain as thin back-compat shims (gpio_led_init, gpio_led_toggle).

Files

Path Role
include/gpio.inc IO_BANK0 + IO_QSPI register/bit defs
include/pads.inc PADS_BANK0 + PADS_QSPI register/bit defs
src/gpio.S the driver (all public functions below)
examples/gpio_demo.S 3-LED bar pattern on GP22/23/24
examples/gpio_irq_demo.S GP0 falling edge → toggle LED
examples/gpio_test_fixture.S unit-test fixture: references every public symbol
tests/unicorn/test_gpio.py T1 trace tests for every public function
tests/unicorn/mocks_gpio.py helpers + AAPCS call wrapper
tests/renode/gpio.resc T3 integration test (gpio_demo.elf)
tests/renode/gpio_peripheral.py RP2350 IO_BANK0/PADS/SIO write decoder

Public API

All functions follow the AAPCS calling convention; r0..r3 are clobbered unless noted. pin is 0..47 for IO_BANK0 functions, 0..5 for the gpio_qspi_* mirror.

Configuration

gpio_init(pin)                  pad: ATOMIC_CLR ISO|OD; mux: SIO;
                                SIO: OUT=0, OE=0      (4 stores)
gpio_deinit(pin)                mux: NULL; pad: ATOMIC_SET ISO|OD
gpio_set_function(pin, func)    IO_BANK0 CTRL FUNCSEL = func
gpio_set_dir(pin, out_bool)     SIO_GPIO_OE_SET / OE_CLR
gpio_set_input_enabled(pin, en) PADS IE
gpio_set_drive_strength(pin, d) PADS DRIVE: 0=2mA 1=4mA 2=8mA 3=12mA
gpio_set_slew_fast(pin, fast)   PADS SLEWFAST
gpio_set_schmitt(pin, on)       PADS SCHMITT
gpio_pull_up(pin)               PADS PUE=1, PDE=0  (atomic SET+CLR)
gpio_pull_down(pin)             PADS PUE=0, PDE=1  (atomic CLR+SET)
gpio_disable_pulls(pin)         PADS PUE=0, PDE=0  (single CLR)

Drive / sense

gpio_put(pin, value)            single store to OUT_SET / OUT_CLR
gpio_toggle(pin)                single store to OUT_XOR
gpio_get(pin) -> r0             read OE, return bit n as 0/1

Interrupts (PROC0 line)

gpio_set_irq_enabled(pin, events_mask, enable)
                                events_mask in {LEVEL_LOW=1, LEVEL_HIGH=2,
                                                EDGE_LOW=4,  EDGE_HIGH=8}
                                writes via PROC0_INTE [pin/8] atomic SET/CLR
gpio_acknowledge_irq(pin, events_mask)
                                W1C on INTR[pin/8] for the matching nibble

IO_QSPI mirror (6 pins)

gpio_qspi_set_function(pin, func)
gpio_qspi_set_irq_enabled(pin, events_mask, enable)
gpio_qspi_acknowledge_irq(pin, events_mask)

v0.1 shims

gpio_led_init      preserved verbatim, still callable
gpio_led_toggle    preserved verbatim, still callable

Pin table

RP2350 has 48 user GPIOs (GP0..GP47) plus 6 QSPI pins. The function-mux maps each GPIO onto one of these per-pin alternate functions (datasheet rev 0.3 sec 9.1.1, table 597):

FUNCSEL Name Typical use
0 XIP flash QSPI
1 SPI SPI controller IO
2 UART PL011 UART TX/RX/CTS/RTS
3 I2C I²C SDA/SCL
4 PWM PWM channel A/B
5 SIO software-driven via SIO single-cycle IO
6 PIO0 PIO0 instruction stream
7 PIO1 PIO1
8 PIO2 PIO2 (RP2350 only)
9 GPCK clock-gen / HSTX
10 USB USB OVCUR / VBUS_DETECT / VBUS_EN
11 UART_AUX UART secondary lines
31 NULL function disconnected (default)

Pad register layout (PADS_BANK0 + PADS_QSPI)

Per-pad register at offset 4 + n*4:

Bit Field Reset Meaning
0 SLEWFAST 0 1 = fast slew
1 SCHMITT 1 1 = Schmitt trigger on input
2 PDE 0 pull-down enable
3 PUE 0 pull-up enable
5:4 DRIVE 1 00=2mA, 01=4mA, 10=8mA, 11=12mA
6 IE 0 input enable, must be 1 to read pin
7 OD 1 output disable
8 ISO 1 pad isolation

ISO + OD erratum note

Per datasheet sec 9.3.1, every pad on RP2350 boots with ISO=1 + OD=1. This is a deliberate ESD-/glitch-safety state on the new silicon; it differs from RP2040, where ISO does not exist. Code that drives a pin must clear those bits before the SIO output reaches the package, or the pin will read floating-input regardless of OE. gpio_init does this as its first store (PADS_BANK0 ATOMIC_CLR with value 0x180).

IRQ programming model

The IO_BANK0 IRQ subsystem packs 4 event bits per pin into one nibble; each 32-bit register holds 8 pins. For pin n:

register index = n / 8        (0..5)
nibble shift   = (n % 8) * 4
events_mask    = LEVEL_LOW | LEVEL_HIGH | EDGE_LOW | EDGE_HIGH (any subset)

Each "kind" lives in its own array of 6 registers:

Array Offset Semantics
INTR0..5 0x230 RAW status; W1C the EDGE bits
PROC0_INTE0..5 0x248 PROC0 enable mask
PROC0_INTF0..5 0x260 PROC0 force
PROC0_INTS0..5 0x278 PROC0 (raw & enable)
PROC1_INTE/INTF/INTS 0x290 same shape, for the second M33
DORMANT_WAKE_* 0x2D8 wakes the chip out of DORMANT

gpio_set_irq_enabled(pin, mask, en) writes (mask << ((pin%8)*4)) to PROC0_INTE0 + (pin/8)*4, choosing between ATOMIC_SET (en≠0) and ATOMIC_CLR (en=0). gpio_acknowledge_irq(pin, mask) writes the same shifted value to INTR0 + (pin/8)*4 on the plain alias; the W1C hardware then clears just the EDGE bits in that nibble. LEVEL events are not W1C; they self-clear when the line condition goes away.

To drive an actual NVIC line, the IO_BANK0 PROC0 IRQ output (IO_IRQ_BANK0, NVIC vector 13) needs to be enabled separately. M3-A intentionally does not program the NVIC (that's the SysTick/TIMER agent's slot); the gpio_irq_demo.S example polls INTS0 in firmware to demonstrate the event nibble shifting end-to-end.

Cycle counts

Measured on Cortex-M33 from SRAM, with the constant pool already loaded (no I-cache miss). Numbers ignore branch-target alignment overhead.

Function Stores Approx cycles Notes
gpio_put 1 4 mask + cmp + str
gpio_toggle 1 4 mask + cmp + str
gpio_get 0 6 ldr + lsrs + ands
gpio_set_function 1 8 shift + str
gpio_set_dir 1 9 cmp + mask + str
gpio_init 4 ~25 PAD CLR + mux + OUT_CLR + OE_CLR
gpio_deinit 2 ~14 mux NULL + PAD SET
gpio_pull_up 2 ~12 atomic SET PUE + CLR PDE
gpio_pull_down 2 ~12 atomic SET PDE + CLR PUE
gpio_disable_pulls 1 8 atomic CLR (PUE | PDE)
gpio_set_drive_strength 2 ~14 CLR DRIVE + SET new value
gpio_set_irq_enabled 1 ~22 shift + atomic SET/CLR
gpio_acknowledge_irq 1 ~16 shift + W1C

The hot path (gpio_put / gpio_toggle / gpio_get) is ≤ 6 cycles, suitable for bit-bang protocols up to ~25 MHz at clk_sys=150 MHz.

High-pin (≥32) handling

Pins 32..47 live in the SIO _HI register window, 0x40 bytes above the low window:

Low offset High offset Register
0x010 0x050 OUT
0x018 0x058 OUT_SET
0x020 0x060 OUT_CLR
0x028 0x068 OUT_XOR
0x030 0x070 OE
0x038 0x078 OE_SET
0x040 0x080 OE_CLR
0x048 0x088 OE_XOR
0x004 0x008 IN

The driver always uses pin - 32 as the bit position in the HI window (rather than pin & 31) to make the intent explicit. Cortex-M33 LSLS takes the full byte of the shift register, so 1 << 33 would be 0 silicon-correct; the explicit subtract is what makes pin 33 land at bit 1 in the HI register, as the datasheet specifies.

Testing

T1 (Unicorn) tests in tests/unicorn/test_gpio.py lock the exact MMIO write trace for every public function. T3 (Renode) loads build/gpio_demo.elf, runs 100 ms of simulated time, and asserts at least 8 SIO_GPIO_OUT_* events on the watched pins.

The v0.1 regression test (tests/unicorn/test_v01_blinky.py, unchanged from M2) continues to pass; gpio_led_init and gpio_led_toggle emit byte-identical write traces.