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 |
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
- Install the development headers:
sudo apt update && sudo apt install liblgpio-dev - Save the code: Save the block above as
pwm_btn.c. - Compile with GCC:
gcc -Wall -O2 -o pwm_btn pwm_btn.c -llgpio - Execute:
./pwm_btn(Note:lgpiorespects standard Linux permissions; if your user is in thegpiogroup, you do not needsudo).
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.
fatal error: lgpio.h: No such file or directoryRanked 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.
FATAL: Could not open GPIO chip 4. Error code: -1Ranked 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:
- Verify the Chip Number: Run
ls /dev/gpiochip*in the terminal. If you see/dev/gpiochip0and/dev/gpiochip4, you are on a Pi 5. If you only see0, change the#define GPIO_CHIP 4to0in the C code. - Check Pin Ownership: Run
sudo lgpios(if installed) orgpioinfoto see if another daemon (like Home Assistant or a stray Python script) has locked GPIO 18. - Validate the Toolchain: Ensure you linked the library during compilation. Forgetting
-llgpioat the end of thegcccommand 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.






