The Shift to libgpiod v2 on Raspberry Pi 5

If you are using C on Raspberry Pi hardware in 2026, you have likely noticed that the old tutorials no longer work. The beloved WiringPi library has been deprecated for years, and the original sysfs GPIO interface (/sys/class/gpio) was officially removed from the Linux kernel. Furthermore, the Raspberry Pi 5 introduced a completely new southbridge architecture (the RP1 chip), which changed how the OS enumerates GPIO devices.

Python developers have largely migrated to gpiozero or lgpio, but for embedded C developers requiring microsecond timing, low memory overhead, or integration with existing C/C++ codebases, the upstream Linux standard is libgpiod (specifically the v2 API). This guide targets the Raspberry Pi 5 (8GB variant, Part: SC1142) running Raspberry Pi OS (64-bit, Bookworm or newer), walking you through the exact hardware differences, physical wiring, and a fully compilable C program using the modern libgpiod v2 character device API.

Hardware Architecture and Pin Mapping

Before writing a single line of C, you must understand the hardware abstraction layer. The most common point of failure for developers migrating from Pi 4 to Pi 5 is assuming the GPIO chip device path remains the same. It does not.

Table 1: GPIO Architecture & libgpiod Mapping (Pi 4 vs Pi 5)
Feature Raspberry Pi 4 Model B Raspberry Pi 5 (8GB)
GPIO Controller IC BCM2711 (Integrated SoC) RP1 (Dedicated Southbridge)
Device Path /dev/gpiochip0 /dev/gpiochip4
Pull-up/down Mechanism BCM SoC internal registers RP1 internal registers (via libgpiod bias settings)
Max GPIO Toggle Speed (C) ~2.5 MHz (sysfs/mmap) ~1.8 MHz (libgpiod chardev overhead)
Recommended C Library libgpiod v1/v2 or pigpio libgpiod v2 (Strictly required for RP1)

Note: The toggle speed drop on the Pi 5 via the character device API is due to the PCIe link latency between the BCM2712 host processor and the RP1 southbridge. If you need >5 MHz toggling, you must use the RP1 PIO (Programmable IO) state machines, not standard GPIO.

Project Pin Mapping

For this build, we are creating an interactive circuit: an LED driven by a GPIO output, and a tactile button read via a GPIO input with an internal pull-up.

Table 2: Physical Wiring Map
BCM GPIO Physical Pin (40-pin Header) Function Connected To
21 40 Output (LED) 330Ω Resistor → LED Anode
16 36 Input (Button) Tactile Switch (Normally Open)
GND 39 Ground Reference LED Cathode & Switch Leg 2
3.3V 1 Power (Optional) Not used (using internal pull-up)

Parts List and Physical Wiring

Safety & Hardware Warning: Never connect 5V logic directly to Raspberry Pi 5 GPIO pins. The Pi 5 GPIO bank is strictly 3.3V tolerant. Applying 5V will permanently destroy the RP1 southbridge chip.

Required Components:

  • Board: Raspberry Pi 5 (8GB) [SC1142] with active cooling (Active Cooler or Argon case).
  • OS: Raspberry Pi OS (64-bit) Bookworm or Trixie.
  • LED: Standard 5mm through-hole LED (Red or Green, 2V forward voltage).
  • Resistor: 330Ω 1/4W metal film (for current limiting: (3.3V - 2V) / 20mA ≈ 65Ω, 330Ω is safe and dimmer).
  • Switch: 6x6mm tactile pushbutton.
  • Wiring: 22 AWG solid core jumper wires, half-size breadboard.

Wiring Steps:

  1. Insert the tactile switch across the breadboard's center trench.
  2. Connect a jumper from Physical Pin 36 (BCM 16) to one leg of the switch.
  3. Connect a jumper from Physical Pin 39 (GND) to the opposite leg of the switch.
  4. Insert the LED. Connect the 330Ω resistor to the Anode (long leg).
  5. Connect a jumper from Physical Pin 40 (BCM 21) to the free end of the resistor.
  6. Connect a jumper from Physical Pin 39 (GND) to the LED Cathode (short leg).

