$linuxjunkies
>

Use GPIO Pins on a Raspberry Pi

Control Raspberry Pi GPIO pins using libgpiod — the modern replacement for deprecated sysfs. Covers CLI tools, Python, and C with real working examples.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • Raspberry Pi 3B+, 4, or 5 with a 64-bit OS installed and network/SSH access
  • Basic electronics: LED, resistors, jumper wires, breadboard
  • GCC toolchain installed (build-essential on Debian/Ubuntu) for the C example
  • User account with sudo privileges to install packages and modify group membership

GPIO (General Purpose Input/Output) pins turn a Raspberry Pi into a bridge between software and physical electronics. The old /sys/class/gpio sysfs interface is deprecated since Linux 4.8 and removed from many modern kernels. The current standard is libgpiod — a character device-based API with C bindings, Python bindings, and a suite of command-line tools. This guide covers installing libgpiod, using its CLI tools, and writing minimal Python and C programs that drive GPIO pins correctly on modern Raspberry Pi OS (Bookworm) and Ubuntu 24.04 for Pi.

Why sysfs GPIO Is Gone

The sysfs interface exported pins through /sys/class/gpio/export and friends. It had no access control, no concept of chip ownership, and was impossible to use safely from multiple processes. The kernel moved to a character device model (/dev/gpiochip0, /dev/gpiochip1, …) where each pin request is an explicit, owned file descriptor. Tools and libraries that still use sysfs will silently fail or produce warnings on kernels ≥ 6.x. Do not use RPi.GPIO or WiringPi for new projects; both depend on the deprecated interface.

Prerequisites and Hardware Notes

  • Raspberry Pi 3B+, 4, or 5 running Raspberry Pi OS Bookworm (64-bit recommended) or Ubuntu 24.04 LTS for Pi.
  • An LED with a 330 Ω resistor (for output example) or a push-button with a 10 kΩ pull-down resistor (for input example).
  • SSH access or a local terminal. Running as a normal user who is a member of the gpio group (see below).
  • Basic familiarity with a text editor and the C compiler toolchain for the C example.

Step 1: Install libgpiod and Tooling

Raspberry Pi OS Bookworm and Ubuntu 24.04 both carry libgpiod 2.x in their main repositories.

# Raspberry Pi OS / Debian / Ubuntu
sudo apt update
sudo apt install -y gpiod libgpiod-dev python3-gpiod
# Fedora / RHEL family (if running on a Pi with Fedora ARM)
sudo dnf install -y libgpiod libgpiod-devel python3-libgpiod
# Arch Linux ARM
sudo pacman -S libgpiod python-libgpiod

The package installs the gpiodetect, gpioinfo, gpioget, gpioset, and gpiomon CLI tools, plus headers and the Python module.

Step 2: Grant User Access to GPIO

The character devices are owned by the gpio group on Raspberry Pi OS. Add your user, then log out and back in (or use newgrp) for the change to take effect.

sudo usermod -aG gpio $USER
newgrp gpio

On Ubuntu for Pi the group may be dialout or require a udev rule. Check with ls -l /dev/gpiochip* and add your user to whichever group owns those devices.

Step 3: Discover Your GPIO Chips and Lines

gpiodetect

Output will look something like:

gpiochip0 [pinctrl-bcm2835] (54 lines)
gpiochip1 [raspberrypi-exp-gpio] (8 lines)

The main header pins live on gpiochip0. List all lines with their current direction and any consumer holding them:

gpioinfo gpiochip0

Pin numbering uses the BCM (Broadcom) GPIO number, not the physical header position. GPIO 17 is physical pin 11; GPIO 27 is physical pin 13. Keep a pinout reference like pinout.xyz open while working.

Step 4: Drive a Pin from the Command Line

Read an input pin

# Read GPIO 17 on gpiochip0 (returns 0 or 1)
gpioget --chip gpiochip0 17

Set an output pin

# Drive GPIO 17 high, hold it for 5 seconds, then release
gpioset --mode=time --sec=5 gpiochip0 17=1

Without --mode=time, gpioset in libgpiod 2.x defaults to holding the line until the process exits, which is the correct behavior — the line is released when the owning process dies. In libgpiod 1.x you may need --mode=signal instead; check gpioset --help for your installed version.

Monitor a pin for edge events

# Print a timestamp each time GPIO 17 changes (Ctrl-C to stop)
gpiomon --num-events=10 --rising-edge --falling-edge gpiochip0 17

Wire an LED through a 330 Ω resistor from GPIO 17 (BCM) to ground. The gpiod Python module (part of python3-gpiod) uses a context manager that guarantees the line is released when the block exits.

cat > blink.py << 'EOF'
import gpiod
import time

CHIP = "/dev/gpiochip0"
LINE = 17

with gpiod.request_lines(
    CHIP,
    consumer="blink-example",
    config={LINE: gpiod.LineSettings(direction=gpiod.line.Direction.OUTPUT)},
) as request:
    for _ in range(10):
        request.set_value(LINE, gpiod.line.Value.ACTIVE)
        time.sleep(0.5)
        request.set_value(LINE, gpiod.line.Value.INACTIVE)
        time.sleep(0.5)

