Raspberry Pi GPIO in C: libgpiod and WiringPi for Non-Python Projects
Our Raspberry Pi GPIO beginner guide covers the Python path — RPi.GPIO and gpiozero — which is the right starting point for most people and most projects. But Python isn't always the right tool: a tight polling loop reading a rotary encoder, a project that needs to share code with an existing C/C++ codebase, or a service you want compiled into a single small binary with no interpreter startup overhead are all cases where working in C makes more sense. This guide covers the two main paths for that: libgpiod, the modern, actively maintained kernel GPIO interface, and WiringPi, the older library that's still floating around in a lot of tutorials but that you should understand the current status of before building new work on it.
Why Not Just Use /sys/class/gpio or Direct Memory Access?
Older Pi tutorials show two other approaches worth knowing about so you recognize them, even though neither is where you should start a new project. The legacy sysfs GPIO interface (writing to /sys/class/gpio/export and toggling values through files) was removed from recent kernels entirely — code written against it will not run on a current Raspberry Pi OS install. Direct register access through /dev/mem (the approach WiringPi historically used under the hood, and what libraries like pigpio's core still do for speed) works but requires root, bypasses the kernel's GPIO character device abstraction, and does nothing for you around pin conflicts if another process is also using a pin. libgpiod exists specifically to replace both of these with something safe, current, and unprivileged.
libgpiod: The Current Standard
libgpiod talks to the kernel's GPIO character device interface (/dev/gpiochipN) rather than sysfs or raw memory, which means it works cleanly with the kernel's own accounting of which process holds which line, supports events (edge-triggered interrupts) natively, and is what current Raspberry Pi OS images ship and expect. It comes in two relevant forms: a command-line toolset for quick testing (gpioget, gpioset, gpiomon) and a C API (libgpiod-dev) for actual program code, plus official bindings for C++, Python, and Rust if you want the same underlying interface from another language later.
Installing and Testing from the Command Line
sudo apt update sudo apt install gpiod libgpiod-dev # List available GPIO chips and lines gpiodetect gpioinfo gpiochip0 # Read a pin gpioget gpiochip0 17 # Set a pin high gpioset gpiochip0 17=1 # Watch a pin for edge events (button press, etc.) gpiomon gpiochip0 17On a Raspberry Pi 5, note that the GPIO chip numbering changed from earlier boards due to the new RP1 southbridge chip handling I/O — run gpiodetect first on any new board rather than assuming gpiochip0 maps to the same lines it did on a Pi 4.
Reading and Writing GPIO in C
#include <gpiod.h> #include <stdio.h> #include <unistd.h> int main() { struct gpiod_chip *chip = gpiod_chip_open_by_name("gpiochip0"); struct gpiod_line *line = gpiod_chip_get_line(chip, 17); // Request as output, initial value low gpiod_line_request_output(line, "gpio-c-demo", 0); for (int i = 0; i < 5; i++) { gpiod_line_set_value(line, 1); usleep(500000); gpiod_line_set_value(line, 0); usleep(500000); } gpiod_line_release(line); gpiod_chip_close(chip); return 0; }Compile with gcc gpio_demo.c -o gpio_demo -lgpiod. Note that this example uses the libgpiod v1 API, which is what's packaged on most current Raspberry Pi OS releases; libgpiod v2 (bindings-compatible but with a reworked C API around "line requests" and "line settings" structs) is rolling out in newer distro packages — check pkg-config --modversion libgpiod before copying example code from the internet, since v1 and v2 code is not interchangeable without changes.
Reading an Edge Event (Interrupt-Style Input)
struct gpiod_line_event event; gpiod_line_request_falling_edge_events(line, "gpio-c-demo"); while (1) { if (gpiod_line_event_wait(line, NULL) > 0) { gpiod_line_event_read(line, &event); printf("Falling edge detected\n"); } }This blocks efficiently on a kernel-level event rather than polling in a tight loop, which is the main reason to reach for edge events instead of just reading the pin value repeatedly — it uses effectively zero CPU while waiting, and catches transitions you'd otherwise risk missing between poll iterations.
WiringPi: Status and Why It's Complicated
WiringPi was, for years, the default answer to "how do I do GPIO in C on a Pi." Its original author officially deprecated it in 2019, partly over frustration with commercial derivatives, though development later resumed under community maintenance and it has since been updated to run on the Pi 4 and 5. The practical issue for a new project isn't that it doesn't work — it's that its future maintenance is less certain than libgpiod, which is a standard part of the Linux kernel's own GPIO tooling and maintained as such. If you're maintaining old code that already uses WiringPi, there's no urgent need to rip it out. If you're starting something new, libgpiod is the better foundation.
libgpiodWiringPi Maintenance statusActive, part of the mainline kernel GPIO toolingCommunity-maintained after original deprecation; less certain long-term footing Access methodKernel GPIO character device (/dev/gpiochipN)Direct /dev/mem register access (plus a sysfs fallback historically) Root required?No, if the user is in the gpio groupYes, for direct memory access Pi 5 supportYes, natively — this is the interface the Pi 5's RP1 chip is designed aroundYes, but added after the fact and less battle-tested than on earlier boards Pin numberingUses BCM/chip-line numbering; wraps cleanly with gpioinfo to checkHas its own "wiringPi pin" numbering scheme distinct from BCM, a frequent source of confusionPermissions: Avoiding sudo for Every Run
A common early frustration is needing sudo to run any GPIO program. Fix it properly instead of reaching for root by default:
# Check your user is in the gpio group (usually already true on Raspberry Pi OS) groups $USER # If not, add it and re-login sudo usermod -aG gpio $USERRunning GPIO code as root when it isn't necessary is bad practice on any Linux system, and it's worth fixing this at setup time rather than habitually prefixing every command with sudo.
When to Actually Reach for C
Use CaseBetter Fit Simple sensor read, occasional GPIO toggle, prototypingPython (gpiozero/RPi.GPIO) — faster to write, easier to debug, plenty fast enough Tight polling loop for a rotary encoder or high-frequency signalC with libgpiod — lower and more consistent latency than the Python interpreter can guarantee Integrating GPIO control into an existing C/C++ application or daemonC with libgpiod — no need to shell out to or IPC with a separate Python process A long-running background service on constrained hardware (Pi Zero)C — much smaller memory and startup footprint than a Python interpreterFor most maker projects, Python remains the right first choice — it's faster to iterate in and the performance difference rarely matters for blinking an LED or polling a temperature sensor once a second. But knowing the C path exists, and knowing to reach for libgpiod rather than the sysfs interface or an under-maintained legacy library, matters the moment your project's timing requirements or integration needs outgrow what a Python interpreter can comfortably guarantee.
Related Guides
- Getting Started with ROS2 on Raspberry Pi for Robotics
- How to Install Klipper on Any 3D Printer: Complete Setup Guide
- Raspberry Pi: Complete Headless Setup Guide (No Monitor Needed)
- Raspberry Pi: Headless OS Setup
- How to Control GPIO Pins on Raspberry Pi with Python
- Automated Plant Watering System with Raspberry Pi