How does Controlling GPIO Pins with libgpiod work in Linux-Free Linux Device Drivers Course

Controlling GPIO Pins with libgpiod

Reading, writing, and catching interrupts on GPIO lines with the modern API, free linux kernel development course

What You Will Learn

  • Why the old /sys/class/gpio interface was retired and what replaced it
  • How to request, read, and write a GPIO line from the command line and from C
  • How to wait for a GPIO edge event instead of polling in a busy loop
  • How to build and run a small original demo that reacts to a button press

Prerequisites

  • Completion of the previous lecture on user-space device access and gpiolib
  • A board with libgpiod v2 tools and headers available (gpiod package, or built from source)
  • A GPIO line you can safely toggle for testing (an onboard LED or a jumper-wired button)

Why Sysfs GPIO Went Away

Older material — including plenty of still-circulating tutorials — teaches GPIO control through plain files under /sys/class/gpio: write a number to export, write out to a direction file, write 0 or 1 to a value file. That interface worked, but it had real problems: numbering was global and unstable across boards, there was no way to request several lines atomically, and nothing stopped two programs from fighting over the same pin. The kernel community replaced it with a character-device interface (/dev/gpiochipN) backed by a proper ioctl API, and the sysfs interface was marked deprecated; on many current kernel configurations it’s compiled out entirely. The libgpiod project provides the standard user-space tools and C/C++/Python bindings for the new interface, and it’s what any current guide — including this one — should be teaching.

