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.
| 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) |
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) |
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
gpiochip0based on Pi 4 tutorials. The Pi 5's RP1 southbridge maps togpiochip4. - Fix: Change your
GPIO_CHIPdefine to4. Verify by runningls /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
pigpioddaemon, or an I2C/SPI overlay configured in/boot/firmware/config.txt. - Fix: Run
sudo lsof | grep gpioto find the hogging process and kill it. Alternatively, uselgGpioClaimAlertif you only need to read interrupts, or reboot to clear phantom locks.
3. "Segmentation fault (core dumped)"
- Cause: You attempted to call
lgTxPwm()orlgGpioWrite()using a negative handle integer becauselgGpiochipOpen()failed silently in your logic flow, or you passed an uninitialized pointer. - Fix: Always check
if (handle < 0)immediately after opening the chip andreturn 1;to abort before touching GPIO functions.
- Verify the Chip ID: Run
cat /sys/kernel/debug/gpioto see which chip owns the base GPIO lines. On Pi 5, it's almost always chip 4. - 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.
- 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.






