If you are programming Raspberry Pi with C in 2026, you have likely hit a massive wall: the code that worked flawlessly on the Pi 4 completely fails on the Pi 5. The direct answer to modern Pi GPIO programming in C is to abandon legacy memory-mapped libraries and use the lgpio library. It natively supports the Pi 5’s RP1 southbridge chip via the Linux character device interface, requiring no background daemons and offering hardware-backed PWM.
This guide targets the Raspberry Pi 5 (8GB variant) and the Raspberry Pi 4 Model B. We will wire a PWM-controlled LED and a button, write robust C code with error handling, and debug the exact errors the new RP1 architecture throws at you.
Hardware BOM and GPIO Pin Mapping
Before writing code, we need to map the physical 40-pin header to the Broadcom (BCM) GPIO numbers that the C library expects. The Pi 5 maintains the same physical pinout as the Pi 4, but the underlying silicon routing is handled by the RP1 chip.
| Component | Exact Variant / Value | Physical Pin | BCM GPIO | Function in Code |
|---|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB) | N/A | N/A | Host / I2C Master |
| LED | 5mm Diffused Red (2.0Vf) | Pin 11 | GPIO 17 | PWM Output (lgpio) |
| Current Limiter | 220Ω 1/4W Resistor | In-line | N/A | Limits current to ~6mA |
| Tactile Switch | 6x6mm Momentary NO | Pin 13 | GPIO 27 | Digital Input (Pull-up) |
| Pull-up Resistor | Internal (Software) | N/A | GPIO 27 | Configured via lgpio |
Choosing Your C Library for the RP1 Southbridge
The most common mistake when programming Raspberry Pi with C today is trying to use wiringPi or bcm2835. On the Pi 4, these libraries used mmap to directly access the ARM core's physical memory addresses for the GPIO peripheral. The Pi 5 uses the RP1 southbridge, meaning the ARM core no longer has direct memory access to the GPIO registers. You must use a library that talks to the Linux /dev/gpiochipX character device.
| C Library | Pi 5 RP1 Support | Daemon Required? | PWM Type | 2026 Status |
|---|---|---|---|---|
| lgpio | Native (via chardev) | No | Hardware | Active / Recommended |
| pigpio | Yes (via pigpiod) | Yes | Hardware | Legacy / Maintenance |
| wiringPi | No | No | Hardware | Abandoned (Do not use) |
| bcm2835 | No | No | Software | Legacy (Pi 4 and older) |
| libgpiod | Native (via chardev) | No | None (Digital only) | Active (Standard Linux) |
We use lgpio (part of the lg project by joan2937) because it provides hardware PWM support—which libgpiod lacks—and doesn't require running a background daemon like pigpio does. You can view the official lgpio C API documentation for the full function reference.
Compilable C Code: Button Interrupts and PWM
Below is the complete, compilable C code. It initializes the GPIO chip, claims the pins, sets up an internal pull-up resistor for the button, and uses hardware PWM to fade the LED based on the button state.
Prerequisites: Install the library via your package manager (sudo apt install liblgpio-dev) or compile from source.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <lgpio.h>
// --- PIN DEFINITIONS ---
#define GPIO_CHIP 0 // Pi 5 header is typically gpiochip0
#define LED_PIN 17 // BCM 17 (Physical Pin 11)
#define BTN_PIN 27 // BCM 27 (Physical Pin 13)
#define PWM_FREQ 1000 // 1kHz PWM frequency
static volatile int keep_running = 1;
void handle_sigint(int sig) {
keep_running = 0;
}
int main() {
signal(SIGINT, handle_sigint);
// 1. Open the GPIO chip
int h = lgGpiochipOpen(GPIO_CHIP);
if (h < 0) {
fprintf(stderr, "Failed to open gpiochip%d: %s\n", GPIO_CHIP, lguErrorText(h));
return EXIT_FAILURE;
}
// 2. Claim LED pin for output and initialize PWM
int led_claim = lgGpioClaimOutput(h, 0, LED_PIN, 0);
if (led_claim < 0) {
fprintf(stderr, "Failed to claim LED pin: %s\n", lguErrorText(led_claim));
lgGpiochipClose(h);
return EXIT_FAILURE;
}
lgTxPwm(h, LED_PIN, PWM_FREQ, 0.0, 0, 0); // Start PWM at 0% duty
// 3. Claim Button pin for input with internal Pull-Up
// LG_SET_PULL_UP is a flag to enable the internal resistor
int btn_claim = lgGpioClaimInput(h, LG_SET_PULL_UP, BTN_PIN);
if (btn_claim < 0) {
fprintf(stderr, "Failed to claim Button pin: %s\n", lguErrorText(btn_claim));
lgGpiochipClose(h);
return EXIT_FAILURE;
}
printf("System initialized. Press CTRL+C to exit.\n");
int btn_state = 1;
int duty_cycle = 0;
// 4. Main polling loop
while (keep_running) {
btn_state = lgGpioRead(h, BTN_PIN);
// Button is Active LOW due to pull-up
if (btn_state == 0) {
duty_cycle = (duty_cycle >= 100) ? 0 : duty_cycle + 10;
lgTxPwm(h, LED_PIN, PWM_FREQ, (float)duty_cycle, 0, 0);
printf("Button pressed! Duty cycle: %d%%\n", duty_cycle);
lguSleep(0.25); // Software debounce
}
lguSleep(0.05); // 50ms loop delay to prevent CPU spiking
}
// 5. Cleanup and release hardware
printf("\nShutting down safely...\n");
lgTxPwm(h, LED_PIN, PWM_FREQ, 0.0, 0, 0); // Kill PWM
lgGpioFree(h, LED_PIN);
lgGpioFree(h, BTN_PIN);
lgGpiochipClose(h);
return EXIT_SUCCESS;
}
Compile and Run:
gcc -o gpio_demo gpio_demo.c -llgpio
./gpio_demo
Debugging: "Device or Resource Busy" and Chip Errors
When programming Raspberry Pi with C on the newer character-device architecture, you will encounter specific OS-level rejections. Here is how to decode them.
lgGpioClaim: error -4 (Device or resource busy)Meaning: The Linux kernel has already granted exclusive access to this specific GPIO line to another process.
Ranked Causes & Fixes:
- Orphaned Python/C Scripts: A previous script crashed before calling
lgGpioFree(). Fix: Runsudo killall python3or find the PID holding the pin viasudo lsof | grep gpiochip. - Daemon Conflicts: You have
pigpiodorlgpiodrunning in the background. Fix:sudo systemctl stop pigpiod. - Device Tree Overlays: An overlay in
/boot/firmware/config.txt(likedtoverlay=gpio-ir) has claimed the pin at boot. Fix: Remove the overlay and reboot.
lgGpiochipOpen: error -1 (No such file or directory)Meaning: The library cannot find
/dev/gpiochip0.
The First Three Things to Check When It Fails:
- Verify the Character Device Exists: Run
ls -l /dev/gpiochip*. On the Pi 5, the 40-pin header is usuallygpiochip0, but if you have custom HATs loaded, it might shift togpiochip4. Usegpiodetect(from thegpiodpackage) to list all available chips and their labels. - Check User Permissions: By default, only
rootor users in thegpiogroup can access/dev/gpiochipX. If you aren't usingsudo, add your user to the group:sudo usermod -aG gpio $USER, then log out and back in. - Confirm Kernel Module Loading: Ensure the
gpio_rp1kernel module is loaded. Runlsmod | grep rp1. If it's missing, your Pi OS installation is likely outdated or corrupted; re-flash the latest 64-bit Raspberry Pi OS.
Extending and Simplifying the Build
Once you have the basic digital I/O and PWM working, you will inevitably want to scale the project. Here is how to adapt the architecture.
How to Extend: Adding I2C Sensors
The lgpio library includes a full I2C API. To add a BME280 temperature sensor without relying on external Python wrappers, use the native I2C functions. You do not need to claim the I2C pins (BCM 2 and 3) via lgGpioClaim; the I2C subsystem handles them.
// Open I2C bus 1, device address 0x76
int i2c_handle = lgI2cOpen(1, 0x76, 0);
if (i2c_handle >= 0) {
uint8_t chip_id;
lgI2cReadI2cBlockData(i2c_handle, 0xD0, 1, &chip_id);
printf("BME280 Chip ID: 0x%02X\n", chip_id);
lgI2cClose(i2c_handle);
}
How to Simplify: Prototyping with CLI Tools
Writing C code to test if a physical button is wired correctly is a waste of time. Before compiling your C program, use the lg command-line utilities to verify the hardware. According to the official Raspberry Pi configuration docs, verifying hardware at the OS level saves hours of debugging.
- Read a pin:
lgGpioRead 0 27(Returns 0 or 1) - Write a pin:
lgGpioWrite 0 17 1(Turns LED on instantly) - Monitor changes:
lgGpioMonitor 0 27(Streams state changes to the terminal as you press the button)
By mastering the lgpio character-device workflow, you bypass the legacy memory-mapping traps that break older tutorials. You get deterministic, hardware-backed PWM and safe, multi-threaded GPIO access that respects the Linux kernel's resource management—exactly what a production-grade embedded C application requires.