AspectLegacy sysfs GPIO (deprecated)Modern libgpiod / character device
Access path/sys/class/gpio/gpioN/* files/dev/gpiochipN ioctl API
Line ownershipNot enforced reliablyExplicit request/release, kernel-enforced
Multi-line operationsNot possibleRequest and toggle several lines atomically
Edge/interrupt waitingedge file + poll()Edge event descriptor + poll() or blocking read
Current statusDeprecated, often disabledStandard, actively maintained

Command-Line Control

Before writing any code, it’s worth getting comfortable with the command-line tools, since they map almost directly onto the C API calls used later. Assume gpiochip0 line 5 drives an LED, and line 6 reads a push button.

# Read the current state of a line
$ gpioget gpiochip0 5
0

# Drive a line high, then low
$ gpioset gpiochip0 5=1
$ gpioset gpiochip0 5=0

# Watch a line for edge events (Ctrl+C to stop)
$ gpiomon --edge=both gpiochip0 6
event:  RISING EDGE  offset: 6  timestamp: [   12.345678123]
event:  FALLING EDGE offset: 6  timestamp: [   12.567891234]

gpioset requests the line, drives it, and releases it as soon as the command exits — for anything that needs to hold a line for longer, or needs to react to events, you want a small program using the library directly.

Request/Read/Write/Release Lifecycle

open(“/dev/gpiochip0”) | gpiod_chip_request_lines( offsets, direction ) | +–> gpiod_line_request_get_value() (read) | +–> gpiod_line_request_set_value() (write) | +–> wait for edge event (interrupt-style, no busy loop) | gpiod_line_request_release()

Reading and Writing a Line from C

This example, ep_gpio_blink, is an original demo: it requests one output line and blinks it five times. It uses the libgpiod v2 API, which is the current stable API shipped with recent libgpiod releases.

// ep_gpio_blink.c
#include <stdio.h>
#include <unistd.h>
#include <gpiod.h>

#define CHIP_PATH "/dev/gpiochip0"
#define LED_OFFSET 5

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line_settings *settings;
    struct gpiod_line_config *line_cfg;
    struct gpiod_request_config *req_cfg;
    struct gpiod_line_request *request;
    unsigned int offset = LED_OFFSET;

    chip = gpiod_chip_open(CHIP_PATH);
    if (!chip) { perror("open chip"); return 1; }

    settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT);

    line_cfg = gpiod_line_config_new();
    gpiod_line_config_add_line_settings(line_cfg, &offset, 1, settings);

    req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_cfg, "ep_gpio_blink");

    request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
    if (!request) { perror("request line"); return 1; }

    for (int i = 0; i < 5; i++) {
        gpiod_line_request_set_value(request, LED_OFFSET, GPIOD_LINE_VALUE_ACTIVE);
        printf("LED on\n");
        sleep(1);
        gpiod_line_request_set_value(request, LED_OFFSET, GPIOD_LINE_VALUE_INACTIVE);
        printf("LED off\n");
        sleep(1);
    }

    gpiod_line_request_release(request);
    gpiod_line_config_free(line_cfg);
    gpiod_line_settings_free(settings);
    gpiod_request_config_free(req_cfg);
    gpiod_chip_close(chip);
    return 0;
}

Build and run it:

$ gcc ep_gpio_blink.c -o ep_gpio_blink $(pkg-config --cflags --libs libgpiod)
$ sudo ./ep_gpio_blink
LED on
LED off
LED on
LED off
LED on
LED off
LED on
LED off
LED on
LED off

Waiting for Interrupts Instead of Polling

Busy-looping to catch a button press wastes CPU cycles and battery on any real device. The modern API solves this the same way the old sysfs edge file plus poll() did conceptually — you configure the line for edge detection and then block until the kernel tells you something happened — but through a cleaner, race-free request object instead of a loosely typed sysfs file. This second original demo, ep_gpio_watch, requests an input line configured for edge detection and blocks on it using poll() against the request’s file descriptor.

// ep_gpio_watch.c
#include <stdio.h>
#include <poll.h>
#include <gpiod.h>

#define CHIP_PATH "/dev/gpiochip0"
#define BUTTON_OFFSET 6

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line_settings *settings;
    struct gpiod_line_config *line_cfg;
    struct gpiod_request_config *req_cfg;
    struct gpiod_line_request *request;
    struct gpiod_edge_event_buffer *buf;
    unsigned int offset = BUTTON_OFFSET;
    struct pollfd pfd;

    chip = gpiod_chip_open(CHIP_PATH);
    if (!chip) { perror("open chip"); return 1; }

    settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
    gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);

    line_cfg = gpiod_line_config_new();
    gpiod_line_config_add_line_settings(line_cfg, &offset, 1, settings);

    req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_cfg, "ep_gpio_watch");

    request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
    if (!request) { perror("request line"); return 1; }

    buf = gpiod_edge_event_buffer_new(1);
    pfd.fd = gpiod_line_request_get_fd(request);
    pfd.events = POLLIN;

    printf("Waiting for button events...\n");
    while (1) {
        int ret = poll(&pfd, 1, -1);
        if (ret > 0) {
            gpiod_line_request_read_edge_events(request, buf, 1);
            struct gpiod_edge_event *event = gpiod_edge_event_buffer_get_event(buf, 0);
            enum gpiod_edge_event_type type = gpiod_edge_event_get_event_type(event);
            printf("Button %s\n",
                type == GPIOD_EDGE_EVENT_RISING_EDGE ? "released" : "pressed");
        }
    }
    return 0;
}
$ gcc ep_gpio_watch.c -o ep_gpio_watch $(pkg-config --cflags --libs libgpiod)
$ sudo ./ep_gpio_watch
Waiting for button events...
Button pressed
Button released
Button pressed
Button released

Compare this to the old model: instead of opening a sysfs value file and polling POLLPRI on it, you request the line with edge detection enabled up front, then poll() the request’s own file descriptor and read structured edge-event records off it — no string parsing, no risk of another process silently unexporting the pin from under you.

Common Mistakes

  • Copy-pasting sysfs GPIO instructions from an old tutorial and finding /sys/class/gpio doesn’t exist
  • Forgetting to release a line request, leaving the line claimed after the program exits abnormally
  • Polling a value file in a tight loop instead of using edge-event notification
  • Requesting a line that’s already claimed by a kernel driver or another process, then wondering why the request fails silently

Best Practices

  • Use gpioinfo to confirm a line is free before requesting it
  • Always release line requests, including on error paths, so lines aren’t left claimed
  • Prefer edge-event waiting over polling a value in a loop for anything battery- or CPU-sensitive
  • Set a consumer name on every request so gpioinfo shows which program owns a line during debugging

Summary

The sysfs GPIO interface that older references teach is deprecated and often unavailable on current kernels. The character-device interface accessed through libgpiod is the standard replacement: it gives you atomic multi-line requests, kernel-enforced ownership, and race-free edge-event notification through a normal pollable file descriptor. With gpioget/gpioset/gpiomon for quick command-line work and the C API for real programs, you now have everything needed to read, drive, and react to GPIO lines the way a current free linux kernel development course should teach it.

FAQ

Do I need root to use gpioget/gpioset?

Usually yes, unless your distribution has udev rules granting a specific group access to /dev/gpiochipN.

Can I request more than one GPIO line at once?

Yes — gpiod_line_config_add_line_settings() accepts an array of offsets, and the resulting request can read/write them atomically.

What happens if two programs request the same line?

The second request fails; the kernel enforces exclusive ownership per line, unlike the old sysfs interface.

Is polling the request file descriptor the only way to catch edge events?

No — you can also call gpiod_line_request_read_edge_events() with a blocking read directly, without a separate poll() call, if you only watch one request.

Where can I check which libgpiod version my board has?

Run gpiodetect --version or check pkg-config --modversion libgpiod; the v2 API shown here needs libgpiod 2.0 or later.

Continue the Free Linux Kernel Development Course

Next Lecture Course Index