While Python dominates introductory Raspberry Pi tutorials, deterministic latency and high-frequency hardware control demand C++. If you are targeting the Raspberry Pi 5 in 2026, the legacy wiringPi and pigpio libraries are officially deprecated and incompatible with the new RP1 silicon. The modern standard for Raspberry Pi C++ GPIO interaction is lgpio, which interfaces directly with the Linux libgpiod kernel subsystem.
This guide walks through building a hardware-debounced PWM fan controller. We will cover the exact pin multiplexing for the Pi 5, provide a complete, compilable C++17 build, and debug the specific linker and runtime errors that trip up developers migrating from older Pi models.
Project Spec Sheet & Parts List
This project targets the specific hardware and software stack of the latest Raspberry Pi generation. Do not use a Pi 4 or older without adjusting the gpiochip index in the code.
| Component | Exact Variant / Specification | Notes |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB Model) | Requires active cooling; runs RP1 southbridge |
| OS Environment | Raspberry Pi OS Bookworm (64-bit) | Kernel 6.6+ required for proper lgpio mapping |
| C++ Library | lgpio (v0.2.2 or newer) | Install via sudo apt install liblgpio-dev |
| PWM Load | Noctua NF-A4x10 5V PWM Fan | Requires 25kHz PWM signal on the blue wire |
| Input Switch | Standard 6x6mm Tactile Switch | Active-low configuration |
| Passive Components | 10kΩ Resistor, 100nF Ceramic Capacitor | Forms a hardware RC debounce filter |
Pin Mapping & Hardware Wiring
The Raspberry Pi 5 routes GPIO through the RP1 chip. Physical pin numbers remain compatible with the 40-header standard, but the internal BCM numbering maps to gpiochip4 in the Linux kernel.
| BCM GPIO | Physical Pin | Function | Connection Target |
|---|---|---|---|
| 3.3V | 1 | Power | 10kΩ pull-up resistor to Button |
| GPIO 17 | 11 | Input (Pull-up) | Button Signal + 100nF Cap to GND |
| GPIO 18 | 12 | Hardware PWM0 | Fan PWM (Blue) Wire |
| 5V | 4 | Power | Fan VCC (Red) Wire |
| GND | 9 | Ground | Common Ground (Fan + Cap + Button) |
Wiring Procedure
- Build the RC Debounce Filter: Connect one leg of the tactile switch to GND. Connect the other leg to GPIO 17. Solder the 100nF ceramic capacitor in parallel with the switch (between GPIO 17 and GND). Connect the 10kΩ resistor between 3.3V and GPIO 17. This hardware filter eliminates switch bounce before it reaches the CPU, saving interrupt overhead.
- Wire the PWM Fan: Connect the fan's red wire to Physical Pin 4 (5V) and the black wire to Physical Pin 9 (GND). Connect the blue PWM control wire directly to GPIO 18. The Pi 5 GPIO pins output 3.3V logic, which is sufficient to trigger the logic-level input of standard 5V PC fans.
- Verify with a Multimeter: Before powering the Pi, use a multimeter in continuity mode to ensure no shorts exist between the 3.3V pull-up and GND. Measure the resistance across the 10kΩ resistor to confirm it is not bridged.
The C++ Build: PWM Fade with Interrupts
The following C++17 code initializes the RP1 gpiochip, claims the pins, and uses a hardware interrupt callback to toggle the fan speed between 20% and 100% duty cycle.
gpiochip4). If compiling for a Pi 4, change GPIOCHIP_HANDLE to 0.
#include <lgpio.h>
#include <iostream>
#include <thread>
#include <chrono>
#include <csignal>
#include <atomic>
// Pin Definitions (BCM Numbering)
constexpr int PIN_BUTTON = 17;
constexpr int PIN_PWM_FAN = 18;
constexpr int PWM_FREQ_HZ = 25000; // 25kHz standard for 4-pin PC fans
constexpr int GPIOCHIP_HANDLE = 4; // Pi 5 uses gpiochip4
std::atomic<bool> keep_running(true);
int current_duty = 20; // Start at 20% duty cycle
void signal_handler(int signum) {
keep_running = false;
}
// Interrupt Callback for Button Press
void button_callback(int e, lgGpioAlert_p alerts, int count, void *userdata) {
if (count > 0 && alerts[0].level == 0) { // Active low (pressed)
current_duty = (current_duty == 100) ? 20 : 100;
std::cout << "Button pressed. Toggling duty to: " << current_duty << "%\n";
}
}
int main() {
std::signal(SIGINT, signal_handler);
// 1. Open the GPIO Chip
int h = lgGpiochipOpen(GPIOCHIP_HANDLE);
if (h < 0) {
std::cerr << "Failed to open gpiochip" << GPIOCHIP_HANDLE << ". Error: " << h << "\n";
return 1;
}
// 2. Claim Pins with Error Handling
int res_btn = lgGpioClaimInput(h, 0, PIN_BUTTON, LG_GPIO_PULL_UP);
if (res_btn < 0) {
std::cerr << "Failed to claim button GPIO. Error: " << res_btn << "\n";
lgGpiochipClose(h);
return 1;
}
int res_fan = lgGpioClaimOutput(h, 0, PIN_PWM_FAN, 0);
if (res_fan < 0) {
std::cerr << "Failed to claim fan GPIO. Error: " << res_fan << "\n";
lgGpiochipClose(h);
return 1;
}
// 3. Setup Hardware PWM and Interrupts
lgTxPwm(h, PIN_PWM_FAN, PWM_FREQ_HZ, current_duty, 0, 0);
lgGpioSetAlertsFunc(h, PIN_BUTTON, button_callback, nullptr);
std::cout << "Fan controller running. Press Ctrl+C to exit.\n";
// Main loop just keeps the process alive while interrupts handle IO
while (keep_running) {
lgTxPwm(h, PIN_PWM_FAN, PWM_FREQ_HZ, current_duty, 0, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Cleanup
lgTxPwm(h, PIN_PWM_FAN, PWM_FREQ_HZ, 0, 0, 0); // Kill PWM
lgGpioFree(h, PIN_BUTTON);
lgGpioFree(h, PIN_PWM_FAN);
lgGpiochipClose(h);
std::cout << "GPIO released cleanly.\n";
return 0;
}
Compilation Command:
g++ -std=c++17 -O2 -o fan_controller main.cpp -llgpio
Debugging: When the Compiler or Kernel Fights Back
Migrating to lgpio on Bookworm introduces specific failure modes. If your build fails or the runtime throws an exception, check these exact error strings.
Error 1: /usr/bin/ld: cannot find -llgpio: No such file or directory
Ranked Causes:
- Missing Dev Package: You installed the runtime library but not the C/C++ headers. Fix:
sudo apt install liblgpio-dev. - Incorrect Linker Order: In GCC, the order of arguments matters. The
-llgpioflag must come after your source files in the compilation command.
Error 2: lgpio error -3: GPIO not allocated or Failed to claim button GPIO. Error: -3
Ranked Causes:
- Pin Already Claimed: Another process (like a lingering Python script or the
pinctrldaemon) holds the pin. RunlgpioCLI tool orpinctrl getto verify pin state, and kill competing processes. - Wrong Chip Index: You are running on a Pi 5 but the code targets
gpiochip0(the Pi 4 default). EnsureGPIOCHIP_HANDLEis set to4. - Permissions: Your user is not in the
gpiogroup. Fix:sudo usermod -aG gpio $USER, then log out and back in.
1. Verify the linker flag placement (
-llgpio at the end).2. Confirm the
gpiochip index matches your physical board (0 for Pi4, 4 for Pi5).3. Check for pin conflicts using
pinctrl allocs in the terminal.
Extending and Simplifying the Build
How to Simplify: If you lack the 100nF capacitor for hardware debouncing, you can implement a software debounce in the callback. Record std::chrono::steady_clock::now() on each interrupt trigger, and ignore subsequent triggers that occur within 50 milliseconds of the last valid edge. This trades a few CPU cycles for a simpler BOM.
How to Extend: To make this a closed-loop thermal controller, add a BME280 sensor via I2C (SDA to GPIO 2, SCL to GPIO 3). Use the Raspberry Pi I2C documentation to enable the bus, and integrate the Adafruit BME280 C++ library. Map the temperature reading (e.g., 30°C to 50°C) to a linear PWM duty cycle curve, updating the lgTxPwm function every 2 seconds.
Raspberry Pi C++ FAQ
Is WiringPi still viable for Raspberry Pi C++ projects in 2026?
No. WiringPi was officially abandoned in 2019 and relies on direct memory mapping to the BCM2835/2711 peripheral addresses. The Raspberry Pi 5 uses the RP1 southbridge, which completely changes the memory map and requires the standard Linux libgpiod character device interface. Attempting to force WiringPi onto a Pi 5 will result in silent failures or kernel panics. Use lgpio or libgpiodcxx instead.
How do I compile Raspberry Pi C++ code with CMake instead of g++?
For production builds, CMake is preferred. Create a CMakeLists.txt file in your project root. You must explicitly find the lgpio package and link it. Add find_library(LGPIO_LIB lgpio) and then target_link_libraries(your_target_name ${LGPIO_LIB}). Ensure you set CMAKE_CXX_STANDARD 17 to support the modern threading and atomic features used in the code above.
Why does my PWM output stutter when running C++ on Raspberry Pi Bookworm?
Stuttering usually occurs when using software PWM (bit-banging) via standard GPIO toggling, as the Linux kernel is not a Real-Time OS (RTOS) and will preempt your thread for background tasks. The code provided above uses lgTxPwm, which offloads the PWM generation to the RP1 hardware peripheral. If you are still seeing stutter, verify you are using a hardware PWM-capable pin (like GPIO 18) and not a standard GPIO pin.
Can I use standard C++ std::thread for GPIO polling instead of interrupts?
You can, but it is highly discouraged for button inputs. A polling loop running at 1kHz (1ms sleep) will constantly wake the CPU, preventing it from entering low-power states and generating unnecessary heat. Furthermore, if the OS scheduler delays your thread by 5ms, you might entirely miss a short button press. Hardware interrupts via lgGpioSetAlertsFunc are handled by the kernel and are both more power-efficient and more reliable.






