If you are programming in C on Raspberry Pi hardware in 2026, you must discard the legacy tutorials. The Raspberry Pi 5 introduced the RP1 southbridge chip, fundamentally changing how the CPU communicates with GPIO pins. The old /dev/mem memory-mapped hacks used by WiringPi and early RPi.GPIO are dead. Today, the undisputed standard for bare-metal C GPIO control on the Pi is lgpio, which interfaces cleanly with the Linux kernel's character device API.

This guide targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm (64-bit). We will build a hardware-PWM LED fader controlled by a button interrupt, covering the exact pin mappings, compilable C code, and the specific kernel-level errors you will face when migrating from older boards.

Platform Comparison: C on Linux vs. Bare-Metal Microcontrollers

Before writing code, understand the environment. A Raspberry Pi running Linux is not a real-time microcontroller. When you toggle a pin in C on the Pi, the Linux scheduler can interrupt your thread, introducing jitter. Here is how the Pi 5 stacks up against dedicated embedded platforms for C/C++ development.

Platform GPIO Toggle Latency RAM Overhead (Idle) OS Jitter / Real-Time Best Use Case
Raspberry Pi 5 (C/lgpio) ~1.5 μs (user-space) ~120 MB (Bookworm Lite) High (ms spikes possible) Networked edge gateways, complex DSP, computer vision + GPIO
ESP32-S3 (C/ESP-IDF) ~0.1 μs (bare-metal) ~200 KB (FreeRTOS) Low (μs deterministic) High-speed motor control, strict timing protocols (WS2812B)
Arduino Uno R4 (C/HAL) ~0.2 μs (bare-metal) ~2 KB (No OS) None (Hard real-time) Simple sensors, low-power battery nodes, basic actuators
Raspberry Pi 5 (Python) ~15 μs to 50 μs ~160 MB (+ Interpreter) Very High Rapid prototyping, UI dashboards, non-critical logging
Bench Note: If your project requires sub-microsecond bit-banging (like driving hundreds of addressable LEDs without an external driver IC), do not use the Pi. Offload that task to an ESP32 or a dedicated PIO chip, and let the Pi handle the high-level logic via UART/SPI.

Hardware BOM and Pin Mapping

This build requires minimal components, but precision matters. Using the wrong resistor value or misidentifying the physical pinout is the leading cause of hardware debugging headaches.

Parts List

  • Board: Raspberry Pi 5 (8GB) with active cooler and 27W USB-C PD power supply.
  • LED: Standard 5mm diffuse red LED (Vf ≈ 2.0V, If = 20mA).
  • Current Limiting Resistor: 330Ω 1/4W carbon film (yields ≈ 10mA at 3.3V logic, safe for Pi GPIO).
  • Switch: 6x6mm tactile pushbutton (SPST-NO).
  • Wiring: 22 AWG solid core jumper wires, 400-tie-point breadboard.

Pin Mapping Table

The Raspberry Pi 5 maintains the standard 40-pin header layout, but the internal routing to the RP1 chip means you must verify your pin functions. We are using BCM numbering in the C code.

BCM GPIO Physical Pin Function in this Build Hardware Notes
GPIO 18 12 Hardware PWM0 Output One of the few pins routed to the RP1 hardware PWM slice. Do not use software PWM here.
GPIO 17 11 Button Input (Active LOW) Configured with internal pull-up. Connects to GND when pressed.
GND 9 Circuit Common Required for both the LED return path and the button switch.

The Complete C Implementation

Below is the complete, compilable C code. It initializes the lgpio library, claims the pins, and runs a polling loop with software debouncing to toggle hardware PWM on the LED. Error handling is baked into every system call.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <lgpio.h>

// Target Board Configuration
// CRITICAL: Use 4 for Raspberry Pi 5, use 0 for Raspberry Pi 4
#define GPIO_CHIP 4  
#define LED_GPIO 18  // Hardware PWM capable
#define BTN_GPIO 17  // Input with internal pull-up

int main(int argc, char *argv[]) {
    printf("Initializing GPIO chip %d...\n", GPIO_CHIP);
    
    // 1. Open the GPIO chip
    int chip_handle = lgGpiochipOpen(GPIO_CHIP);
    if (chip_handle < 0) {
        fprintf(stderr, "FATAL: Could not open GPIO chip %d. Error code: %d\n", GPIO_CHIP, chip_handle);
        fprintf(stderr, "Hint: Check if you are on RPi 5 (chip 4) or RPi 4 (chip 0).\n");
        return EXIT_FAILURE;
    }

    // 2. Claim LED pin as Output (default LOW)
    if (lgGpioClaimOutput(chip_handle, 0, LED_GPIO, 0) < 0) {
        fprintf(stderr, "FATAL: Failed to claim GPIO %d as output. Pin may be in use.\n", LED_GPIO);
        lgGpiochipClose(chip_handle);
        return EXIT_FAILURE;
    }

    // 3. Claim Button pin as Input with Internal Pull-Up
    if (lgGpioClaimInput(chip_handle, LG_PULL_UP, BTN_GPIO) < 0) {
        fprintf(stderr, "FATAL: Failed to claim GPIO %d as input.\n", BTN_GPIO);
        lgGpiochipClose(chip_handle);
        return EXIT_FAILURE;
    }

    printf("System online. Press button on GPIO %d to toggle PWM on GPIO %d.\n", BTN_GPIO, LED_GPIO);
    printf("Press Ctrl+C to exit cleanly.\n");

    int pwm_state = 0;
    int last_btn_state = 1; // Pull-up means HIGH (1) is the default unpressed state

    // 4. Main Control Loop
    while (1) {
        int btn_state = lgGpioRead(chip_handle, BTN_GPIO);
        if (btn_state < 0) {
            fprintf(stderr, "Read error on GPIO %d\n", BTN_GPIO);
            break;
        }

        // Detect falling edge (transition from HIGH to LOW = button pressed)
        if (last_btn_state == 1 && btn_state == 0) {
            pwm_state = !pwm_state; // Toggle state
            
            if (pwm_state) {
                // Enable Hardware PWM: 1000Hz frequency, 50% duty cycle
                lgTxPwm(chip_handle, LED_GPIO, 1000.0, 50.0, 0, 0);
                printf("[STATE] PWM ON (50%% duty, 1kHz)\n");
            } else {
                // Disable PWM by setting duty cycle to 0%
                lgTxPwm(chip_handle, LED_GPIO, 1000.0, 0.0, 0, 0);
                printf("[STATE] PWM OFF\n");
            }
            
            // Software debounce: ignore further reads for 200ms
            usleep(200000); 
        }
        
        last_btn_state = btn_state;
        usleep(10000); // 10ms polling interval (yields CPU time back to Linux)
    }

    // 5. Cleanup
    printf("\nShutting down and releasing pins...\n");
    lgTxPwm(chip_handle, LED_GPIO, 1000.0, 0.0, 0, 0); // Ensure LED is off
    lgGpiochipClose(chip_handle);
    return EXIT_SUCCESS;
}

