Skip to content

Pad and mode switch

The Super IO has six discrete switches (up, down, left, right, OK, back) and a three-position slide switch labelled Power OFF / Power ON / GPS ON.

The pad

Six buttons, all active low with the chip's internal pull-ups: up P0.21, down P0.17, left P1.05, right P0.16, OK P0.10, back P0.15.

The driver is split on the same line as everything else in the project. oxinode_core::pad is the state machine: it is told a switch's level and the time, and says which gestures are due and when it next needs to be asked. Debounce, auto-repeat, which switches repeat, and the bounce statistics are all there and tested with a clock that is a number. oxinode::pad is the driver: it claims the six pins, sleeps on the GPIO PORT interrupt until any of them changes or the core's deadline arrives, re-reads all six, feeds the core, and puts gestures on a bounded channel the modem loop drains once per iteration.

Why it is a pad and not a trackball

Meshtastic's variant declares HAS_TRACKBALL and reuses its trackball driver for the pad. A trackball emits a burst of edges as the ball rolls and is read by counting them. A pad emits one edge and then a level that lasts as long as the finger does. Counting edges from a pad gives one step per press and no repeat; watching levels on a ball gives a runaway cursor. So the core is about levels and time, never about edges: an edge is only the moment the firmware learns a level changed, and it re-reads every pin on every wake regardless.

The numbers

constant value why
DEBOUNCE_MS 10 a level must hold this long to be believed
REPEAT_DELAY_MS 400 a held direction repeats after this
REPEAT_PERIOD_MS 125 and then every this: eight lines a second

Debounce is "stable for 10 ms", not "ignore for 10 ms". Every edge restarts the timer and the change commits only once the level has held, so the number of bounces is irrelevant and only their duration matters. The alternative, accepting the first edge and ignoring the line, commits to whatever the first edge said, and on a switch that bounces on release that is a phantom press. Ten milliseconds is twice what tactile domes are specified to bounce for; the driver measures how long every committed press took to settle and how many edges it produced and logs it, so the figure can be checked against a worn switch rather than guessed. A compile-time assertion keeps the three numbers ordered.

Repeat only on the four directions. A held direction scrolls. A held OK is one OK, because an action chosen once is chosen once, and a held back is one back. No chording, no long-press, no double-press: six switches, six gestures, one meaning each.

A late poll gets one repeat, not a burst. The next repeat is scheduled from the poll that emitted the last one, so if the render loop stalls for a second the user gets one step, not eight. The channel holds eight gestures and drops the newest when full, counting and logging the drops.

Interrupt-driven, and quiet

embassy-nrf sets each pin's SENSE to the level it is not at and the chip raises one PORT interrupt when it gets there. While nothing is pressed no timer runs and nothing polls. The obvious version has a race (read the pin, then arm the wait, and an edge in between is lost until the next one); the driver closes it by arming each pin to wait for the level it is not at as of the arm, which returns immediately if the pin has already moved.

P0.10 is an NFC pin

On this part P0.09 and P0.10 belong to the NFC peripheral until the PROTECT bit of UICR.NFCPINS is cleared, and that is a word in the chip's user information page, not a register. The failure mode is indistinguishable from a dead OK button.

embassy-nrf does not name P0.09 or P0.10 at all without the nfc-pins-as-gpio feature, so the feature is on, and it is not only a name. With it, embassy_nrf::init reads UICR.NFCPINS on every boot; if bit 0 is already clear it does nothing; otherwise it writes the word back with bit 0 cleared as a single masked word write and resets once so it takes. Flash bits go from 1 to 0 without an erase, embassy-nrf refuses rather than erasing if asked for a 0→1 change, and REGOUT0 is a different word this write does not touch. So the thing to be afraid of (a page erase that loses REGOUT0 and drops the board to 1.8 V with the panel's boost converter and the QSPI flash expecting 3.3) cannot happen down this path.

On a board that shipped running Meshtastic the bit is already clear, because Meshtastic builds with CONFIG_NFCT_PINS_AS_GPIOS. The product image logs both words at every boot so the answer can be read before anything is flashed:

board: nav pad usable=true (UICR.NFCPINS), regulator=3.3 V

The mode switch

P1.09 and P0.12 carry the three-position switch. Both lines are read with no pull, as Meshtastic reads them, because an internal pull fighting an unknown external one could read the same in every position.

position P1.09 P0.12 log
Power ON high low board: mode switch P1.09=high P0.12=low -> power on
GPS ON low high board: mode switch P1.09=low P0.12=high -> gps on
Power OFF the board is unpowered; nothing runs to read anything

Each line is driven high in its own position, the opposite polarity to the six switches beside it, and the third position is not a mode: it cuts the board's power, USB included. "Neither high" is mid-travel, or a board with no Super IO on it, where both unpulled lines float; Mode::decode in the core names it and the two readings above are its test vectors.

The only thing the firmware does with the position is decide whether the GPS receiver is powered. See GPS.