Compilable C Code: LED and Button Polling

Most online C tutorials for Raspberry Pi still use the deprecated libgpiod v1 API (gpiod_chip_get_line), which will fail to compile on modern Raspberry Pi OS. Below is a complete, robust implementation using the libgpiod v2 API. It requests both an output line (LED) and an input line (Button) with an internal pull-up resistor configured in software.

Prerequisites: Install the development headers via terminal:
sudo apt update && sudo apt install libgpiod-dev build-essential

#include <gpiod.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// Target: Raspberry Pi 5 (RP1 Southbridge exposes GPIO on chip 4)
#define CHIP_PATH "/dev/gpiochip4"
#define LED_PIN 21
#define BTN_PIN 16

volatile sig_atomic_t keep_running = 1;

void handle_sigint(int sig) {
    keep_running = 0;
}

int main(void) {
    signal(SIGINT, handle_sigint);

    // 1. Open the GPIO chip
    struct gpiod_chip *chip = gpiod_chip_open(CHIP_PATH);
    if (!chip) {
        perror("gpiod_chip_open failed");
        return EXIT_FAILURE;
    }

    // 2. Configure LED (Output)
    struct gpiod_line_settings *led_settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(led_settings, GPIOD_LINE_DIRECTION_OUTPUT);
    gpiod_line_settings_set_output_value(led_settings, GPIOD_LINE_VALUE_INACTIVE);

    struct gpiod_line_config *led_cfg = gpiod_line_config_new();
    unsigned int led_offset = LED_PIN;
    gpiod_line_config_add_line_settings(led_cfg, &led_offset, 1, led_settings);

    struct gpiod_request_config *led_req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(led_req_cfg, "pi5-led-demo");

    struct gpiod_line_request *led_req = gpiod_chip_request_lines(chip, led_req_cfg, led_cfg);
    if (!led_req) {
        perror("LED line request failed");
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }

    // 3. Configure Button (Input with Internal Pull-Up)
    struct gpiod_line_settings *btn_settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(btn_settings, GPIOD_LINE_DIRECTION_INPUT);
    gpiod_line_settings_set_bias(btn_settings, GPIOD_LINE_BIAS_PULL_UP);

    struct gpiod_line_config *btn_cfg = gpiod_line_config_new();
    unsigned int btn_offset = BTN_PIN;
    gpiod_line_config_add_line_settings(btn_cfg, &btn_offset, 1, btn_settings);

    struct gpiod_request_config *btn_req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(btn_req_cfg, "pi5-btn-demo");

    struct gpiod_line_request *btn_req = gpiod_chip_request_lines(chip, btn_req_cfg, btn_cfg);
    if (!btn_req) {
        perror("Button line request failed");
        gpiod_line_request_release(led_req);
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }

    printf("System Ready. Press button on BCM %d to toggle LED on BCM %d.\n", BTN_PIN, LED_PIN);
    printf("Press Ctrl+C to exit cleanly.\n");

    int led_state = 0;
    int last_btn_state = 1; // Pull-up means HIGH (1) when unpressed

    // 4. Main Polling Loop
    while (keep_running) {
        enum gpiod_line_value current_btn = gpiod_line_request_get_value(btn_req, BTN_PIN);
        
        // Detect falling edge (button pressed, goes from 1 to 0)
        if (current_btn == GPIOD_LINE_VALUE_ACTIVE && last_btn_state == 1) {
            led_state = !led_state;
            enum gpiod_line_value new_led_val = led_state ? GPIOD_LINE_VALUE_ACTIVE : GPIOD_LINE_VALUE_INACTIVE;
            gpiod_line_request_set_value(led_req, LED_PIN, new_led_val);
            printf("Button pressed! LED state: %s\n", led_state ? "ON" : "OFF");
            usleep(200000); // Basic software debounce (200ms)
        }
        
        last_btn_state = (current_btn == GPIOD_LINE_VALUE_ACTIVE) ? 0 : 1;
        usleep(10000); // Poll every 10ms to save CPU cycles
    }

    // 5. Clean Release (Crucial for libgpiod v2)
    printf("\nCleaning up resources...\n");
    gpiod_line_request_set_value(led_req, LED_PIN, GPIOD_LINE_VALUE_INACTIVE);
    gpiod_line_request_release(led_req);
    gpiod_line_request_release(btn_req);
    gpiod_request_config_free(led_req_cfg);
    gpiod_request_config_free(btn_req_cfg);
    gpiod_line_config_free(led_cfg);
    gpiod_line_config_free(btn_cfg);
    gpiod_line_settings_free(led_settings);
    gpiod_line_settings_free(btn_settings);
    gpiod_chip_close(chip);

    return EXIT_SUCCESS;
}

