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.
| 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.
| 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
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:
- Insert the tactile switch across the breadboard's center trench.
- Connect a jumper from Physical Pin 36 (BCM 16) to one leg of the switch.
- Connect a jumper from Physical Pin 39 (GND) to the opposite leg of the switch.
- Insert the LED. Connect the 330Ω resistor to the Anode (long leg).
- Connect a jumper from Physical Pin 40 (BCM 21) to the free end of the resistor.
- 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_PATHas"/dev/gpiochip0". Change it to"/dev/gpiochip4". - Cause B (10%): The
gpiokernel module failed to load. Checkdmesg | grep rp1for 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
gpiogroup access to/dev/gpiochip4. Run withsudo ./pi5_gpio_demoto test, then fix udev rules for production. - Cause B: Another process (like a Python script using
gpiozeroor thepigpioddaemon) 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 aNULLpointer intogpiod_line_config_add_line_settings(). Always check forNULLafter every allocation or open call.
- Verify the Chip Path: Run
gpiodetectin the terminal. Look for the chip labeledrp1-gpio. Note its number (e.g.,gpiochip4). - Check Line Availability: Run
gpioinfo gpiochip4 | grep 21. If it says "used", another process is hogging your pin. - Verify User Groups: Run
groups. Ifgpioordialoutis missing, add your user viasudo usermod -aG gpio $USERand 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.






