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

  1. 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.
  2. 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.
  3. 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.

Target Variant: This code explicitly targets the Raspberry Pi 5 (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:

  1. Missing Dev Package: You installed the runtime library but not the C/C++ headers. Fix: sudo apt install liblgpio-dev.
  2. Incorrect Linker Order: In GCC, the order of arguments matters. The -llgpio flag 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:

  1. Pin Already Claimed: Another process (like a lingering Python script or the pinctrl daemon) holds the pin. Run lgpio CLI tool or pinctrl get to verify pin state, and kill competing processes.
  2. Wrong Chip Index: You are running on a Pi 5 but the code targets gpiochip0 (the Pi 4 default). Ensure GPIOCHIP_HANDLE is set to 4.
  3. Permissions: Your user is not in the gpio group. Fix: sudo usermod -aG gpio $USER, then log out and back in.
The First 3 Things to Check When It Fails:
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.