Compilation Command:
gcc -o pi5_gpio_demo pi5_gpio_demo.c $(pkg-config --cflags --libs libgpiod)
./pi5_gpio_demo

Debugging: Exact Errors and the "First Three" Checks

When using C on Raspberry Pi with the character device API, the kernel is unforgiving about permissions and device paths. If your program crashes, do not guess. Look at the exact perror string.

Common Error Strings and Ranked Causes

Error 1: gpiod_chip_open failed: No such file or directory

  • Cause A (90%): You are running on a Pi 5, but your code defines CHIP_PATH as "/dev/gpiochip0". Change it to "/dev/gpiochip4".
  • Cause B (10%): The gpio kernel module failed to load. Check dmesg | grep rp1 for southbridge initialization errors.

Error 2: LED line request failed: Permission denied

  • Cause A: You are running the binary as a standard user, and the udev rules haven't granted the gpio group access to /dev/gpiochip4. Run with sudo ./pi5_gpio_demo to test, then fix udev rules for production.
  • Cause B: Another process (like a Python script using gpiozero or the pigpiod daemon) already has an exclusive lock on BCM 21. The chardev API enforces strict line locking.

Error 3: Segmentation fault (core dumped)

  • Cause A: You ignored the return value of gpiod_chip_open() and passed a NULL pointer into gpiod_line_config_add_line_settings(). Always check for NULL after every allocation or open call.
The "First Three" Checks When It Fails:
  1. Verify the Chip Path: Run gpiodetect in the terminal. Look for the chip labeled rp1-gpio. Note its number (e.g., gpiochip4).
  2. Check Line Availability: Run gpioinfo gpiochip4 | grep 21. If it says "used", another process is hogging your pin.
  3. Verify User Groups: Run groups. If gpio or dialout is missing, add your user via sudo usermod -aG gpio $USER and reboot.

Extending and Simplifying the Build

The code provided above uses a polling loop (usleep) to read the button state. While sufficient for simple UI buttons, polling wastes CPU cycles and introduces latency.

How to Extend: Edge-Triggered Interrupts

To make this production-ready, replace the polling loop with libgpiod's edge event buffer. By configuring the button line settings with gpiod_line_settings_set_edge_detection(btn_settings, GPIOD_LINE_EDGE_FALLING), you can use gpiod_line_request_wait_edge_events(). This puts the thread to sleep at the kernel level until the physical button is actually pressed, dropping CPU usage to effectively zero and eliminating the need for software debounce delays. For a deep dive into the kernel character device API, refer to the official libgpiod kernel repository documentation.

How to Simplify: Dropping to Python or sysfs

If you are strictly prototyping and do not need C-level execution speeds, writing C for GPIO control is overkill. The Raspberry Pi 5 official documentation heavily favors Python via the gpiozero library, which handles the RP1 southbridge translation automatically. Alternatively, if you are writing a quick bash script, you can interact with the gpioinfo and gpioset command-line utilities that ship with libgpiod, bypassing the need to compile C code entirely.

However, if you are building a high-frequency data logger, interfacing with SPI/I2C sensors where bit-banging is required, or integrating GPIO triggers into a larger C++ robotics stack, mastering the libgpiod v2 API on the Pi 5 is an essential, non-negotiable skill for modern embedded development.