If you need sub-millisecond GPIO latency, true hardware PWM without OS jitter, or multi-threaded sensor polling without Python’s Global Interpreter Lock (GIL) bottlenecking your CPU, C++ and Raspberry Pi is the correct stack. Specifically, targeting the Raspberry Pi 5 with the modern lgpio C library gives you direct, low-level access to the RP1 southbridge chip.

This guide walks through building a 25kHz hardware PWM thermal fan controller. We will cover the exact hardware required, the C++ implementation with robust error handling, and the specific debugging steps for the most common GPIO failures on Pi 5.

The Verdict: Python vs C++ for Raspberry Pi GPIO

Before writing a single line of code, you need to know if C++ is actually required for your project. Python (via gpiozero) is excellent for 90% of hobbyist tasks, but it falls apart at high-frequency hardware control. Use the decision matrix below to make your final pick.

Decision Tree: Python vs C++ for Raspberry Pi Embedded Projects
Requirement Python (gpiozero / RPi.GPIO) C++ (lgpio)
GPIO Toggle Latency ~10µs to 50µs (OS dependent) < 1µs (Direct memory mapped)
Hardware PWM Stability Software PWM (jittery above 1kHz) True Hardware PWM (stable to 25kHz+)
Multi-threading Blocked by GIL (CPU bound) True POSIX threads (pthread)
Setup Complexity Low (pip install) Medium (Requires g++ and liblgpio-dev)
The Default Pick: If your project involves driving standard 4-wire PC fans (which require a strict 25kHz PWM signal per Intel specifications), reading high-speed encoders, or running a tight PID control loop, choose C++ with lgpio. For simple relays, buttons, or slow I2C sensors, stick to Python.

Hardware Spec Sheet & Pin Mapping for Pi 5

This build targets the Raspberry Pi 5 8GB variant running Raspberry Pi OS (Bookworm). The Pi 5 uses the RP1 southbridge for GPIO, which changes the underlying chip architecture compared to the Pi 4. We are using a 5V PWM fan and a level shifter to protect the Pi's 3.3V logic.

