Difficulty: Intermediate | Time: 45 Minutes | Target Board: Raspberry Pi 5 (8GB) running Raspberry Pi OS Bookworm or later

If you are programming the Raspberry Pi in C for GPIO control in 2026, your default choice must be the libgpiod v2 character device API. Legacy libraries like WiringPi are abandoned, and memory-mapped hacks (like early versions of pigpio) fail entirely on the Raspberry Pi 5 because its RP1 southbridge chip moved GPIO control off the main SoC memory map. The Linux kernel's character device interface is now the only reliable, future-proof way to toggle pins with microsecond latency.

Why Write Raspberry Pi Code in C Instead of Python?

Python with gpiozero is excellent for prototyping, but it introduces garbage collection pauses and interpreter overhead that ruin timing-critical applications. When you need to bit-bang a protocol, read a high-frequency encoder, or maintain strict sub-millisecond interrupt latency, C is mandatory.

Language & Library Decision Path
Requirement Best Tool Why
Fast prototyping, web APIs, simple sensors Python (gpiozero) Huge ecosystem, hardware-abstracted, slow execution.
Memory safety, concurrent networking Rust (rppal) Zero-cost abstractions, steep learning curve for beginners.
Raw GPIO speed, < 10µs latency, legacy C integration C (libgpiod v2) Direct kernel syscall mapping, no daemon overhead, maximum control.

The Verdict: If your project requires deterministic timing or you are porting existing microcontroller firmware to a Linux SBC, pick C with libgpiod.

Parts List and Pin Mapping

This build targets the Raspberry Pi 5 (8GB variant). The Pi 5 uses the RP1 chip, which enforces strict character-device GPIO access. The code below will also work on a Pi 4 Model B, provided you are running a modern kernel (6.1+).

Bill of Materials
Component Specification / Part Number Est. Cost
Microcomputer Raspberry Pi 5 (8GB RAM) $80.00
LED 5mm Red Diffused (2.0V forward voltage, 20mA) $0.10
Current Limiting Resistor 330Ω 1/4W Carbon Film (Orange-Orange-Brown-Gold) $0.05
Switch 6x6mm SPST Momentary Tactile Switch $0.10
Wiring Half-size breadboard, 22 AWG solid core jumper wires $5.00

GPIO Pin Mapping (BCM Numbering)

Always wire based on physical pin numbers on the header, but configure your C code using the Broadcom (BCM) GPIO numbers. On the Pi 5, the RP1 chip maps these logically to match legacy BCM numbers for software compatibility.

  • LED Anode (+): GPIO 17 (Physical Pin 11) → 330Ω Resistor → LED → GND (Physical Pin 9)
  • Button Signal: GPIO 27 (Physical Pin 13) → Switch → GND (Physical Pin 14) (Relies on internal pull-up)

Complete C Code: GPIO Output with libgpiod v2

Most online tutorials still show the deprecated libgpiod v1 API (using gpiod_chip_get_line). The v2 API, required for modern Raspberry Pi OS Bookworm, uses line configurations and request objects. This code requests GPIO 17 as an output and blinks it 5 times with full error handling.

Prerequisite: Install the development headers before compiling:
sudo apt update && sudo apt install libgpiod-dev
#include <gpiod.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// Hardware pin definitions (BCM numbering)
#define GPIO_CHIP_PATH "/dev/gpiochip0"
#define LED_LINE_OFFSET 17
#define CONSUMER_NAME "electricalflux-blink"

