Back From The Dead? - Bringing sysfs Back To Life
Legacy sysfs GPIO on Debian Trixie : Survival Guide for CM3+/4/4S
The Linux sysfs GPIO interface (/sys/class/gpio) has been deprecated upstream for several kernel cycles, but on Debian Trixie it is still present for BCM2835 and BCM2711 based devices. If you have existing scripts or systemd services that rely on sysfs GPIO, this article explains how to check whether it is available on your system, what has changed, and how to adapt your scripts to work reliably on Trixie's newer kernels.
Scope and Limitations
- This article applies to Raspberry Pi CM3+ (BCM2835), CM4, and CM4S (BCM2711) only.
- BCM2712-based devices (Pi 5, CM5) have removed sysfs GPIO entirely — this guide does not apply to them.
- sysfs GPIO is deprecated. Future kernel releases may remove it without notice. Treat this as a bridge, not a destination. For new designs, consider
libgpiodor thepinctrlutility.
Step 1: Check Whether sysfs GPIO Is Available
The quickest test is simply to look for the interface:
ls /sys/class/gpio/
If the directory exists and contains gpiochipN entries, sysfs GPIO is compiled into your running kernel. If the directory is missing entirely, the kernel was built without CONFIG_GPIO_SYSFS and you will need to use an alternative such as libgpiod or pinctrl.
You can verify the kernel config option directly:
# Check the running kernel config (if available)
zcat /proc/config.gz 2>/dev/null | grep GPIO_SYSFS
# Or check the config file shipped with the kernel
grep GPIO_SYSFS /boot/config-$(uname -r) 2>/dev/null
You are looking for CONFIG_GPIO_SYSFS=y. If it shows # CONFIG_GPIO_SYSFS is not set or is absent, sysfs GPIO is not available.
Step 2: Understand the Numbering Change
This is the critical difference that breaks most legacy scripts on Trixie.
On older kernels (Bullseye, Bookworm with legacy numbering), the BCM pinctrl GPIO chip was always assigned base 0. So BCM GPIO 18 mapped directly to /sys/class/gpio/gpio18. Scripts could hardcode GPIO numbers and everything just worked.
On Trixie (kernel 6.12+), GPIO chip numbering is dynamically allocated. The BCM pinctrl chip typically lands at base 512 or higher, depending on what other GPIO expanders are present on the device tree. BCM GPIO 18 might now be /sys/class/gpio/gpio530 (512 + 18).
Any script that hardcodes a GPIO number like echo 18 > /sys/class/gpio/export will either fail, or worse, configure the wrong GPIO on a completely different chip.
Step 3: Find the Correct Base Offset
Each GPIO chip registered in sysfs has a label and a base number. To find the BCM pinctrl chip:
# List all GPIO chips and their labels
for chip in /sys/class/gpio/gpiochip*; do
echo "$(basename $chip): label=$(cat $chip/label) base=$(cat $chip/base) ngpio=$(cat $chip/ngpio)"
done
On a CM3+ you will see a chip labelled pinctrl-bcm2835. On a CM4 or CM4S it will be labelled pinctrl-bcm2711. The base value is the offset you need to add to all BCM GPIO numbers.
Example output from a CM4 on Trixie:
gpiochip512: label=pinctrl-bcm2711 base=512 ngpio=58
gpiochip504: label=brcmexp-gpio base=504 ngpio=8
In this case, BCM GPIO 39 maps to Linux GPIO 551 (512 + 39).
Step 4: Adapt Your Scripts
The fix is straightforward: auto-detect the base offset at runtime instead of hardcoding GPIO numbers. Here is a reusable pattern:
#!/bin/bash
set -euo pipefail
# Auto-detect GPIO base offset from the BCM pinctrl chip
GPIO_BASE=""
for chip in /sys/class/gpio/gpiochip*; do
label=$(cat "${chip}/label" 2>/dev/null || true)
case "${label}" in
pinctrl-bcm2835|pinctrl-bcm2711)
GPIO_BASE=$(cat "${chip}/base")
break
;;
esac
done
if [ -z "${GPIO_BASE}" ]; then
echo "ERROR: Could not find BCM pinctrl gpiochip"
exit 1
fi
# Now use GPIO_BASE + BCM pin number everywhere
LINUX_GPIO=$((GPIO_BASE + 39))
# Export and configure
echo "${LINUX_GPIO}" > /sys/class/gpio/export
echo 1 > /sys/class/gpio/gpio${LINUX_GPIO}/active_low
echo high > /sys/class/gpio/gpio${LINUX_GPIO}/direction
# Optional: create a friendly /dev/ symlink
ln -sf /sys/class/gpio/gpio${LINUX_GPIO}/value /dev/led2-red
This pattern works identically on both old kernels (where the base is 0) and Trixie (where the base is 512+), so it is safe to deploy across mixed fleets.
Step 5: Template for Multiple GPIOs
For carrier boards with several GPIOs to configure, a template approach keeps things maintainable. Define your GPIOs in a configuration block and loop through them:
#!/bin/bash
set -euo pipefail
# --- Auto-detect base (same as above) ---
GPIO_BASE=""
for chip in /sys/class/gpio/gpiochip*; do
label=$(cat "${chip}/label" 2>/dev/null || true)
case "${label}" in
pinctrl-bcm2835|pinctrl-bcm2711)
GPIO_BASE=$(cat "${chip}/base")
break
;;
esac
done
[ -z "${GPIO_BASE}" ] && { echo "No BCM gpiochip found"; exit 1; }
# --- GPIO definitions: BCM_PIN|DIRECTION|ACTIVE_LOW|SYMLINK_NAME ---
GPIOS=(
"39|high|1|led2-red"
"40|high|1|led2-green"
"41|high|1|led2-blue"
"17|in|0|button1"
)
for entry in "${GPIOS[@]}"; do
IFS="|" read -r pin dir act_low name <<< "${entry}"
gpio=$((GPIO_BASE + pin))
gpio_path="/sys/class/gpio/gpio${gpio}"
# Export if needed
[ -d "${gpio_path}" ] || echo "${gpio}" > /sys/class/gpio/export
sleep 0.1
# Configure
echo "${act_low}" > "${gpio_path}/active_low"
echo "${dir}" > "${gpio_path}/direction"
# Symlink
[ -n "${name}" ] && ln -sf "${gpio_path}/value" "/dev/${name}"
echo "${name}: BCM ${pin} -> gpio${gpio} [${dir}, active_low=${act_low}]"
done
Common Pitfalls
- Hardcoded GPIO numbers: The single most common breakage on Trixie. Any script that does
echo 18 > /sys/class/gpio/exportwithout adding the base offset will fail or misconfigure hardware. Always detect the base at runtime. - Race condition on export: After writing to
/sys/class/gpio/export, the sysfs attributes may not be immediately visible. A briefsleep 0.1or a retry loop avoids intermittent failures, particularly on slower CM3+ devices. - active_low vs direction ordering: Set
active_lowbeforedirectionwhen usinghighorlowas the direction value. The initial output level is applied at the moment direction is written, so the active_low inversion must already be in place. - Permissions: sysfs GPIO export and configuration requires root. If your application runs unprivileged, export and configure in a systemd oneshot service at boot, then grant access to the
/dev/symlinks via udev rules.
Systemd Integration
Run your GPIO setup script as a oneshot service so it executes once at boot before your application starts:
[Unit]
Description=Configure carrier board GPIOs (sysfs)
DefaultDependencies=no
After=sys-subsystem-gpio.target
Before=multi-user.target
[Service]
Type=oneshot
ExecStart=/opt/carrier-board/gpio/setup-gpios.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Future-Proofing: When sysfs Goes Away
sysfs GPIO has been marked deprecated in the kernel since version 4.8 (2016). It has survived this long because of widespread use in embedded deployments, but it will eventually be removed. Some signs that removal is approaching:
- BCM2712 (Pi 5 / CM5) kernels have already dropped it.
- Kernel maintainers have explicitly stated that
CONFIG_GPIO_SYSFSwill not be carried forward indefinitely. - New Raspberry Pi OS images are defaulting to libgpiod tooling.
When that happens, your options are:
- libgpiod: The official kernel-supported replacement. Provides
gpioget,gpioset,gpiomoncommand-line tools and C/Python libraries. Recommended for production use. - pinctrl: Raspberry Pi's own utility for direct hardware access. Excellent for debugging and development. See our companion article on pinctrl for details.
- Device tree overlays: For dedicated functions like LEDs, use the
gpio-ledsoverlay to let the kernel manage them natively via/sys/class/leds/.
For carrier boards that need to support both current Trixie deployments (sysfs) and future kernels (libgpiod), consider wrapping your GPIO access behind a thin abstraction layer that detects the available interface at runtime.
Summary
- sysfs GPIO still works on Debian Trixie for CM3+, CM4, and CM4S — but the GPIO numbering has changed.
- Always auto-detect the BCM pinctrl chip base offset; never hardcode GPIO numbers.
- This is a bridge solution. Future kernels will remove sysfs GPIO entirely.
- For new designs and long-term support, migrate to libgpiod or pinctrl.