Why Write C on the Raspberry Pi 5? (The RP1 Architecture Shift)
If you are reaching for the Raspberry Pi C language toolchain instead of Python, you likely need microsecond timing precision, a memory footprint under 2MB, or direct hardware integration without the overhead of a garbage collector. For years, hobbyists relied on wiringPi or direct BCM2835 memory-mapped registers to achieve this.
That era ended with the Raspberry Pi 5. The Pi 5 uses the custom RP1 southbridge chip, which completely changed how GPIO and PWM are addressed. Direct memory mapping to the CPU is no longer viable for GPIO control. Instead, modern C development on the Pi 5 relies on the Linux gpiochar subsystem via the lgpio library. This guide walks through building a hardware PWM fan controller using C, explicitly targeting the RP1 architecture.
Target Board: Raspberry Pi 5 (4GB or 8GB variant).
Project Spec Sheet & Parts List
Before writing a single line of code, verify your bench inventory. The Pi 5's 5V rail can supply up to 5A total (depending on your USB-C PD power supply), but individual GPIO pins are strictly limited to 3.3V logic and ~16mA max current. We use a MOSFET to isolate the Pi's logic from the fan's power draw.
| Component | Exact Model / Variant | Key Specification | Est. 2026 Price |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB) | RP1 Southbridge, 2.4GHz Cortex-A76 | $80.00 |
| Cooling Fan | Noctua NF-A4x20 5V PWM | 25kHz PWM freq, 5V DC, 1.2W max | $15.90 |
| Switching MOSFET | 2N7000 N-Channel | Vgs(th) ~2.0V (Logic-level compatible) | $0.15 |
| Gate Pull-down | 100kΩ 1/4W Carbon Film | Prevents floating gate spin-ups | $0.02 |
| Gate Series Resistor | 10kΩ 1/4W Carbon Film | Limits GPIO current during switching | $0.02 |
Pin Mapping & Hardware Wiring
The RP1 chip maps Hardware PWM0 to GPIO 18 (Physical Pin 12). This is the exact same physical pin as the Pi 4, but the underlying clock routing is handled by the RP1 firmware. Standard 4-pin PC fans require a 25kHz PWM signal; if the signal drops to 0Hz or the pin floats, the Noctua fan defaults to 100% speed as a fail-safe.
| Pi 5 Physical Pin | BCM / GPIO | Function | Wire Destination |
|---|---|---|---|
| Pin 2 | 5V Power | VCC (Fan Power) | Fan Pin 2 (Red / VCC) |
| Pin 6 | GND | Common Ground | Fan Pin 1 (Black) & MOSFET Source |
| Pin 12 | GPIO 18 | Hardware PWM0 | 10kΩ Resistor to MOSFET Gate |
Wiring Steps
- Power Down: Disconnect the Pi 5 USB-C power supply. Never wire MOSFETs to live GPIO headers.
- MOSFET Gate Drive: Connect a jumper from Pi Pin 12 (GPIO 18) through the 10kΩ resistor to the Gate (middle pin) of the 2N7000.
- Pull-down Resistor: Connect the 100kΩ resistor between the MOSFET Gate and Source (right pin). This ensures the fan stays off during Pi boot-up before the C program claims the GPIO.
- Load Connection: Connect the MOSFET Drain (left pin) to the Fan's PWM wire (Blue/Yellow). Connect the Fan's GND (Black) to Pi Pin 6.
- Fan Power: Connect the Fan's VCC (Red) to Pi Pin 2 (5V). Note: Ensure your fan is the 5V variant. A 12V fan will not spin reliably on the Pi's 5V rail.
The C Code: Compiling and Running with lgpio
Unlike Python's RPI.GPIO, C requires explicit compilation and linking. We use lgpio, which interfaces cleanly with the Pi 5's /dev/gpiochip4 character device. The code below initializes a 25kHz PWM signal at a 50% duty cycle.
Install dependencies (Raspberry Pi OS Bookworm/Wormhole):
sudo apt update
sudo apt install liblgpio-dev gcc make
Complete C Source (fan_ctrl.c):
#include <stdio.h>
#include <stdlib.h>
#include <lgpio.h>
#include <unistd.h>
// Pi 5 RP1 maps GPIO to gpiochip4
#define PI5_GPIOCHIP 4
#define FAN_PWM_PIN 18
#define PWM_FREQ 25000.0 // 25kHz standard for 4-pin PC fans
int main(void) {
// 1. Open the GPIO chip
int h = lgGpiochipOpen(PI5_GPIOCHIP);
if (h < 0) {
fprintf(stderr, "[FATAL] Failed to open gpiochip%d: %s\n",
PI5_GPIOCHIP, lguErrorText(h));
return EXIT_FAILURE;
}
// 2. Start Hardware PWM (50% duty cycle)
// lgTxPwm(handle, gpio, freq, duty, offset, cycles)
int status = lgTxPwm(h, FAN_PWM_PIN, PWM_FREQ, 50.0, 0, 0);
if (status < 0) {
fprintf(stderr, "[FATAL] PWM setup failed on GPIO %d: %s\n",
FAN_PWM_PIN, lguErrorText(status));
lgGpiochipClose(h);
return EXIT_FAILURE;
}
printf("PWM active on GPIO %d at 50%% duty cycle.\n", FAN_PWM_PIN);
printf("Press ENTER to stop the fan and exit...\n");
// Block until user presses Enter
getchar();
// 3. Graceful shutdown: Drop duty to 0% before releasing
lgTxPwm(h, FAN_PWM_PIN, PWM_FREQ, 0.0, 0, 0);
lgGpiochipClose(h);
return EXIT_SUCCESS;
}
Compile and Execute:
gcc -O2 -o fan_ctrl fan_ctrl.c -llgpio
./fan_ctrl
Debugging: First Three Things to Check When It Fails
When moving from Pi 4 to Pi 5, or migrating from older libraries like pigpio, C programs frequently fail at the hardware abstraction layer. If your fan doesn't spin or the program crashes, check these three failure modes in order.
1. The Character Device Permission Error
Exact Error String: [FATAL] Failed to open gpiochip4: /dev/gpiochip4: Permission denied
The Cause: The lgpio library uses the Linux gpiochar interface. By default, standard users do not have read/write access to /dev/gpiochip4.
The Fix: Do not run your compiled binary with sudo (this creates security risks and breaks environment variables). Instead, add your user to the gpio group and reboot:
sudo usermod -aG gpio $USER
sudo reboot
2. The RP1 Address Space Mismatch
Exact Error String: [FATAL] Failed to open gpiochip0: /dev/gpiochip0: No such file or directory
The Cause: You copied code from a Pi 4 tutorial. On the Pi 4, the main GPIO bank was gpiochip0. On the Pi 5, the RP1 southbridge exposes the main header on gpiochip4. (Chips 0-3 are reserved for internal RP1 power management and PCIe routing).
The Fix: Ensure your #define PI5_GPIOCHIP 4 macro is set correctly. If you are writing cross-compatible code for Pi 4 and Pi 5, use lgGpiochipOpen(0) and iterate up to 4, or use the lgpio Python/C wrapper functions that auto-detect the board revision via the official device tree.
3. PWM Clock Contention and Allocation
Exact Error String: [FATAL] PWM setup failed on GPIO 18: lgTxPwm: GPIO is not allocated for PWM (or Device or resource busy)
The Cause: Another process (like the pigpiod daemon, dtoverlay audio configs, or a stray Python script) has already claimed the PWM hardware clocks on the RP1 chip.
The Fix: Kill competing daemons (sudo systemctl stop pigpiod). Furthermore, verify that /boot/firmware/config.txt does not contain dtparam=audio=on, which historically hijacked PWM0 for analog audio output on older Pis (though RP1 handles this better, it can still cause clock contention in early 2026 firmware builds).
Extending and Simplifying the Build
Once you have the baseline C program compiling and spinning the fan, you can adapt the hardware to fit your specific project constraints.
How to Simplify (The Direct-Drive Shortcut)
If you are using a low-current 5V fan (like the Noctua NF-A4x10 which draws only ~0.8W / 160mA), you can simplify the build by removing the 2N7000 MOSFET and driving the PWM pin directly from GPIO 18 to the Fan's PWM wire. The Noctua PWM specification sheet confirms their fans have internal pull-ups and high-impedance logic inputs that draw less than 2mA from the PWM signal wire. Warning: This only applies to the PWM signal wire (Pin 4). Never connect the Fan VCC (Pin 2) directly to a Pi GPIO pin.
How to Extend (Closed-Loop PID Control)
To turn this into a thermal management system, extend the C code by adding an I2C temperature sensor. The TI TMP117 (±0.1°C accuracy) is ideal for C implementations because it requires no floating-point math to parse the raw 16-bit two's complement registers.
- Wire the TMP117 SDA/SCL to Pi 5 Pins 3 and 5.
- Use the Linux
i2c-devC API to read the 2-byte temperature register. - Implement a simple PID loop in your
main()while(1)block, adjusting thepwmDutyparameter inlgTxPwm()based on the delta between the target temperature and the TMP117 reading. - Use
usleep(500000)to sample every 500ms, preventing I2C bus flooding while keeping the fan response smooth.