print("Done. Line released.")
EOF
python3 blink.py

Note: The gpiod.request_lines() API is the libgpiod 2.x Python interface. If your system has libgpiod 1.x (Ubuntu 22.04, Debian Bullseye), use the older gpiod.Chip / chip.get_line() API instead and consult the 1.x documentation.

Step 6: C Example — Read a Button

Wire a push-button between GPIO 27 and 3.3 V with a 10 kΩ pull-down resistor to ground. The C API is the most direct and lowest-overhead way to use libgpiod.

cat > button.c << 'EOF'
#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>

#define CHIP  "/dev/gpiochip0"
#define LINE  27

int main(void) {
    struct gpiod_chip *chip = gpiod_chip_open(CHIP);
    if (!chip) { perror("gpiod_chip_open"); return 1; }

    struct gpiod_line_request *req;
    struct gpiod_request_config *rcfg = gpiod_request_config_new();
    struct gpiod_line_config  *lcfg  = gpiod_line_config_new();
    struct gpiod_line_settings *ls   = gpiod_line_settings_new();

    gpiod_line_settings_set_direction(ls, GPIOD_LINE_DIRECTION_INPUT);
    gpiod_line_settings_set_bias(ls, GPIOD_LINE_BIAS_PULL_DOWN);
    gpiod_line_config_add_line_settings(lcfg, (unsigned int[]){LINE}, 1, ls);
    gpiod_request_config_set_consumer(rcfg, "button-read");

    req = gpiod_chip_request_lines(chip, rcfg, lcfg);
    if (!req) { perror("request_lines"); return 1; }

    for (int i = 0; i < 20; i++) {
        enum gpiod_line_value val = gpiod_line_request_get_value(req, LINE);
        printf("GPIO %d = %s\n", LINE, val == GPIOD_LINE_VALUE_ACTIVE ? "HIGH" : "LOW");
        sleep(1);
    }

    gpiod_line_request_release(req);
    gpiod_line_settings_free(ls);
    gpiod_line_config_free(lcfg);
    gpiod_request_config_free(rcfg);
    gpiod_chip_close(chip);
    return 0;
}
EOF
gcc -o button button.c $(pkg-config --cflags --libs libgpiod)
./button

The kernel-side pull-down set via GPIOD_LINE_BIAS_PULL_DOWN eliminates the need for an external resistor on Pi 4 and Pi 5 (BCM2711/BCM2712 both support software bias). On Pi 3, software bias is not available; use the physical resistor.

Step 7: Verify Everything Is Working

# Confirm the line is claimed while your program runs in another terminal
gpioinfo gpiochip0 | grep -E "line 17|line 27"

A correctly claimed line shows the consumer name you set (e.g., blink-example) and its direction. If it shows unused, the line was never claimed or was already released.

Troubleshooting

  • Permission denied on /dev/gpiochipN: Your user is not in the correct group. Re-run groups to confirm membership took effect. You may need to fully log out and log back in rather than just using newgrp.
  • "Device or resource busy": Another process owns the line. Use gpioinfo to find the consumer name, then identify and stop that process.
  • gpioset releases the pin immediately: You are using libgpiod 2.x which changed the default --mode. Pass --mode=time --sec=N or --mode=signal explicitly.
  • Python ImportError for gpiod: The package name is python3-gpiod (system) or install via pip install gpiod inside a virtual environment. The PyPI package tracks the libgpiod 2.x API.
  • C compile error — gpiod.h not found: Install libgpiod-dev (Debian/Ubuntu) or libgpiod-devel (Fedora/RHEL). Confirm with pkg-config --modversion libgpiod.
tested on:Debian 12 (Bookworm) / Raspberry Pi OSUbuntu 24.04 LTS (Pi)Arch Arch Linux ARM (rolling)

Frequently asked questions

Can I still use RPi.GPIO or WiringPi?
Avoid them for new projects. Both rely on the deprecated sysfs interface or direct memory mapping that conflicts with the character device model. RPi.GPIO is unmaintained; WiringPi's author discontinued it. Use libgpiod instead.
What is the difference between gpiochip0 and gpiochip1 on the Pi?
On Pi 3 and 4, gpiochip0 is the main BCM283x/BCM2711 controller covering all 40-pin header GPIOs. gpiochip1 is a secondary expander (the RP1 chip on Pi 5 restructures this significantly — gpiochip0 through gpiochip4 exist, with the 40-pin header on gpiochip4).
How do I use PWM with libgpiod?
libgpiod does not provide hardware PWM; use the kernel's PWM subsystem via /sys/class/pwm for hardware PWM channels, or a userspace PWM library. GPIO 18 (BCM) is the main hardware PWM pin on the Pi.
Is it safe to set both input and output on the same pin from two processes?
No. The character device model enforces exclusive ownership — the second request will fail with EBUSY. Design your application so one process owns each line, or use inter-process communication to share control.
Does this work on Raspberry Pi 5?
Yes, but the Pi 5 uses an RP1 south-bridge chip that exposes GPIO through gpiochip4, not gpiochip0. Run gpiodetect to find the right chip number, then target it in your code and CLI commands.

Related guides