Skip to content

High-Speed GPIO Control: C++ Direct Register Access vs libgpiod

When developing embedded applications, robotics, or hardware interfaces on the Raspberry Pi, controlling General Purpose Input/Output (GPIO) pins is a fundamental requirement. However, different control methods yield vastly different performance, code safety, and portability.

This guide contrasts the modern standard libgpiod library with bare-metal Direct Register Access (via memory-mapped I/O), showing you how to choose the right tool and write high-speed, robust C++ code.


1. The Evolution of Raspberry Pi GPIO APIs

Over the years, the way Linux (and consequently Raspberry Pi OS) exposes GPIO to user-space has evolved:

  1. Sysfs Interface (Deprecated)
  ┌────────────────────────────────────────────────────────┐
  │ /sys/class/gpio (File-based, slow, deprecated)         │
  └────────────────────────────────────────────────────────┘

  2. libgpiod Character Device (Modern Linux Standard)
  ┌────────────────────────────────────────────────────────┐
  │ /dev/gpiochipN (Type-safe, event-driven, transaction)  │
  └────────────────────────────────────────────────────────┘

  3. Direct Register Access (Low-level Hack)
  ┌────────────────────────────────────────────────────────┐
  │ /dev/gpiomem (Bypasses OS, direct register mapping)   │
  └────────────────────────────────────────────────────────┘
  • Sysfs (/sys/class/gpio): The classic file-based interface. Now fully deprecated because writing strings to virtual files is extremely slow and lacks safety.
  • libgpiod: The modern standard. It operates on /dev/gpiochipN character devices, offering proper access control, bulk operations, and event monitoring.
  • Direct Register Access (/dev/gpiomem or /dev/mem): Bypasses the Linux kernel entirely by mapping the CPU's physical GPIO registers directly into your process's virtual address space.

2. The Modern Standard: libgpiod

libgpiod is the officially supported way to interact with GPIO on modern Linux kernels. It is safe, handles pin ownership, and is portable across different SBCs (Single Board Computers).

C++ implementation using gpiod.hpp

Install the required library first:

sudo apt install libgpiod-dev gpiod -y

Here is a C++ program to toggle a GPIO pin using libgpiod:

#include <gpiod.hpp>
#include <iostream>
#include <chrono>
#include <thread>

int main() {
    // GPIO 18 (BCM layout)
    unsigned int pin_num = 18;

    // Find the primary GPIO chip (typically gpiochip0 or gpiochip4 on Pi 5)
    std::string chip_name = "gpiochip4"; // Adjust based on your model

    try {
        gpiod::chip chip(chip_name);
        gpiod::line line = chip.get_line(pin_num);

        // Request the line as output
        line.request({
            "gpiod_toggle_demo",
            gpiod::line_request::DIRECTION_OUTPUT,
            0
        });

        std::cout << "Toggling Pin " << pin_num << " using libgpiod...\n";

        // Loop to toggle pin
        for (int i = 0; i < 100000; ++i) {
            line.set_value(1);
            line.set_value(0);
        }

    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }

    return 0;
}
  • Pros: Exception-safe, multi-thread friendly, handles pin multiplexing, and works out-of-the-box on Raspberry Pi 5.
  • Cons: Toggling speed is throttled by system call overhead (typically limited to ~100 kHz - 400 kHz).

3. The Performance Limit: Direct Register Access (MMAP)

For ultra-high-frequency applications (e.g. driving custom bitbang protocols, software-defined PWM, or high-speed logic analyzers), system call overhead is too high.

By mapping /dev/gpiomem into memory, you can write directly to CPU registers, achieving speeds of several megahertz (MHz).

[!WARNING] Hardware Dependency Warning: Register addresses and layouts differ significantly between Raspberry Pi models: - Raspberry Pi 4 (BCM2711): Peripheral base address is 0xFE000000. GPIO registers start at offset 0x200000. - Raspberry Pi 5 (RP1 chip): The Pi 5 moves GPIO management to the RP1 southbridge chip. Direct /dev/gpiomem access requires mapping RP1 peripheral space (0x1f000d0000), which is structurally different.

C++ implementation for Raspberry Pi 4 (BCM2711)

#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>

// BCM2711 (Pi 4) GPIO Base
#define BCM2711_PERI_BASE 0xFE000000
#define GPIO_BASE (BCM2711_PERI_BASE + 0x200000)
#define BLOCK_SIZE (4 * 1024)

// GPIO Register offsets
struct GpioRegisters {
    uint32_t GPFSEL[6]; // Function Select
    uint32_t Reserved0;
    uint32_t GPSET[2];  // Pin Output Set
    uint32_t Reserved1;
    uint32_t GPCLR[2];  // Pin Output Clear
};

volatile GpioRegisters* gpio = nullptr;

// Set pin as output
void setup_output(int pin) {
    int reg = pin / 10;
    int shift = (pin % 10) * 3;
    // Clear function bits
    gpio->GPFSEL[reg] &= ~(7 << shift);
    // Set function bits to 001 (Output)
    gpio->GPFSEL[reg] |= (1 << shift);
}

// Set pin HIGH
inline void set_high(int pin) {
    gpio->GPSET[pin / 32] = (1 << (pin % 32));
}

// Set pin LOW
inline void set_low(int pin) {
    gpio->GPCLR[pin / 32] = (1 << (pin % 32));
}

int main() {
    int mem_fd = open("/dev/gpiomem", O_RDWR | O_SYNC);
    if (mem_fd < 0) {
        std::cerr << "Failed to open /dev/gpiomem. Need sudo privileges.\n";
        return 1;
    }

    // Map GPIO registers
    void* gpio_map = mmap(
        nullptr,
        BLOCK_SIZE,
        PROT_READ | PROT_WRITE,
        MAP_SHARED,
        mem_fd,
        GPIO_BASE
    );

    close(mem_fd);

    if (gpio_map == MAP_FAILED) {
        std::cerr << "mmap failed\n";
        return 1;
    }

    gpio = reinterpret_cast<volatile GpioRegisters*>(gpio_map);
    int pin = 18;
    setup_output(pin);

    std::cout << "Toggling Pin " << pin << " via direct register access...\n";

    // Loop at maximum CPU speed
    for (int i = 0; i < 10000000; ++i) {
        set_high(pin);
        set_low(pin);
    }

    // Unmap memory
    munmap(gpio_map, BLOCK_SIZE);
    return 0;
}

4. Performance Comparison

Toggling a GPIO pin repeatedly as fast as possible yields the following typical metrics:

Method Typical Toggle Frequency CPU Overhead Safety / Portability
libgpiod (C++) ~250 kHz Moderate Excellent. Safe across standard kernels and multi-process environments.
Direct Register (MMAP) ~25 MHz - 40 MHz High (100% Core Load) Poor. Can crash the system if memory offsets are wrong; highly CPU-specific.

How to Choose:

  1. Choose libgpiod by default for standard applications (LEDs, relays, standard buttons, basic sensors) or when writing software that needs to run on multiple Raspberry Pi revisions without code changes.
  2. Choose Direct Register Access only when you need nano-second precision for high-frequency bit-banging and cannot use hardware-driven interfaces like SPI, I2C, or PWM channels.