RFC: a generic GPIO API as a device node and ioctl
RTEMS has GPIO support in two BSPs and no general interface. I would like to
propose one, with four prototype drivers written against it, and get the shape
reviewed before any of it is proposed for merge.
Where this came from
I have been bringing up the ESP32-C3 on RTEMS, with ESPHome running on top of
it. That is almost entirely a GPIO application – the switches, relays,
buttons, one-wire sensors and bit-banged buses that make up a configuration
all come down to configuring a pin and then reading or writing it. Without a
general interface that layer gets rewritten against whatever each BSP happens
to offer, which for most BSPs is nothing. With one, a new board or the next
ESP part is a controller driver, and the application above it does not change.
The design brief is not mine. After surveying every GPIO implementation in the
tree, Chris Johns set out what the interface should be:
BSP hardware support is an RTEMS driver implemented in the BSP and the BSP
exclusively controls the pin allocation and modes at the SOC level. The API
controls the GPIO settings only for configured pins. A BSP has a standard
RTEMS driver with a node in the IMFS and an ioctl handler to implement the
API functionality. If the BSP does not support a feature not supported is
returned. The API uses logical pin numbering that are continuous and the
BSP maps that to the hardware. BSP pins can be in banks, and sparse. … Add
physical and virtual pin characteristic. A BSP driver can implement virtual
IO. Provide a call to return the configuration details of a pin in terms of
the published API set of configuration options.
What follows implements that brief, virtual IO included.
Something to say up front
This was written with AI assistance, and it is the first API of that origin
the project will have to review. I would rather that be stated than
discovered. I have tried to make it reviewable on evidence rather than on
trust: the API is at 100% of coverable lines under tcgcov on a real QEMU run;
four independent drivers exist against it, which is the real test of whether
an abstraction fits; the documentation is complete in both manuals; and the
things that are not verified are listed below rather than left for a
reviewer to discover. Review it as hard as any other contribution.
The shape
A controller is a BSP driver registered as a device node, publishing
pin_count logical pins numbered 0…pin_count-1 with no holes; banks and
sparse hardware are the driver’s problem, not the caller’s. Thirteen
directives, each one ioctl():
fd = open("/dev/gpio0", O_RDWR);
rtems_gpio_pin_by_name(fd, "LED0", &pin); /* name the pad, not the number */
memset(&config, 0, sizeof(config));
config.direction = RTEMS_GPIO_DIRECTION_OUTPUT;
config.flags = RTEMS_GPIO_FLAG_ACTIVE_LOW; /* no hardware inverter needed */
config.drive_strength = 12000; /* microamps; reads back 20000 */
rtems_gpio_pin_configure(fd, pin, &config);
rtems_gpio_pin_set(fd, pin, 1); /* logical 1; the pad goes low */
The generic layer does the locking, the pin range checks and the capability
checks, so no driver writes them. A handler left NULL answers ENOTSUP,
so a controller that can only read and write pins is a legal driver.
This ground has been covered before
Worth saying plainly, because none of the choices below are new:
- Linux, December 2012. Alexandre Courbot’s RFC “gpiolib: introduce
descriptor-based GPIO interface”
replaced integer pin IDs with opaque descriptors, because the integer
namespace was fixed at system build time, needed a statically allocated
descriptor array, and required theARCH_NR_GPIOSmaximum. The modern
gpiod_*interface is what came of it. - RTEMS, 2014-2015. This is not the project’s first generic GPIO API.
bsps/include/bsp/gpio.handbsps/shared/dev/gpio/gpio-support.care
Andre Marques’s, from the Raspberry Pi GSoC work, and they already answer
“pins or ports?” with both: individual pins, plus an explicit
rtems_gpio_groupwithrtems_gpio_write_group()/
rtems_gpio_read_group()over artems_gpio_bsp_multi_read(bank, bitmask)
backend op. It is still in the tree and still used by two BSPs — see the
open questions. - Zephyr, April 2026. RFC: Fast GPIO API
Extension —
one descriptor is one port plus a pin mask, all masked pins change in a
single register write, and cross-port atomicity is explicitly not promised.
It is a latency extension for bit-bang drivers rather than a redesign;
Zephyr’s standard API already has the port-and-mask shape.
The one thing the 2014 RTEMS API does that this one does not is fix its size
at compile time: it #errors unless the BSP defines
BSP_GPIO_PIN_COUNT and BSP_GPIO_PINS_PER_BANK, and rejects
BSP_GPIO_PINS_PER_BANK > 32. That is the same build-time-maximum problem
Courbot’s RFC set out to remove, and it is why pin_count here is a runtime
property of a controller instance.
Trade study: Linux gpiolib, Zephyr, ESP-IDF, and this proposal
| Linux gpiolib | Zephyr | ESP-IDF | this proposal | |
|---|---|---|---|---|
| unit | gpio_chip, ngpio lines |
a port device, ≤32 pins |
flat gpio_num_t per SoC |
controller node, pin_count logical pins |
| access | /dev/gpiochipN + chardev v2 |
direct C calls | direct C calls | device node + ioctl() |
| pin numbering | 0…ngpio-1 per chip | 0…31 within a port | fixed per SoC | 0…pin_count-1 per controller |
| bulk | get_multiple/set_multiple, unsigned long* |
gpio_port_set_masked(), uint32_t |
pin_bit_mask in gpio_config_t, uint64_t |
caller-sized uint32_t bitmaps |
| active low | in gpiolib, above the driver | GPIO_ACTIVE_LOW, above the driver |
none | in the generic layer, above the driver |
| may block | gpio_chip.can_sleep |
no equivalent | n/a | rtems_gpio_drv_ctrl::can_block |
| ownership | line-request fd, per process | none | none | application’s, see below |
| units | debounce in µs |
flags; none documented | enum indexes | µA and µs |
| names | gpio-line-names |
devicetree | none | per-pin name, and owner |
ESP-IDF is the simplest of the four: one flat pin space per SoC, no
controller object, no device node, a 64-bit pin_bit_mask in gpio_config_t.
A good fit for one chip, and it does not carry over to the next one — which
is the problem here.
Zephyr is the closest match, and differs in one choice worth noting: its
gpio_port_pins_t is a uint32_t, so a port is at most 32 pins and a wider
controller is shown as several port devices. That is a fair answer — it makes
the bank the unit — but it hands the bank sums back to the caller, which the
brief above rules out for us. So: one controller with pin_count pins, and
caller-sized bitmaps across the whole of it.
Six things changed because of what Linux does.
- The fixed bulk maximum is gone.
RTEMS_GPIO_PIN_LIST_MAXwas 32 and
tied together three unrelated things — transaction size, pin choice, and
value word width. Now controller-relative bitmaps whose length the caller
supplies, which answers Chris’s “what if the BSP has 96 pins?”. uint32_t, notunsigned long. Linux usesunsigned longinternally
but its userspace ABI is fixed width, deliberately. This header is both
at once, andunsigned longis 32 bits on rv32 and 64 on aarch64.- Active low moved above the driver and
CAP_INVERTwas removed.
Requiring a hardware inversion register to support an active-low pin is
backwards — it is a property of how the board is wired. No driver now
contains any inversion at all. can_blockis a property, not a guess. It was being worked out from
physical-vs-virtual, which is a different question: a pad behind a slow bus
may block and a software pin may not.BIDIRECTIONALrenamed toCAP_OUTPUT_READBACK. Bidirectional means a
line that switches direction; this was an output whose read returns the pad
rather than the latch.- The atomicity claim was weakened to “done as one transaction”.
Linux’sset_multipledoes not promise the pins change at the same
instant either, Zephyr promises nothing at all about its port
operations, and a controller wider than one register cannot give it —
the Zynq driver is exactly that case.
Considered and not taken: line-request file descriptors. Linux gives you a
request fd from /dev/gpiochipN and operations go against that, so closing
it releases the lines. It exists because of process isolation. RTEMS is one
address space, so a descriptor is not a trust boundary, and the same calls are
made from BSP code holding no descriptor at all; any fd-keyed rule needs a
“no owner” escape that a caller can simply use. Per the brief, the BSP
alone controls allocation, and coordination between users is the
application’s. What the API does check is the board’s decision:
RTEMS_GPIO_PIN_RESERVED is refused with EACCES before the driver is asked
anything, and rtems_gpio_pin_info::owner reports what holds a pin. That is
now written down in the header rather than left looking like something I
missed.
Also not taken: gpio-line-names. The FDT drivers use child nodes
instead, per the requested binding shape; the array would be a second way to
say the same thing.
Where this goes further than either:
- Capability, board reservation and current owner are three separate
questions, not one.CAP_OUTPUTsays the silicon can;PIN_RESERVEDsays
the board says no, and is refused before the driver is asked anything;
ownersays who has it now. Neither Linux nor ESP-IDF separates the middle
one, and on a part where GPIO12…17 are the SPI flash it executes from,
driving one does not produce an error — it produces a board that stops
running. - Physical units — µA and µs — rather than hardware enum indexes, with the
driver rounding up and writing back what it actually selected. - A virtual controller as a first-class driver, so a clock gate or a
subsystem enable is driven through the same directives.
The drivers
Four, deliberately different from each other, because one driver proves
nothing about an abstraction:
| driver | what it exercises |
|---|---|
| ESP32-C3 | 22 pads in one register; flash and strapping pins; drive strength in µA mapped to four hardware settings |
| Zynq / ZynqMP | one driver for an ARM and an AArch64 BSP, pin layout read from the device tree; banks that are not word-aligned (bank 1 is 22 pins wide, so bank 2 starts mid-word); MIO vs EMIO differing in what a read means |
| TI OMAP4 / BeagleBone Black | four banks as four controllers; FDT ranges walked three levels; #interrupt-cells resolved from the interrupt parent |
| virtual | pins that are callbacks — a proxy, a clock gate, a subsystem enable, a software signal with interrupt delivery |
The two FDT drivers share one binding: stock vendor properties on the
controller node, rtems,-prefixed child nodes describing individual pins.
Documentation
Both manuals, as separate MRs against rtems-docs: c-user/gpio/, a
five-file manager chapter with thirteen directives in the rubric template,
modelled on c-user/regulator/; and bsp-howto/gpio.md, the driver-writer
half, in can.md’s shape. Both build with no new Sphinx warnings against
the manual’s baseline.
A question for the docs maintainers: c-user is largely generated from
rtems-central spec items. regulator and iodev are hand-written, so it has
been done by hand before, but if spec items are wanted instead I would rather
know now.
What is verified
cpukit/dev/gpio/gpio.c— 306/306 coverable lines, tcgcov over an
unmodified image onqemu-system-arm -M xilinx-zynq-a9- the virtual driver — 183/183
gpio01andgpio02both pass;gpio02’s seven negative controls are
recorded in its.doc, each mutation compiled and run
What is not verified, and this matters
No hardware. Not one of the four drivers has run on silicon. QEMU does not
model the Zynq PS GPIO registers at all — a write to DIRM_0 reads back zero
— and there is no am335x machine in QEMU. So the FDT parsing, the working out of
the pin layout and the API plumbing are tested; every register-level behaviour
in the three hardware drivers comes from a TRM and has never been executed.
There is also no AArch64 toolchain here, so the ZynqMP half of that driver is
checked by reading it, not by a build.
If anyone has a Zynq, a BeagleBone Black or a C3 on a desk, that is the most
useful thing anyone could contribute to this.
Open questions
- The existing API. Andre Marques’s
bsps/include/bsp/gpio.hand
bsps/shared/dev/gpio/gpio-support.care used by beagle and raspberrypi.
There is no symbol collision — that one isrtems_gpio_bsp_*and this is
rtems_gpio_pin_*/rtems_gpio_drv_*— so they can coexist. But two GPIO
APIs is not a good end state. Deprecate, port the two BSPs, or leave it?
Note the TI driver andbbb-gpio.ctouch the same registers and both ISRs
read-and-clear IRQSTATUS for the whole bank, so enabling both on one bank
is a real risk of losing interrupts. - Level-triggered re-entrancy. The block holds the status up while the
level lasts, so a handler that does not remove its cause is re-entered.
Linux masks the line and unmasks on ack. Both FDT drivers behave the same
way; it should be settled once in the API rather than per driver. gpio-line-names— worth supporting alongside the child-node form?
Branches
- RTEMS GPIO Driver API
- Untested example drivers
- API Documentation
Five, stacked on the API branch:gpio-api-ioctl(API, tests and Doxygen),
thengpio-esp32c3,gpio-zynq,gpio-beagleandgpio-virtual; plus two
branches againstrtems-docs.
Happy to reshape any of it. The API is the part worth arguing about; the
drivers are evidence that it fits, and are replaceable.