If you want to program Raspberry Pi in C for direct hardware control, you must account for the architectural shift introduced by the Raspberry Pi 5. The legacy pigpio library relied on direct /dev/mem memory mapping, which the new RP1 southbridge chip on the Pi 5 blocks for user-space applications. To achieve microsecond timing precision and minimal memory overhead in 2026, the modern standard is to use the lgpio C library via the Linux character device interface.
This guide provides a decision framework for board selection, a complete hardware spec sheet, and a fully compilable C program with robust error handling to blink an LED and read a button state.
The Decision Path: Choosing Your Board and C Library
Before writing code, you must align your hardware with the correct C library. Using legacy libraries on modern hardware will result in immediate segmentation faults.
| Condition / Requirement | Hardware Pick | C Library | Verdict |
|---|---|---|---|
Need legacy /dev/mem mmap speed for bit-banging RF |
Raspberry Pi 4 Model B | pigpio |
Legacy path. Avoid for new designs. |
Need strict kernel-compliant GPIO via libgpiod wrappers |
Raspberry Pi 5 (Any) | libgpiod v2.x |
Steep learning curve, verbose C API. |
| Need modern RP1 compatibility with simple, pigpio-like C syntax | Raspberry Pi 5 (8GB) | lgpio |
DEFAULT PICK. Best balance of speed and usability. |
Concrete Pick: For this build, we are targeting the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (64-bit, Bookworm or later), utilizing Joan's lgpio C library. This combination guarantees compatibility with the RP1 chip while keeping the C syntax clean and readable.
Hardware Spec Sheet & Pin Mapping
Do not guess your resistor values. The Pi 5 GPIO pins operate at exactly 3.3V logic. Feeding 5V back into these pins will permanently destroy the RP1 chip.
Parts List
- Board: Raspberry Pi 5 (8GB) - ~$80 USD
- Resistor: 270Ω ±5%, 1/4W carbon film (limits current to ~12mA, safe for the 16mA per-pin RP1 limit)
- LED: 5mm diffused red (forward voltage ~2.0V)
- Switch: 6x6mm SPST-NO tactile switch
- Wiring: 40-pin female-to-male 24 AWG jumper wires
Pin Mapping Table (BCM Numbering)
The lgpio library strictly uses Broadcom (BCM) GPIO numbering, not the physical pin numbers on the header.
| BCM GPIO | Physical Pin | Direction | Component Connection |
|---|---|---|---|
| 17 | 11 | Output | 270Ω Resistor → LED Anode |
| 27 | 13 | Input | Tactile Switch Pin 1 (Internal Pull-Up enabled) |
| N/A (GND) | 9 | Ground | LED Cathode & Tactile Switch Pin 2 |
Environment Setup and Compilation Steps
Follow these steps to prepare your Pi 5 toolchain. You need the development headers to compile C code against the library.
- Update the OS and install build tools:
sudo apt update && sudo apt install build-essential - Install the lgpio C library:
sudo apt install liblgpio-dev - Verify GPIO group permissions:
Ensure your user can access/dev/gpiochip0without root. Runls -l /dev/gpiochip0. If your user is not in thegpiogroup, run:
sudo usermod -aG gpio $USER(then log out and back in). - Compile the code:
Save the C code below asgpio_demo.cand compile it with the lgpio linker flag:
gcc -o gpio_demo gpio_demo.c -llgpio - Execute:
./gpio_demo
-Wall -Wextra to catch implicit GPIO type conversions before they cause runtime crashes on the hardware.
Complete C Source Code with Error Handling
This code targets the Pi 5 via lgpio. It claims GPIO 17 as an output and GPIO 27 as an input with an internal pull-up resistor. It includes a signal handler to safely release the GPIO pins back to the kernel when you press Ctrl+C.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <lgpio.h>
// BCM Pin Definitions
#define LED_PIN 17
#define BTN_PIN 27
#define GPIO_CHIP 0
int h; // gpiochip handle
// Cleanup function to release pins on exit
void cleanup(int signum) {
printf("\nCaught signal %d, releasing GPIO pins...\n", signum);
if (h >= 0) {
lgGpioWrite(h, LED_PIN, 0);
lgGpioFree(h, LED_PIN);
lgGpioFree(h, BTN_PIN);
lgGpiochipClose(h);
}
exit(0);
}
int main() {
// Trap Ctrl+C and kill signals
signal(SIGINT, cleanup);
signal(SIGTERM, cleanup);
// Open the gpiochip device
h = lgGpiochipOpen(GPIO_CHIP);
if (h < 0) {
fprintf(stderr, "Failed to open gpiochip%d: %s\n", GPIO_CHIP, lguErrorText(h));
return 1;
}
// Claim LED pin as output, initialize LOW
if (lgGpioClaimOutput(h, 0, LED_PIN, 0) < 0) {
fprintf(stderr, "Failed to claim output pin %d. Is it in use?\n", LED_PIN);
lgGpiochipClose(h);
return 1;
}
// Claim Button pin as input with internal Pull-Up
if (lgGpioClaimInput(h, LG_PULL_UP, BTN_PIN) < 0) {
fprintf(stderr, "Failed to claim input pin %d.\n", BTN_PIN);
lgGpioFree(h, LED_PIN);
lgGpiochipClose(h);
return 1;
}
printf("GPIO Demo Running. Press Ctrl+C to exit.\n");
int btn_state;
int led_state = 0;
while(1) {
btn_state = lgGpioRead(h, BTN_PIN);
// Button pressed (Active LOW due to pull-up)
if (btn_state == 0) {
led_state = !led_state;
lgGpioWrite(h, LED_PIN, led_state);
lguSleep(0.2); // 200ms software debounce
} else {
// Standard blink when button is released
led_state = !led_state;
lgGpioWrite(h, LED_PIN, led_state);
lguSleep(0.5); // 500ms blink interval
}
}
return 0;
}
Troubleshooting: Exact Errors and the First 3 Checks
When programming Raspberry Pi in C, the kernel's character device interface is strict. If your code fails to run, look for these exact error strings in your terminal output.
Exact Error String: Failed to open gpiochip0: error -13 (Permission denied)
Ranked Causes:
- Missing User Group: Your user is not in the
gpiogroup. Fix:sudo usermod -aG gpio $USERand reboot. - Udev Rule Conflict: A custom udev rule is overriding default permissions on
/dev/gpiochip*. Check/etc/udev/rules.d/.
Exact Error String: Failed to claim output pin 17. Is it in use? (Error -16 / Device or resource busy)
Ranked Causes:
- Ghost Process: A previous instance of your C program crashed without triggering the cleanup handler, leaving the pin locked. Fix:
killall gpio_demo. - Device Tree Overlay: The pin is claimed by an overlay in
/boot/firmware/config.txt(e.g.,dtoverlay=gpio-fan). Fix: Comment out conflicting overlays and reboot. - SPI/I2C Conflict: You are trying to use a pin reserved for the primary SPI0 or I2C1 bus. Stick to safe BCM pins like 17, 27, 22, 5, 6.
- Permissions: Run
groupsin the terminal. Ifgpiois missing, you cannot access the hardware. - Pin Numbering Scheme: Verify you are passing the BCM number (17) to
lgGpioClaimOutput, not the physical header number (11).lgpiodoes not auto-translate physical pins. - Linker Flags: Ensure you compiled with
-llgpioat the end of the gcc command. Putting it before the.cfile will result in undefined reference errors during linking.
Extending or Simplifying the Build
Once the baseline code is running, you will likely need to adapt it for your specific project requirements. Here is how to adjust the architecture.
How to Simplify (Pure Output)
If you are driving a relay module or just need a heartbeat LED, strip out the input logic. Remove lgGpioClaimInput and the btn_state conditional block. Replace the while(1) loop body with a simple lgGpioWrite(h, LED_PIN, 1); lguSleep(1.0); lgGpioWrite(h, LED_PIN, 0); lguSleep(1.0);. This reduces CPU polling overhead to near zero.
How to Extend (Hardware Interrupts)
Polling lgGpioRead in a while(1) loop wastes CPU cycles and introduces latency. For production-grade sensor reading, replace the polling loop with an asynchronous hardware alert.
Use lgGpioSetAlertsFunc(h, BTN_PIN, my_callback_function). This registers a callback that the kernel triggers only when the physical pin state changes. According to the official Raspberry Pi compute architecture documentation, utilizing the underlying libgpiod event loops via lgpio alerts reduces interrupt latency to under 50 microseconds on the Pi 5, compared to the 2-5 millisecond jitter you get from Python-based GPIO libraries.
When building embedded C applications on the Pi 5, always default to interrupt-driven architectures for inputs. It frees your main thread to handle network sockets, display rendering, or heavy computation without missing a single button press.