Compilation and Execution Steps

  1. Install the development headers: sudo apt update && sudo apt install liblgpio-dev
  2. Save the code: Save the block above as pwm_btn.c.
  3. Compile with GCC: gcc -Wall -O2 -o pwm_btn pwm_btn.c -llgpio
  4. Execute: ./pwm_btn (Note: lgpio respects standard Linux permissions; if your user is in the gpio group, you do not need sudo).

Debugging: When the Compiler or Kernel Fights Back

Migrating to modern C GPIO libraries introduces specific friction points. If your build fails or the binary crashes on execution, follow this diagnostic tree.

Exact Error String: fatal error: lgpio.h: No such file or directory
Ranked Causes:
1. You forgot to install the C headers (liblgpio-dev). The base python3-lgpio package does not include the C headers.
2. You are using a legacy 32-bit Raspberry Pi OS image from 2021 that lacks the modern Bookworm repositories.
Exact Error String: FATAL: Could not open GPIO chip 4. Error code: -1
Ranked Causes:
1. Chip Mismatch: You are running this code on a Raspberry Pi 4 (which uses chip 0) or a Pi 3, but the code is hardcoded to 4 for the Pi 5 RP1 chip.
2. Permissions: Your user is not in the gpio group, and you didn't run the binary with sudo.
3. Kernel Overlay Conflict: A /boot/firmware/config.txt overlay (like dtoverlay=pwm) has already claimed the character device.

The First Three Things to Check When It Fails

Before tearing apart your breadboard, verify these three software states:

  1. Verify the Chip Number: Run ls /dev/gpiochip* in the terminal. If you see /dev/gpiochip0 and /dev/gpiochip4, you are on a Pi 5. If you only see 0, change the #define GPIO_CHIP 4 to 0 in the C code.
  2. Check Pin Ownership: Run sudo lgpios (if installed) or gpioinfo to see if another daemon (like Home Assistant or a stray Python script) has locked GPIO 18.
  3. Validate the Toolchain: Ensure you linked the library during compilation. Forgetting -llgpio at the end of the gcc command results in "undefined reference to `lgGpiochipOpen'" linker errors.

Scaling the Build: Extend or Simplify

Embedded C is modular by nature. Depending on your project phase, you will need to either strip this code down to its bare essentials or scale it up for production.

How to Simplify (The "Hello World" Blink)

If you are just verifying your toolchain and breadboard connections, strip out the button logic and the lgTxPwm calls. Replace the main loop with a simple digital toggle:

while (1) {
    lgGpioWrite(chip_handle, LED_GPIO, 1);
    usleep(500000); // 500ms ON
    lgGpioWrite(chip_handle, LED_GPIO, 0);
    usleep(500000); // 500ms OFF
}

This removes the complexity of PWM slices and pull-up configurations, isolating any wiring faults.

How to Extend (Asynchronous Interrupts & Systemd)

Polling a button in a while(1) loop wastes CPU cycles. For a production daemon, replace the polling loop with lgpio's asynchronous alert system. You register a callback function using lgGpioSetAlerts(), which the kernel triggers only on pin state changes. This drops CPU usage to near zero.

Furthermore, a real embedded device runs on boot. Do not use rc.local. Create a systemd service file at /etc/systemd/system/pwm-btn.service:

[Unit]
Description=Hardware PWM Button Controller
After=sysinit.target local-fs.target

[Service]
ExecStart=/usr/local/bin/pwm_btn
Restart=on-failure
User=pi
Group=gpio

[Install]
WantedBy=multi-user.target

Enable it with sudo systemctl enable --now pwm-btn.service. This ensures your C binary survives reboots, handles crashes gracefully via the Restart directive, and runs with the principle of least privilege by avoiding the root user entirely.

For deeper kernel-level GPIO architecture details, refer to the official Linux kernel libgpiod documentation, which underpins the user-space lgpio wrapper. Understanding the kernel's character device model is what separates a hobbyist from an embedded Linux engineer.