int main(void) {
    // 1. Open the GPIO chip character device
    struct gpiod_chip *chip = gpiod_chip_open(GPIO_CHIP_PATH);
    if (!chip) {
        perror("gpiod_chip_open failed");
        return EXIT_FAILURE;
    }

    // 2. Configure line settings for Output
    struct gpiod_line_settings *settings = gpiod_line_settings_new();
    if (!settings) {
        perror("gpiod_line_settings_new failed");
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }
    gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT);
    gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_INACTIVE);

    // 3. Apply settings to the specific line offset
    struct gpiod_line_config *line_cfg = gpiod_line_config_new();
    unsigned int offset = LED_LINE_OFFSET;
    gpiod_line_config_add_line_settings(line_cfg, &offset, 1, settings);

    // 4. Create a request configuration to identify our process
    struct gpiod_request_config *req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_cfg, CONSUMER_NAME);

    // 5. Request the line from the kernel
    struct gpiod_line_request *request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
    if (!request) {
        perror("gpiod_chip_request_lines failed");
        // Cleanup on failure
        gpiod_request_config_free(req_cfg);
        gpiod_line_config_free(line_cfg);
        gpiod_line_settings_free(settings);
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }

    printf("Successfully claimed GPIO %d. Blinking...\n", LED_LINE_OFFSET);

    // 6. Execution Loop
    for (int i = 0; i < 5; i++) {
        gpiod_line_request_set_value(request, LED_LINE_OFFSET, GPIOD_LINE_VALUE_ACTIVE);
        sleep(1);
        gpiod_line_request_set_value(request, LED_LINE_OFFSET, GPIOD_LINE_VALUE_INACTIVE);
        sleep(1);
    }

    // 7. Release resources gracefully
    gpiod_line_request_release(request);
    gpiod_request_config_free(req_cfg);
    gpiod_line_config_free(line_cfg);
    gpiod_line_settings_free(settings);
    gpiod_chip_close(chip);

    printf("GPIO released. Exiting.\n");
    return EXIT_SUCCESS;
}

Compiling and Running on the Pi

Do not hardcode library paths. Use pkg-config to dynamically link the correct libgpiod flags.

  1. Save the code above as blink.c on your Pi.
  2. Compile with GCC: gcc -o blink blink.c $(pkg-config --cflags --libs libgpiod)
  3. Execute the binary: ./blink

Note: On a properly configured Raspberry Pi OS, the default user is in the gpio group and does not need sudo to run this. If you get a permission error, see the debugging section below.

Debugging: First Three Things to Check When It Fails

When moving from Python to C on Linux SBCs, hardware abstraction layers hide the OS-level resource locks. Here is your ranked troubleshooting path for the most common compile and runtime failures.

1. Error: gpiod_chip_open failed: Permission denied

Cause: Your user lacks read/write access to the /dev/gpiochip0 character device. This happens on fresh OS installs or if you created a new user without assigning secondary groups.

Fix: Add your user to the gpio group and reboot (or log out and back in):
sudo usermod -aG gpio $USER

2. Error: gpiod_chip_request_lines failed: Device or resource busy

Cause: The Linux kernel enforces strict single-owner rules for GPIO lines. Another process (a background Python script, Node-RED, or a system service) has already requested GPIO 17.

Fix: Find the hogging process and kill it. Use the gpioset tool or lsof:
sudo lsof | grep gpiochip0
Alternatively, use sudo gpioinfo to see which lines are currently marked as "used" and by which consumer.

3. Hardware: Code Runs Successfully, But LED Stays Dark

Cause: You confused Physical Pin numbering with BCM GPIO numbering. Physical Pin 17 is actually BCM GPIO 27. Furthermore, the LED might be reverse-biased.

Fix: Verify your wiring against the BCM map. Use a multimeter in DC voltage mode to probe Physical Pin 11 (BCM 17) against Ground while the code is running. You should see 3.3V toggle to 0V every second. If you see 0V constantly, your offset macro is wrong. If you see 3.3V but no light, swap the LED legs.

Extending and Simplifying the Build

The code provided is a robust foundation. Depending on your project constraints, you should adapt it using these specific paths:

  • To Simplify (Headless Output Only): If you only need to trigger a relay and don't care about graceful cleanup (e.g., in a quick-and-dirty cron job), you can drop the gpiod_line_request_release() calls. The kernel will automatically release the GPIO lines when your C process terminates and the file descriptors close. However, explicit release is best practice for daemonized applications.
  • To Extend (Hardware Interrupts): To read the tactile switch on GPIO 27 without burning CPU cycles in a while(1) polling loop, change the line direction to GPIOD_LINE_DIRECTION_INPUT, set the edge detection to GPIOD_LINE_EDGE_FALLING, and use gpiod_line_request_wait_edge_events(). This puts your C thread to sleep until the physical button is pressed, yielding near-zero CPU usage.
  • To Add Analog Sensors: The Raspberry Pi has no native ADC. If your C project needs to read a potentiometer or thermistor, do not attempt to bit-bang an RC circuit. Wire an MCP3008 or ADS1115 ADC via SPI/I2C and use the libiio or linux/spi/spidev.h headers to read the digital values directly into your C structs.

For deeper kernel-level GPIO documentation, refer to the official libgpiod kernel repository and the Raspberry Pi hardware documentation regarding the RP1 southbridge architecture.