Skip to content

parabun:gpio

import gpio from "parabun:gpio";

A small module wrapping the Linux GPIO character device interface (/dev/gpiochipN, uAPI v2). Same call shape across Raspberry Pi 4, Raspberry Pi 5 (where the new pinctrl-rp1 driver exposes the same uAPI), Jetson, and any other Linux SBC. No vendored libgpiod; pure ioctl on the kernel character device.

parabun:gpio is currently Linux-only. macOS / Windows hosts don’t have an equivalent kernel surface for SBC pin headers — those don’t ship without an external bridge.

Synchronously enumerates /dev/gpiochipN entries with their driver label and line count.

gpio.chips();
// [
// { path: "/dev/gpiochip0", label: "gpio-brcmstb@107d508500", lines: 32 },
// { path: "/dev/gpiochip4", label: "pinctrl-rp1", lines: 54 },
// ...
// ]

On a Pi 5 the user-accessible header pins live on /dev/gpiochip4 (the RP1). On a Pi 4 they’re on /dev/gpiochip0 (BCM2711). The label field is how you tell them apart.

Opens a chip and runs GPIO_GET_CHIPINFO_IOCTL to confirm it’s a real gpiochip. Returns a Chip.

await using chip = gpio.open("/dev/gpiochip4");
chip.path; // "/dev/gpiochip4"
chip.label; // "pinctrl-rp1"
chip.lines; // 54

Chip is AsyncDisposable — await using releases the chip fd at scope exit. Lines acquired through this chip stay open until they’re individually closed (you can drop the chip handle without affecting in-flight Line requests).

Acquire a single line on this chip. Returns a Line you can read() / write() / toggle() / edges().

// Output:
const led = chip.line(17, { mode: "out", initial: 0 });
led.write(1);
led.toggle();
led.value.get(); // 0 | 1 — Signal of the most recent observed value
// Input with hardware debounce + edge events:
const button = chip.line(27, {
mode: "in",
pull: "up",
debounceMs: 5,
edge: "falling",
});
for await (const e of button.edges()) {
// e.kind: "rising" | "falling"
// e.timestampNs: bigint (kernel monotonic)
// e.value: 0 | 1
}

Options:

  • mode: "in" | "out" — required.
  • pull: "up" | "down" | "off" — input bias resistor. Default "off".
  • debounceMs: number — hardware debounce. 0 disables. RPi 5 (RP1) supports this; RPi 4 (BCM2711) returns ENOTSUP at request time.
  • edge: "rising" | "falling" | "both" | "none" — edge events to deliver via edges(). Default "none".
  • initial: 0 | 1 — output starting value. Default 0.

Line is AsyncDisposable. The reactive line.value Signal updates on read() and on edge events — pair with @lyku/para-signals for effect { ... } / ~> composition.

Acquire several lines as one atomic unit. Up to 64 lines per call (kernel uAPI v2 cap). All lines share mode / pull / edge / debounceMs. Reads + writes go through a single ioctl, so multi-pin transitions hit the bus simultaneously.

await using bank = chip.bank([17, 22, 23, 27], {
mode: "out",
initial: 0b1010n, // BigInt: bit i = offsets[i]
});
bank.read(); // BigInt — bit i = current value of offsets[i]
bank.write(0b0101n); // all lines
bank.write(0b0001n, 0b0001n); // values + mask: only modify bit 0

Named bank rather than lines because chip.lines is already the chip’s line count.

The full IoT control loop in one effect block. chip.line({ pollHz: 50 }) updates button.value from the kernel; effect { ... } re-runs on every change and writes the LED — no manual setInterval, no callback wiring.

import gpio from "parabun:gpio";
import { effect } from "@lyku/para-signals";
await using chip = gpio.open("/dev/gpiochip4");
await using button = chip.line(27, {
mode: "in", pull: "up", debounceMs: 5, pollHz: 50,
});
await using led = chip.line(17, { mode: "out", initial: 0 });
effect(() => {
const pressed = button.value.get() === 0;
led.write(pressed ? 1 : 0);
});
await new Promise(() => {}); // keep alive

Measured on a Pi 5 RP1 (bench/parabun-gpio-toggle):

  • Single-line write() — ~2.1 M writes/s, ~470 ns per ioctl
  • Single-line toggle() — ~2.0 M toggles/s, ~490 ns per call
  • 4-line bank.write() — ~920 k writes/s = ~3.7 M pin-writes/s (1.1 µs covers 4 lines atomically)

uAPI v2 is ioctl-bound at ~1 MHz toggle rate per pin (two writes per toggle cycle). For sustained > 1 MHz toggle rates or DMA-driven PWM, see bun:mmio (off by default — bypasses the kernel and pokes the controller’s GPIO registers directly).