If you are writing production-grade C++ for hardware interfacing on a Raspberry Pi in 2026, the definitive stack is libgpiod v2 targeting the RP1 southbridge on the Raspberry Pi 5. Legacy libraries like WiringPi are dead, sysfs is deprecated, and pigpio is no longer maintained for modern Pi OS kernels. This guide provides a complete, decision-forward blueprint for building a 12V DC motor controller with hardware debouncing using modern C++ RAII patterns and the libgpiod v2 C API.
The 2026 C++ Raspberry Pi GPIO Stack: Why libgpiod Wins
Choosing a GPIO library for embedded C++ on the Pi used to be a fragmented mess. Today, the kernel-level character device API (exposed via libgpiod) is the only path forward for deterministic, secure hardware control. Use the decision tree below to select your stack.
| Library / Method | Status in 2026 | C++ Compatibility | Verdict |
|---|---|---|---|
| sysfs (/sys/class/gpio) | Deprecated by Kernel | Standard file I/O | Reject: High latency, removed in newer kernels. |
| WiringPi | Abandoned (2019) | Native C/C++ | Reject: Fails on Pi 4/5 hardware. |
| pigpio | Legacy / Unmaintained | C API / C++ wrappers | Reject: Relies on deprecated /dev/mem access. |
| RPi.GPIO | Active | Python Only | Reject: Not applicable for C++ builds. |
| libgpiod v2 | Official Kernel Standard | C API (wrap in C++ RAII) | SELECT: Secure, fast, future-proof. |
Default Recommendation: Install libgpiod-dev and wrap the v2 C API in C++ classes using std::unique_ptr for automatic resource cleanup.
Hardware BOM and Pin Mapping for the Pi 5 Motor Controller
The Raspberry Pi 5 routes its GPIO through the RP1 southbridge chip. This means the logical GPIO numbers (BCM numbering) remain the same as older Pis, but the underlying hardware chip identifier changes from gpiochip0 to gpiochip4. We are using a Texas Instruments DRV8871 brush motor driver, which accepts 3.3V logic directly from the RP1.
Spec Sheet & Parts List
| Component | Exact Variant / Part Number | Est. Cost (2026) | Role |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | $80.00 | Host running C++ binary |
| Thermal Mgmt | Official Pi 5 Active Cooler | $5.00 | Prevents RP1 thermal throttling |
| Motor Driver | Texas Instruments DRV8871 (Adafruit 3190) | $6.50 | H-Bridge, 3.3V logic compatible |
| Encoder | CUI Devices PEC12R-4215F-S0024 | $3.20 | 24-pulse rotary encoder for RPM |
| Power Supply | Mean Well LRS-35-12 (12V 3A) | $18.00 | Dedicated 12V for motor |
Pin Mapping Table
| Pi 5 GPIO (BCM) | Physical Pin | Destination | Function |
|---|---|---|---|
| GPIO 18 | 12 | DRV8871 IN1 | Motor Direction / Enable (PWM capable) |
| GPIO 19 | 35 | DRV8871 IN2 | Motor Direction / Brake |
| GPIO 16 | 36 | PEC12R Pin A | Encoder Quadrature A (Pull-up enabled) |
| GPIO 26 | 37 | PEC12R Pin B | Encoder Quadrature B (Pull-up enabled) |
| GND | 39 | Common Ground | Must tie Pi GND, DRV8871 GND, and 12V PSU GND |
Complete C++ Implementation with libgpiod v2 RAII
The libgpiod v2 API requires allocating line settings, line configs, and requests. In C, this leads to massive memory leak risks. Below is a complete, compilable C++ implementation that uses std::unique_ptr with custom deleters to guarantee resource cleanup, even if an exception is thrown.
#include <iostream>
#include <gpiod.h>
#include <thread>
#include <chrono>
#include <memory>
#include <stdexcept>
// Custom RAII Deleters for libgpiod v2 C API
struct ChipDeleter { void operator()(gpiod_chip* c) const { if(c) gpiod_chip_close(c); } };
struct SettingsDeleter { void operator()(gpiod_line_settings* s) const { if(s) gpiod_line_settings_free(s); } };
struct ConfigDeleter { void operator()(gpiod_line_config* c) const { if(c) gpiod_line_config_free(c); } };
struct RequestDeleter { void operator()(gpiod_line_request* r) const { if(r) gpiod_line_request_release(r); } };
using ChipPtr = std::unique_ptr<gpiod_chip, ChipDeleter>;
using SettingsPtr = std::unique_ptr<gpiod_line_settings, SettingsDeleter>;
using ConfigPtr = std::unique_ptr<gpiod_line_config, ConfigDeleter>;
using RequestPtr = std::unique_ptr<gpiod_line_request, RequestDeleter>;
class Pi5MotorController {
private:
ChipPtr chip;
RequestPtr request;
unsigned int pin_in1 = 18;
unsigned int pin_in2 = 19;
public:
Pi5MotorController() {
// Pi 5 RP1 southbridge exposes GPIO on gpiochip4
const char* chip_path = "/dev/gpiochip4";
chip.reset(gpiod_chip_open(chip_path));
if (!chip) {
throw std::runtime_error("Failed to open " + std::string(chip_path) + ". Is the RP1 firmware loaded?");
}
SettingsPtr settings(gpiod_line_settings_new());
gpiod_line_settings_set_direction(settings.get(), GPIOD_LINE_DIRECTION_OUTPUT);
gpiod_line_settings_set_output_value(settings.get(), GPIOD_LINE_VALUE_INACTIVE);
ConfigPtr line_cfg(gpiod_line_config_new());
unsigned int offsets[] = {pin_in1, pin_in2};
gpiod_line_config_add_line_settings(line_cfg.get(), offsets, 2, settings.get());
request.reset(gpiod_chip_request_lines(chip.get(), NULL, line_cfg.get()));
if (!request) {
throw std::runtime_error("gpiod_chip_request_lines failed. Check udev permissions.");
}
}
void spinForward() {
gpiod_line_request_set_value(request.get(), pin_in1, GPIOD_LINE_VALUE_ACTIVE);
gpiod_line_request_set_value(request.get(), pin_in2, GPIOD_LINE_VALUE_INACTIVE);
}
void brake() {
gpiod_line_request_set_value(request.get(), pin_in1, GPIOD_LINE_VALUE_INACTIVE);
gpiod_line_request_set_value(request.get(), pin_in2, GPIOD_LINE_VALUE_INACTIVE);
}
};
int main() {
try {
Pi5MotorController motor;
std::cout << "Spinning motor for 3 seconds..." << std::endl;
motor.spinForward();
std::this_thread::sleep_for(std::chrono::seconds(3));
motor.brake();
std::cout << "Motor stopped. Resources auto-released via RAII." << std::endl;
} catch (const std::exception& e) {
std::cerr << "Hardware Fault: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Compile with: g++ -std=c++17 -O2 motor_control.cpp -o motor_control -lgpiod
Debugging "Permission Denied" and Chip Request Failures
When transitioning from Python to C++ on the Pi, the character device API throws strict OS-level errors. If your binary crashes on startup, look for these exact strings in your terminal output.
Error 1: gpiod_chip_request_lines: Permission denied
What it means: Your C++ binary successfully found the RP1 chip, but the Linux kernel blocked the user from claiming the GPIO lines because they lack the required group privileges.
- Cause A (Most Likely): You are running the binary as a standard user, and the
/dev/gpiochip4device node is restricted to thegpioordialoutgroup. - Cause B: Another process (like a lingering Python script or
pigpioddaemon) already holds an exclusive lock on GPIO 18 or 19.
Error 2: Failed to open /dev/gpiochip4. No such file or directory
What it means: The OS cannot find the RP1 GPIO controller mapping.
- Cause A: You copied code from a Pi 4 tutorial that hardcodes
/dev/gpiochip0. The Pi 5 usesgpiochip4. - Cause B: The RP1 firmware failed to load during boot, usually due to an outdated EEPROM or a corrupted
/lib/firmware/raspberrypi/rp1.binfile.
- Verify the Chip Number: Run
ls /dev/gpiochip*. If you only seegpiochip0, you are on a Pi 4 or older. If you seegpiochip4, update the path in your C++ code. - Check Group Memberships: Run
groups. Ifgpioordialoutis missing, runsudo usermod -aG gpio $USER, then log out and log back in. - Verify RP1 Firmware: Run
sudo dmesg | grep rp1. You should seerp1 1f00108000.pci: RP1 firmware loaded. If not, runsudo rpi-eeprom-update -aand reboot.
Scaling the Build: Simplify or Extend with I2C
Once the core motor control is stable, you will likely need to adapt the project for your specific enclosure or data-logging requirements. Here is how to adjust the scope without rewriting the core RAII architecture.
How to Simplify (The Bench-Test Mode)
If you are testing on a workbench and do not have a 12V motor or DRV8871 handy, simplify the build to verify your C++ toolchain and libgpiod setup:
- Drop the DRV8871: Connect a standard 3.3V LED with a 220Ω current-limiting resistor directly to GPIO 18.
- Modify the Code: Change the
spinForward()sleep timer to a 500ms loop to create a visible blink pattern. This isolates software bugs from hardware wiring faults.
How to Extend (Adding I2C RPM Telemetry)
To read the CUI Devices PEC12R rotary encoder and display the motor's RPM, you need to extend the system without blocking the main thread.
- Add the Display: Wire an SSD1306 128x64 I2C OLED to physical pins 3 (SDA) and 5 (SCL).
- Implement Interrupts: Do not poll the encoder in a
while(true)loop; it will consume 100% of a Pi 5 core. Instead, usegpiod_line_settings_set_edge_detection(settings.get(), GPIOD_LINE_EDGE_BOTH)to configure GPIO 16 for hardware interrupts. - Use Edge Event Requests: Replace
gpiod_chip_request_lineswith an edge-event request, and usegpiod_line_request_wait_edge_events()with a timeout. This puts the thread to sleep until the physical encoder detent clicks, ensuring zero CPU usage while idle.
For deeper kernel-level documentation on edge events and character device ABI v2, consult the official libgpiod kernel repository and the Raspberry Pi 5 hardware specifications. By sticking to the v2 API and enforcing RAII memory management, your C++ binaries will remain stable across future Pi OS kernel upgrades.






