Guide: How to use libgpiod with C
Background
I own a Raspberry Pi 5, a small computer that features GPIO (General-Purpose Input/Output) pins. These pins allow you to connect external components like LEDs or sensors. Although I had never used them before, I recently wanted to add a simple software-controlled status LED to my Pi.
I researched libraries to interact with the GPIO pins and frequently found people recommending gpiozero, a Python library built exactly for this use case. Unfortunately, it didn't work. It threw an error saying it couldn't connect to the GPIO chip. After looking the error up online, I discovered this is a known issue: gpiozero doesn't yet support the Raspberry Pi 5.
So, I looked for alternatives and found libgpiod, the official library for interacting with the Linux GPIO Character Device. Originally written in C, it was later ported in v2 to C++, Python, GObject, and Rust. Since I didn't know about those other language bindings at the time, I decided to try using it with C.
It was not a straightforward experience. The official documentation reads more like an API reference than a guide. It simply lists each function with a brief description, leaving you to figure out the correct order and context for calling them yourself. In this post, I want to save you that headache and share exactly how to use libgpiod in C.
Concepts
libgpiod v2 is highly modular, this results in many structs you need to create and connect in order to get a result. Here is every concept you need to know to get started.
- Chip: The physical port you are talking to
- Setting: Contains configuration for only one pin
- Line Config: Maps settings to specific line offsets
- Request: The actual lock/handle you use to control the pin at runtime
The acutal Guide
0. Prerequisites
Of course, you'll need a computer with a GPIO chip onboard—any Single Board Computer (SBC) running Linux will do. To verify that your GPIO chips are recognized, look in /dev/ for gpiochip character devices (e.g., /dev/gpiochip4).
On a Raspberry Pi, and likely other SBCs, you will probably find multiple gpiochip devices. This is because modern boards often have dedicated chips handling internal hardware, like the power button or the Ethernet controller, separate from the main user-accessible GPIO pins. On my Raspberry Pi 5, I see five chips.
To figure out which chip corresponds to the physical header pins, install the gpiod package. This contains the library along with handy command-line tools. One of these tools is gpioinfo. Note that you must run this as root, or be part of the gpio group. Running gpioinfo will list every chip, its lines, and a description, making it easy to spot the right one. For me, it was /dev/gpiochip0.
Here is what the output looks like on a Raspberry Pi 5:
gpiochip0 - 54 lines:
line 0: "ID_SDA" input
line 1: "ID_SCL" input
line 2: "GPIO2" input
line 3: "GPIO3" input
line 4: "GPIO4" input
line 5: "GPIO5" input
[...]
Note: When compiling your
libgpiodprograms, don't forget to link the library using the-lgpiodflag (e.g.,gcc main.c -o led -lgpiod).
1. Opening the chip
This is the easy part of the setup. Use gpiod_chip_open() to create a gpiod_chip struct, which contains a file descriptor and the path to the device. On success, the function returns a pointer to the struct; on failure, it returns NULL. To close the chip later, use gpiod_chip_close().
struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0");
if (!chip) {
fputs("Failed to open GPIO chip\n", stderr);
exit(1);
}
// Doing stuff with the chip...
gpiod_chip_close(chip);
2. Creating settings
If you have worked with microcontrollers, you know that you first need to configure the GPIO pins. Most libraries, like the Arduino one, allow you to do this in a single line. But remember, this is C—of course we can't do it in one line.
To configure a pin, you first need to create a gpiod_line_settings struct. This holds the configuration properties for a GPIO pin.
Caution:
gpiod_line_settingsDO NOT contain any mappings to physical line offsets, just properties for a pin.
To create one, use gpiod_line_settings_new(), which returns a pointer to the struct (or NULL on failure).
There are many functions to set and get the properties of the line settings. I recommend checking out the documentation for all available settings: https://libgpiod.readthedocs.io/en/master/core_line_settings.html. You can find the definitions for the enums used here: https://libgpiod.readthedocs.io/en/master/core_line_defs.html.
All of the set functions return 0 on success and -1 on failure.
Here is an example that creates a line settings struct for an active-high, pull-down, output pin:
struct gpiod_line_settings *settings = gpiod_line_settings_new();
if (!settings) {
fputs("Failed to create line settings\n", stderr);
gpiod_chip_close(chip);
exit(1);
}
gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT);
gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
Note: I omitted error checking for the
gpiod_line_settings_set_*functions because, according to the source, they will only return -1 if you supply values that are not part of the enum.
3. Creating a line configuration
Now that we have settings for a pin, we need to map these settings to specific pin offsets. A line configuration contains exactly this information. To create one, use gpiod_line_config_new(), which will return a pointer to a gpiod_line_config on success and NULL on failure.
You can now use the gpiod_line_config_add_line_settings() function to add one line config to multiple pins. Here is the definition:
int gpiod_line_config_add_line_settings(
struct gpiod_line_config *config,
const unsigned int *offsets,
size_t num_offsets,
struct gpiod_line_settings *settings
);
- config: The line config you want to add the settings to
- offsets: An array of line offsets where you want to apply the settings
- num_offsets: Number of offsets provided in your
offsetsarray - settings: Your line settings from step 2
If you only want to apply one setting to one pin, you don't need to create a new array or variable for this. Since the function expects a pointer, you can use a compound literal to pass the value inline. You can do this by casting the value to unsigned int[], which creates a temporary array containing your pin offset on the fly.
This example assigns settings to GPIO pin 42:
struct gpiod_line_config *line_config = gpiod_line_config_new();
if (!line_config) {
fputs("Failed to create line config\n", stderr);
gpiod_line_settings_free(settings);
gpiod_chip_close(chip);
exit(1);
}
if (gpiod_line_config_add_line_settings(line_config, (unsigned int[]){ 42 }, 1, settings) == -1) {
fputs("Failed to assign settings to line config\n", stderr);
gpiod_line_config_free(line_config);
gpiod_line_settings_free(settings);
gpiod_chip_close(chip);
exit(1);
}
4. Making a request
Now we have prepared a line config that we need to apply. To do this, we need to make a request to the kernel with our line config using gpiod_chip_request_lines(), which returns a pointer to a gpiod_line_request or NULL on failure. On success, it will apply our initial line config to the GPIO pins.
We can also give our program a name that we supply together with our request. This name will show up, for example, in gpioinfo. To do this, you need to create yet another struct called gpiod_request_config. It works like settings: you create one using gpiod_request_config_new() and set the name with gpiod_request_config_set_consumer().
Here is an example that also sets a consumer:
struct gpiod_request_config *req_cfg = gpiod_request_config_new();
if (!req_cfg)
fputs("Failed to create a request config. Will continue without one\n", stderr);
struct gpiod_line_request *request = gpiod_chip_request_lines(chip, req_cfg, line_config);
if (!request) {
fputs("Failed to create line request\n", stderr);
if (req_cfg) gpiod_request_config_free(req_cfg);
gpiod_line_config_free(line_config);
gpiod_line_settings_free(settings);
gpiod_chip_close(chip);
exit(1);
}
Note: You don't need to specify a request configuration; you can also leave the field
NULL.
Warning: Remember to save the
gpiod_line_request; it will be needed in the next steps.
Caution: To free a
gpiod_line_request, you need to usegpiod_line_request_release.
5. Runtime control
To control the values of the GPIO lines, you need the gpiod_line_request from the previous step. Now you can set the value of a pin in ONE LINE OF CODE (I wouldn't expect that from C). To do this, call gpiod_line_request_get_value() or gpiod_line_request_set_value() with a request, an offset, and a value if you want to set the value.
Here I would recommend reading the documentation for line requests: https://libgpiod.readthedocs.io/en/master/core_line_request.html
Note: You can only set or get the value of a pin at runtime. Setting other properties like the direction requires reconfiguring the line.
6. Edge detection
So far we have only read and written the static value of a pin. Edge detection lets you react to changes on a line: a rising edge (low to high) or a falling edge (high to low). This is useful for buttons, encoders, interrupts, and any signal where you care about transitions rather than the current level.
Edge detection requires three things in libgpiod:
- Enable edge detection on the line settings before requesting the line
- Create an edge event buffer to hold the events the kernel queues up
- Wait for and read events from the request at runtime
Enabling edge detection
Edge detection is configured through the line settings object from step 2 using gpiod_line_settings_set_edge_detection(). It takes one of the values from the gpiod_line_edge enum.
gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
Note: Edge detection only works on input lines. Make sure the direction is set to
GPIOD_LINE_DIRECTION_INPUTbefore requesting the line, otherwise the kernel will reject the request.
After this, you create the line config and request the lines exactly like in steps 3 and 4. The only difference is that the settings now carry edge detection, so the kernel will start queuing events for that line.
Creating an edge event buffer
When the kernel detects an edge, it stores the event in an internal kernel buffer. To pull those events into your program, libgpiod uses a user space buffer object. You create one with gpiod_edge_event_buffer_new():
struct gpiod_edge_event_buffer *event_buffer = gpiod_edge_event_buffer_new(64);
if (!event_buffer) {
fputs("Failed to create edge event buffer\n", stderr);
/* ... free everything allocated so far ... */
exit(1);
}
The argument is the capacity, i.e. the maximum number of events the buffer can hold in one read. If you pass 0, it defaults to 64; if you pass more than 1024, it is clamped to 1024.
Note: The user space buffer is independent of the kernel buffer. Since the user space buffer is filled from the kernel buffer, there is no benefit in making the user space buffer larger than the kernel buffer. The default kernel buffer size per request is
16 * num_lines.
Waiting for and reading events
Once the line is requested with edge detection enabled, you wait for events with gpiod_line_request_wait_edge_events():
int gpiod_line_request_wait_edge_events(
struct gpiod_line_request *request,
int64_t timeout_ns
);
- request: The line request from step 4
- timeout_ns: Wait time in nanoseconds.
0returns immediately, a negative value blocks indefinitely until an event is available
It returns 1 if an event is pending, 0 on timeout, and -1 on error.
After the wait succeeds, read the events into your buffer with gpiod_line_request_read_edge_events():
int gpiod_line_request_read_edge_events(
struct gpiod_line_request *request,
struct gpiod_edge_event_buffer *buffer,
size_t max_events
);
- request: The line request
- buffer: The edge event buffer, sized to hold at least
max_events - max_events: Maximum number of events to read
On success it returns the number of events read, on failure -1.
Warning:
gpiod_line_request_read_edge_events()blocks if no event was queued for the line request. Always callgpiod_line_request_wait_edge_events()first if you want non-blocking behavior, or pass a timeout to the wait function.
Caution: Any existing events in the buffer are overwritten on each read. This is not an append operation, so process the events before reading again.
Inspecting events
Each event in the buffer can be retrieved with gpiod_edge_event_buffer_get_event(), and you can find out how many events were read with gpiod_edge_event_buffer_get_num_events(). From an event you can query:
gpiod_edge_event_get_event_type(): returnsGPIOD_EDGE_EVENT_RISING_EDGEorGPIOD_EDGE_EVENT_FALLING_EDGEgpiod_edge_event_get_timestamp_ns(): returns the timestamp in nanoseconds (the source clock depends on the line'sevent_clocksetting)gpiod_edge_event_get_line_offset(): returns the offset of the line that triggered the eventgpiod_edge_event_get_global_seqno(): sequence number across all lines in the requestgpiod_edge_event_get_line_seqno(): sequence number for this specific line
Debouncing
Mechanical inputs like buttons bounce, producing many rapid edges on a single press. You can ask the kernel to filter these out by setting a debounce period on the line settings with gpiod_line_settings_set_debounce_period_us(), which takes the period in microseconds:
gpiod_line_settings_set_debounce_period_us(settings, 10000); /* 10 ms */
When a debounce period is set, the kernel suppresses edges that occur within that window after a detected edge, so your program only sees one clean event per physical press.
Note: Debounce only affects edge events. Reading the raw value of the line with
gpiod_line_request_get_value()still returns the instantaneous logical level.
Examples
On this Github Gist I have written some examples using libgpiod