If you are starting a new embedded project in 2026, the first thing you need to know about C programming with Raspberry Pi is that the old ways are dead. For years, hobbyists relied on wiringPi or direct memory mapping via /dev/mem. With the release of the Raspberry Pi 5 and its RP1 southbridge chip, direct memory mapping is restricted, and legacy libraries fail to compile or execute. The official, future-proof standard for GPIO control in C on Raspberry Pi OS (Bookworm and newer) is the libgpiod v2 API.
This guide bypasses outdated tutorials and provides a production-ready, fully compilable C project using libgpiod v2. We will build a debounced button-to-LED toggle, map the exact physical pins, and debug the most common kernel-level GPIO errors you will encounter on the bench.
Project Spec Sheet & Parts List
Estimated Time: 45 minutes
Target Board Variant: Raspberry Pi 5 (4GB or 8GB) or Raspberry Pi 4 Model B, running Raspberry Pi OS (64-bit, Bookworm/Trixie)
Core Library:
libgpiod v2.x (libgpiod-dev)
Required Hardware
- Microcontroller: Raspberry Pi 5 (4GB) or Pi 4 Model B
- Output: 5mm Red LED (standard 20mA forward current)
- Current Limiting: 330Ω resistor (1/4W, 5% tolerance)
- Input: 6x6mm Tactile pushbutton switch
- Prototyping: Half-size breadboard, male-to-female jumper wires
Pin Mapping & Breadboard Wiring
The Raspberry Pi uses the Broadcom (BCM) GPIO numbering scheme in software, which differs from the physical header pin numbers. The libgpiod library strictly requires BCM offsets. The following table maps our physical wiring to the software definitions used in the C code below.
| Component | BCM GPIO (Software) | Physical Pin (Header) | Wiring Notes |
|---|---|---|---|
| LED Anode (+) | GPIO 27 | Pin 13 | Connect through 330Ω resistor to Pin 13 |
| LED Cathode (-) | N/A (GND) | Pin 14 | Connect directly to Ground |
| Button Leg 1 | GPIO 17 | Pin 11 | Internal pull-up enabled in software |
| Button Leg 2 | N/A (GND) | Pin 9 | Connect directly to Ground |
The Code: libgpiod v2 Button-LED Toggle
The transition from libgpiod v1 to v2 introduced an object-oriented paradigm in C. You no longer request a single line directly. Instead, you configure a gpiod_line_settings object, apply it to a gpiod_line_config, and finally submit a gpiod_line_request to the kernel. This prevents race conditions and allows atomic configuration of multiple pins.
Save the following code as main.c. It includes comprehensive error handling and explicit pin definitions.
#include <gpiod.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
// Target Board: Raspberry Pi 4 / 5 (Primary GPIO chip)
#define GPIO_CHIP_PATH "/dev/gpiochip0"
// Pin Definitions (BCM Numbering)
#define LED_LINE_OFFSET 27
#define BTN_LINE_OFFSET 17
// Debounce and polling timings (microseconds)
#define DEBOUNCE_US 200000 // 200ms
#define POLL_US 10000 // 10ms
int main(void) {
struct gpiod_chip *chip = NULL;
struct gpiod_line_settings *settings = NULL;
struct gpiod_line_config *line_cfg = NULL;
struct gpiod_line_request *led_req = NULL;
struct gpiod_line_request *btn_req = NULL;
int exit_code = EXIT_SUCCESS;
// 1. Open the GPIO chip
chip = gpiod_chip_open(GPIO_CHIP_PATH);
if (!chip) {
fprintf(stderr, "gpiod_chip_open failed: %s\n", strerror(errno));
return EXIT_FAILURE;
}
// 2. Initialize configuration objects
settings = gpiod_line_settings_new();
line_cfg = gpiod_line_config_new();
if (!settings || !line_cfg) {
fprintf(stderr, "Failed to allocate libgpiod config objects\n");
exit_code = EXIT_FAILURE;
goto cleanup;
}
// --- Configure LED (Output) ---
unsigned int led_offsets[] = { LED_LINE_OFFSET };
gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT);
gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_INACTIVE);
gpiod_line_config_add_line_settings(line_cfg, led_offsets, 1, settings);
led_req = gpiod_chip_request_lines(chip, NULL, line_cfg);
if (!led_req) {
fprintf(stderr, "LED gpiod_line_request failed: %s\n", strerror(errno));
exit_code = EXIT_FAILURE;
goto cleanup;
}
// --- Configure Button (Input with Pull-Up) ---
gpiod_line_config_reset(line_cfg); // Clear previous LED settings
unsigned int btn_offsets[] = { BTN_LINE_OFFSET };
gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
gpiod_line_config_add_line_settings(line_cfg, btn_offsets, 1, settings);
btn_req = gpiod_chip_request_lines(chip, NULL, line_cfg);
if (!btn_req) {
fprintf(stderr, "BTN gpiod_line_request failed: %s\n", strerror(errno));
exit_code = EXIT_FAILURE;
goto cleanup;
}
printf("GPIO initialized. Press button on GPIO %d to toggle LED on GPIO %d.\n",
BTN_LINE_OFFSET, LED_LINE_OFFSET);
// 3. Main Execution Loop
int led_state = 0;
while (1) {
enum gpiod_line_value val = gpiod_line_request_get_value(btn_req, BTN_LINE_OFFSET);
// Button is Active-Low due to Pull-Up resistor configuration
if (val == GPIOD_LINE_VALUE_ACTIVE) {
led_state = !led_state;
enum gpiod_line_value new_led_val = led_state ?
GPIOD_LINE_VALUE_ACTIVE : GPIOD_LINE_VALUE_INACTIVE;
gpiod_line_request_set_value(led_req, LED_LINE_OFFSET, new_led_val);
usleep(DEBOUNCE_US); // Block to debounce mechanical switch bounce
}
usleep(POLL_US);
}
cleanup:
if (led_req) gpiod_line_request_release(led_req);
if (btn_req) gpiod_line_request_release(btn_req);
if (line_cfg) gpiod_line_config_free(line_cfg);
if (settings) gpiod_line_settings_free(settings);
if (chip) gpiod_chip_close(chip);
return exit_code;
}
Compiling, Linking, and Execution
Before compiling, ensure the development headers are installed on your Pi. Open your terminal and run:
sudo apt update
sudo apt install libgpiod-dev build-essential
Compile the code using GCC, explicitly linking the gpiod library:
gcc -Wall -O2 -o gpio_toggle main.c -lgpiod
Execute the binary. While libgpiod v2 respects standard Linux file permissions, you may need sudo if your user is not in the gpio group:
./gpio_toggle
Debugging: "Device or resource busy" & Common Failures
When doing C programming with Raspberry Pi hardware, the kernel strictly enforces resource locking. The most frequent showstopper you will encounter is this exact error string:
BTN gpiod_line_request failed: Device or resource busy
This maps to the EBUSY errno. The kernel refuses to hand over the GPIO line because another entity has already claimed it. Here are the ranked causes and fixes:
- Device Tree Overlay Conflict (Most Likely): You have an I2C, SPI, or UART overlay enabled in
/boot/firmware/config.txtthat consumes GPIO 17 or 27. For example, enablingdtparam=i2c_arm=onclaims specific pins. Fix: Comment out unused overlays inconfig.txtand reboot. - Zombie User-Space Process: A previous run of your C code crashed before reaching the
cleanup:block, or a Python script usingRPi.GPIOis running in the background. Fix: Runsudo lsof | grep gpiochipto find the PID andkill -9it. - Kernel Driver Binding: A specific kernel module (like
leds-gpiofor the Pi's onboard activity LED) has bound to the pin. Fix: Choose a different BCM GPIO offset that is not reserved by the Pi's hardware schematic.
1. Verify the chip path: On Pi 5, the primary user-accessible GPIO chip is
/dev/gpiochip0. If you get "No such file or directory", run ls /dev/gpiochip* to verify the kernel assigned it correctly.2. Check your physical wiring: A short circuit between the 3.3V rail and your input pin will cause erratic reads, even if the C code compiles perfectly.
3. Confirm library version: Run
dpkg -s libgpiod-dev | grep Version. If it reports v1.6.x, you are on an older OS (Bullseye) and the v2 API code above will throw compiler errors. Upgrade to Bookworm.
Extending and Simplifying the Build
How to Extend: Once you have GPIO working, the natural next step in embedded C is adding environmental sensors. You can extend this exact project by reading a BME280 via I2C. Unlike GPIO, I2C in C on Linux does not use libgpiod. Instead, you open the I2C bus as a standard file descriptor (open("/dev/i2c-1", O_RDWR)) and use ioctl calls defined in <linux/i2c-dev.h>. The official Raspberry Pi I2C documentation details the bus addressing.
How to Simplify: If the verbose, object-heavy nature of the libgpiod v2 C API feels like overkill for a simple school project, simplify the build by switching to Python. Python's gpiozero library abstracts all of the kernel-level configuration into three lines of code. Use C when you need microsecond-precise timing, minimal memory footprint (under 2MB RAM), or integration with existing C/C++ backend systems; use Python for rapid prototyping.
FAQ: C Programming with Raspberry Pi
Is wiringPi still viable for C programming with Raspberry Pi in 2026?
No. wiringPi has been officially deprecated and abandoned since 2019. It relies on direct memory mapping to the Broadcom SoC's peripheral addresses. Because the Raspberry Pi 5 uses the RP1 southbridge chip with an entirely different memory map and PCIe-based peripheral routing, wiringPi will either fail to compile or cause a kernel panic if forced. You must use libgpiod or the updated liblgpio for modern Pi hardware.
How do I handle PWM outputs in C on the Raspberry Pi 5?
The libgpiod library strictly handles digital logic (high/low states). For hardware Pulse Width Modulation (PWM) in C, you must interact with the Linux kernel's PWM subsystem via /sys/class/pwm/ or use the libpwm API. Alternatively, for software PWM (bit-banging), you can use high-resolution POSIX timers (clock_nanosleep) in a dedicated C thread, though this will suffer from jitter if the Pi's CPU is under heavy load.
Why does my C GPIO code run slower than Python on the Pi?
If your C code feels sluggish compared to Python's gpiozero, you are likely polling the pin state inside a tight loop with a blocking sleep() call, or you are opening and closing the GPIO chip on every single read. The libgpiod v2 code provided above avoids this by requesting the line handle once at startup and keeping it open. For zero-latency event detection in C, abandon polling entirely and use gpiod_line_request_wait_edge_events() to let the kernel interrupt your program only when the physical button state changes.
Can I use C to read I2C sensors on the Raspberry Pi?
Yes, but it uses a different subsystem than GPIO. While GPIO relies on the kernel libgpiod interface, I2C communication in C is handled via standard POSIX file I/O operations on the /dev/i2c-X character devices. You will need to include <linux/i2c-dev.h> and use ioctl(file, I2C_SLAVE, address) to target your specific sensor before executing read() and write() commands.






