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.

Difficulty: Intermediate | Time: 45 mins | Cost: ~$85 (Pi 5 kit) + $2 components

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.

Spec-Sheet Note: The Pi 5 GPIO pins output 3.3V logic but are powered by a separate 3.3V rail. Do not backfeed 5V into these pins, or you will permanently damage the RP1 southbridge.
ComponentExact Variant / ValuePhysical PinBCM GPIOFunction in Code
MicrocontrollerRaspberry Pi 5 (8GB)N/AN/AHost / I2C Master
LED5mm Diffused Red (2.0Vf)Pin 11GPIO 17PWM Output (lgpio)
Current Limiter220Ω 1/4W ResistorIn-lineN/ALimits current to ~6mA
Tactile Switch6x6mm Momentary NOPin 13GPIO 27Digital Input (Pull-up)
Pull-up ResistorInternal (Software)N/AGPIO 27Configured 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 LibraryPi 5 RP1 SupportDaemon Required?PWM Type2026 Status
lgpioNative (via chardev)NoHardwareActive / Recommended
pigpioYes (via pigpiod)YesHardwareLegacy / Maintenance
wiringPiNoNoHardwareAbandoned (Do not use)
bcm2835NoNoSoftwareLegacy (Pi 4 and older)
libgpiodNative (via chardev)NoNone (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.

Exact Error String: 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:

  1. Orphaned Python/C Scripts: A previous script crashed before calling lgGpioFree(). Fix: Run sudo killall python3 or find the PID holding the pin via sudo lsof | grep gpiochip.
  2. Daemon Conflicts: You have pigpiod or lgpiod running in the background. Fix: sudo systemctl stop pigpiod.
  3. Device Tree Overlays: An overlay in /boot/firmware/config.txt (like dtoverlay=gpio-ir) has claimed the pin at boot. Fix: Remove the overlay and reboot.
Exact Error String: lgGpiochipOpen: error -1 (No such file or directory)
Meaning: The library cannot find /dev/gpiochip0.

The First Three Things to Check When It Fails:

  1. Verify the Character Device Exists: Run ls -l /dev/gpiochip*. On the Pi 5, the 40-pin header is usually gpiochip0, but if you have custom HATs loaded, it might shift to gpiochip4. Use gpiodetect (from the gpiod package) to list all available chips and their labels.
  2. Check User Permissions: By default, only root or users in the gpio group can access /dev/gpiochipX. If you aren't using sudo, add your user to the group: sudo usermod -aG gpio $USER, then log out and back in.
  3. Confirm Kernel Module Loading: Ensure the gpio_rp1 kernel module is loaded. Run lsmod | 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.