Parts List

  • Compute: Raspberry Pi 5 8GB (Part # SC1113) - ~$80 USD
  • Actuator: Noctua NF-A4x10 5V PWM Fan (Part # NF-A4x10-PWM) - ~$15 USD
  • Level Shifter: Adafruit 4-Channel I2C-safe Bi-directional Logic Level Converter (BSS138, Product ID 757) - ~$4 USD
  • Wiring: 22 AWG solid core jumper wires, 10kΩ pull-up resistors (if not pre-populated on shifter).

Pin Mapping Table

The Pi 5 GPIO header remains physically identical to previous models, but the internal routing goes through the RP1 chip. GPIO 18 is one of the few pins capable of true hardware PWM0.

Pi 5 Pin (BCM) Physical Pin Function Connection Target
3V3 Power 1 Logic High (LV) Level Shifter LV Pin
5V Power 2 Logic High (HV) & Fan Power Level Shifter HV Pin & Fan Pin 2 (VCC)
GPIO 18 (PWM0) 12 PWM Output Level Shifter LV1 (Input)
GND 6 Common Ground Level Shifter GND & Fan Pin 1 (GND)
N/A (Shifter) N/A Shifted PWM Level Shifter HV1 to Fan Pin 4 (PWM)
N/A (Fan) N/A Tachometer (RPM) Fan Pin 3 (Leave unconnected or wire to GPIO 17 for reading)
Safety Warning: Never connect a 12V PC fan directly to the Pi's 5V rail or GPIO pins. If you substitute the 5V Noctua fan for a standard 12V case fan, you must use an N-channel MOSFET (like the IRLZ44N) to switch the ground path, and place a 1N4007 flyback diode across the fan terminals to prevent inductive voltage spikes from destroying the Pi's RP1 chip.

Complete C++ Build: Hardware PWM Thermal Controller

Before compiling, install the lgpio development headers on your Pi 5:

sudo apt update
sudo apt install liblgpio-dev

The following C++ program reads the Pi 5's internal SoC temperature, maps it to a 20%–100% duty cycle, and drives the hardware PWM pin. It includes explicit error handling for file I/O and GPIO claims.

#include <iostream>
#include <fstream>
#include <string>
#include <unistd>
#include <lgpio.h>

// --- PIN & HARDWARE DEFINITIONS ---
#define GPIO_CHIP 4       // Pi 5 RP1 southbridge is gpiochip4
#define PWM_PIN 18        // Hardware PWM0 capable pin
#define PWM_FREQ 25000    // 25kHz standard for 4-wire PWM fans
#define TEMP_MIN 45.0     // Temp (C) to start ramping fan
#define TEMP_MAX 75.0     // Temp (C) for 100% fan speed
#define DUTY_MIN 20.0     // Minimum duty cycle to keep fan spinning
#define DUTY_MAX 100.0    // Maximum duty cycle

int main() {
    // 1. Open the GPIO chip
    int handle = lgGpiochipOpen(GPIO_CHIP);
    if (handle < 0) {
        std::cerr << "Error: Failed to open /dev/gpiochip" << GPIO_CHIP 
                  << ". Error code: " << handle << std::endl;
        return 1;
    }

    // 2. Claim the PWM pin
    int err = lgGpioClaim(handle, PWM_PIN, 0);
    if (err < 0) {
        std::cerr << "Error: Failed to claim GPIO " << PWM_PIN 
                  << ". It may be busy. Error code: " << err << std::endl;
        lgGpiochipClose(handle);
        return 1;
    }

    std::cout << "PWM Thermal Controller Started. Press Ctrl+C to exit." << std::endl;

    // Main control loop
    while (true) {
        // 3. Read SoC Temperature
        std::ifstream temp_file("/sys/class/thermal/thermal_zone0/temp");
        if (!temp_file.is_open()) {
            std::cerr << "Error: Cannot read thermal zone file." << std::endl;
            break;
        }
        
        int temp_mc = 0;
        temp_file >> temp_mc;
        temp_file.close();
        
        double temp_c = temp_mc / 1000.0;
        double duty_cycle = DUTY_MIN;

        // 4. Map temperature to PWM duty cycle
        if (temp_c >= TEMP_MAX) {
            duty_cycle = DUTY_MAX;
        } else if (temp_c > TEMP_MIN) {
            double ratio = (temp_c - TEMP_MIN) / (TEMP_MAX - TEMP_MIN);
            duty_cycle = DUTY_MIN + ratio * (DUTY_MAX - DUTY_MIN);
        }

        // 5. Apply Hardware PWM
        // lgTxPwm(handle, gpio, freq, duty, offset, cycles)
        err = lgTxPwm(handle, PWM_PIN, PWM_FREQ, duty_cycle, 0, 0);
        if (err < 0) {
            std::cerr << "Error: Failed to set PWM. Code: " << err << std::endl;
        }

        std::cout << "Temp: " << temp_c << "C | PWM Duty: " << duty_cycle << "%" << std::endl;
        
        // Poll every 2 seconds
        sleep(2);
    }

    // 6. Cleanup
    lgTxPwm(handle, PWM_PIN, 0, 0, 0, 0); // Stop PWM
    lgGpioFree(handle, PWM_PIN);
    lgGpiochipClose(handle);
    return 0;
}

Compile and run:

g++ -O2 -o thermal_fan main.cpp -llgpio
sudo ./thermal_fan

Note: sudo is required on Bookworm to access /dev/gpiochip4 unless you add your user to the gpio group and configure udev rules.

Debugging the "Failed to Open GPIO" and Segfault Errors

When transitioning from Python to C++ on the Pi 5, you will hit hardware abstraction errors. Here are the exact error strings the lgpio library throws, ranked by frequency, and how to fix them.

1. "lgGpiochipOpen: error -1 (Failed to open /dev/gpiochip0)"

  • Cause: You hardcoded gpiochip0 based on Pi 4 tutorials. The Pi 5's RP1 southbridge maps to gpiochip4.
  • Fix: Change your GPIO_CHIP define to 4. Verify by running ls /dev/gpiochip* in the terminal.

2. "lgGpioClaim: error -5 (GPIO busy)"

  • Cause: Another process has claimed the pin. This is often a leftover Python script, the pigpiod daemon, or an I2C/SPI overlay configured in /boot/firmware/config.txt.
  • Fix: Run sudo lsof | grep gpio to find the hogging process and kill it. Alternatively, use lgGpioClaimAlert if you only need to read interrupts, or reboot to clear phantom locks.

3. "Segmentation fault (core dumped)"

  • Cause: You attempted to call lgTxPwm() or lgGpioWrite() using a negative handle integer because lgGpiochipOpen() failed silently in your logic flow, or you passed an uninitialized pointer.
  • Fix: Always check if (handle < 0) immediately after opening the chip and return 1; to abort before touching GPIO functions.
The First 3 Things to Check When It Fails:
  1. Verify the Chip ID: Run cat /sys/kernel/debug/gpio to see which chip owns the base GPIO lines. On Pi 5, it's almost always chip 4.
  2. Check Logic Levels: Put your multimeter in DC Voltage mode. Probe the LV1 pin on the level shifter while the code runs. You should see it toggling between 0.0V and 3.3V. If it sits at 0V, your C++ code isn't executing or the pin is wrong.
  3. Inspect Permissions: If running without sudo, ensure your user is in the gpio group: sudo usermod -aG gpio $USER, then log out and back in.

Extending and Simplifying the Build

Once the baseline thermal controller is stable, you can adapt the hardware and software to fit tighter constraints or broader system requirements.

How to Simplify (Drop the Level Shifter)

If you want to eliminate the BSS138 level shifter to save board space, you must swap the Noctua fan for a 3.3V-logic-tolerant PWM fan (like the Sunon MF40100V2 or specific 3D printer cooling fans). Wire the Pi's GPIO 18 directly to the fan's PWM wire. Do not do this with standard 5V PC fans; while some tolerate 3.3V logic, many will run at 100% speed continuously or fail to recognize the PWM signal, defeating the purpose of the build.

How to Extend (Add MQTT Telemetry)

To integrate this into a Home Assistant dashboard, add the Eclipse Paho MQTT C++ library. 1. Install via sudo apt install libpaho-mqttpp3-dev. 2. Inside the while(true) loop, format the temp_c and duty_cycle variables into a JSON string. 3. Publish to a topic like homeassistant/sensor/pi5_rack/temp. This allows you to graph the Pi's thermal throttling behavior over time without adding the overhead of Python's paho-mqtt library, keeping the CPU overhead of the monitoring tool itself near zero.

For deeper technical reference on the RP1 chip architecture and C API specifics, consult the official Raspberry Pi 5 hardware documentation and the lgpio C library reference manual.