If you need sub-millisecond GPIO latency or hardware-timed PWM on a Raspberry Pi, Python will bottleneck you. Writing C on Raspberry Pi using the modern lgpio library bypasses the Python interpreter overhead, giving you direct, low-jitter access to the RP1 southbridge chip on the Pi 5. This guide provides the exact hardware list, pin mappings, and compilable C code to build a high-speed interrupt-driven button reader with hardware PWM output.

Why Write C on Raspberry Pi Instead of Python?

The Raspberry Pi 5 introduced the RP1 I/O controller, changing how GPIO is handled at the silicon level. While Python libraries like gpiozero are excellent for basic automation, they suffer from garbage collection pauses and interpreter overhead that introduce 1ms to 5ms of jitter. When you are reading a high-resolution rotary encoder, bit-banging a custom protocol, or driving a fast-stepping motor, that jitter causes missed steps or phantom reads.

The Verdict: Use C with lgpio when your timing budget is under 100µs. Stick to Python for I2C/SPI sensor polling where a 5ms delay is irrelevant.

Decision Path: Which Language and Library to Pick

RequirementLanguageLibraryLatency / Jitter
Basic LED blinking, slow sensor polling (>10ms)Pythongpiozero / lgpio~1 - 5ms
Standard PWM, I2C/SPI displaysC / C++pigpio (Legacy)~50µs
High-speed interrupts, sub-100µs polling, Pi 5 nativeClgpio (Modern)< 5µs
Hard real-time, zero OS jitterC / RustBare Metal / RTOSNanoseconds

Concrete Pick: For 95% of embedded makers needing speed on Raspberry Pi OS Bookworm, C with lgpio is the definitive choice. It is the officially supported successor to the deprecated wiringPi and the C-level backbone of the modern Pi GPIO stack.

Hardware Spec Sheet & Parts List

This build targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm (64-bit). The Pi 5 operates its GPIO at 3.3V, but the RP1 chip is notably more sensitive to voltage spikes than the BCM2711 on the Pi 4. Do not feed 5V into these pins.

ComponentExact Variant / Part NumberEst. Cost (2026)Purpose
MicrocontrollerRaspberry Pi 5 (8GB)$80.00Main compute & RP1 I/O controller
SwitchOmron D2HW-C201M (Subminiature)$2.50Low-bounce tactile input for interrupt testing
Resistors10kΩ 1/4W (x2) & 330Ω (x1)$0.10Pull-ups for switch, current limiting for LED
Capacitor0.1µF Ceramic (x1)$0.05Hardware debounce across switch terminals
Indicator5mm Diffused Red LED$0.10PWM output visual verification

Pin Mapping & Wiring the Pi 5

The Pi 5 maintains the standard 40-pin physical layout, but the internal routing goes through the RP1 chip. We will use GPIO 17 for the interrupt-driven button and GPIO 18 for hardware PWM. GPIO 18 is one of the few pins on the Pi 5 that supports true hardware PWM channel 0.

Pi 5 Pin (Physical)GPIO (BCM)Function in this BuildWiring Destination
Pin 11GPIO 17Input (Active Low with Pull-up)Omron Switch Terminal A
Pin 12GPIO 18Hardware PWM Output330Ω Resistor -> LED Anode
Pin 6GNDCommon GroundSwitch Terminal B & LED Cathode
Pin 13.3VPower (Not used directly here)N/A (Internal pull-up used)
Safety & Hardware Note: Always wire the 0.1µF capacitor directly across the switch terminals (GPIO 17 and GND). While lgpio supports software debouncing, hardware debouncing prevents the physical switch contacts from ringing and generating high-frequency voltage spikes that can degrade the RP1 silicon over time.

The Code: Sub-Millisecond Interrupts with lgpio

Below is the complete, compilable C code. It configures GPIO 17 as an input with an internal pull-up, sets an alert callback for both edges (to catch the exact microsecond of the press), and initializes hardware PWM on GPIO 18. When the button is pressed, the PWM duty cycle increments.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <lgpio.h>

// --- PIN DEFINITIONS ---
#define GPIOCHIP 0       // Pi 5 40-pin header is typically chip 0 on Bookworm
#define BUTTON_PIN 17    // Physical Pin 11
#define PWM_PIN 18       // Physical Pin 12 (Hardware PWM0)

// --- GLOBAL STATE ---
int gpio_handle;
int pwm_duty_cycle = 0;  // 0 to 100
volatile sig_atomic_t keep_running = 1;

// --- CALLBACKS & HANDLERS ---
void cleanup_and_exit(int sig) {
    printf("\nCaught signal %d. Releasing GPIO and exiting...\n", sig);
    lgGpiochipClose(gpio_handle);
    exit(0);
}

// lgpio alert callback signature
void button_callback(int num_alerts, lgGpioAlert_p alerts, void *data) {
    for (int i = 0; i < num_alerts; i++) {
        // We only care about the falling edge (button press, active low)
        if (alerts[i].level == 0) {
            pwm_duty_cycle += 10;
            if (pwm_duty_cycle > 100) pwm_duty_cycle = 0;
            
            // Update hardware PWM: 10kHz frequency, calculated duty cycle
            // lgTxPwm(handle, gpio, pwmFreq, pwmDuty, pwmOffset, pwmCycles)
            lgTxPwm(gpio_handle, PWM_PIN, 10000, pwm_duty_cycle, 0, 0);
            printf("Button pressed! PWM Duty: %d%%\n", pwm_duty_cycle);
        }
    }
}

int main() {
    // 1. Setup signal handlers for clean exit
    signal(SIGINT, cleanup_and_exit);
    signal(SIGTERM, cleanup_and_exit);

    // 2. Open the GPIO chip
    gpio_handle = lgGpiochipOpen(GPIOCHIP);
    if (gpio_handle < 0) {
        fprintf(stderr, "Failed to open GPIO chip %d: %s\n", GPIOCHIP, lguErrorText(gpio_handle));
        return 1;
    }

    // 3. Claim BUTTON_PIN as input with internal pull-up and alert on both edges
    int claim_res = lgGpioClaimInput(gpio_handle, LG_ALERT_BOTH, BUTTON_PIN);
    if (claim_res < 0) {
        fprintf(stderr, "Failed to claim GPIO %d: %s\n", BUTTON_PIN, lguErrorText(claim_res));
        lgGpiochipClose(gpio_handle);
        return 1;
    }
    
    // Enable internal pull-up
    lgGpioSetPullUpDown(gpio_handle, BUTTON_PIN, LG_PULL_UP);

    // 4. Claim PWM_PIN for output
    claim_res = lgGpioClaimOutput(gpio_handle, 0, PWM_PIN);
    if (claim_res < 0) {
        fprintf(stderr, "Failed to claim PWM GPIO %d: %s\n", PWM_PIN, lguErrorText(claim_res));
        lgGpiochipClose(gpio_handle);
        return 1;
    }

    // 5. Register the interrupt callback
    lgGpioSetAlertsFunc(gpio_handle, BUTTON_PIN, button_callback, NULL);

    // 6. Initialize PWM at 0% duty, 10kHz
    lgTxPwm(gpio_handle, PWM_PIN, 10000, 0, 0, 0);

    printf("System ready. Press the button to increase PWM duty cycle.\n");
    printf("Press Ctrl+C to exit cleanly.\n");

    // Main loop just keeps the process alive while interrupts do the work
    while (keep_running) {
        sleep(1);
    }

    return 0;
}

Compilation, Execution, and Debugging

To compile this on your Pi 5, ensure you have the build tools and the lgpio C library installed:

sudo apt update
sudo apt install build-essential liblgpio-dev

Compile and run:

gcc -O2 -o pi_fast_io main.c -llgpio
./pi_fast_io

Troubleshooting: The 'GPIO Busy' Error

The most common failure when running C on Raspberry Pi is encountering the following exact error string in your terminal:

Failed to claim GPIO 17: GPIO busy

This corresponds to the LG_GPIO_BUSY (-5) error code. It means the Linux kernel or another user-space process has already claimed the pin.

The First 3 Things to Check:

  1. Check for Zombie Processes: If your previous C program crashed before calling lgGpiochipClose(), the pin might remain locked. Run sudo fuser -v /dev/gpiochip0 to find and kill the PID holding the lock.
  2. Verify Pinmux Conflicts: Run gpioinfo in the terminal. If GPIO 17 or 18 shows as "used" by a system function (like UART or I2S audio), you must disable that overlay in /boot/firmware/config.txt (e.g., dtparam=audio=off frees up PWM pins).
  3. Check Permissions: While Bookworm handles GPIO permissions better than older OS versions via udev rules, ensure your user is in the gpio group (sudo usermod -aG gpio $USER), or run the binary with sudo as a temporary test.

Extending or Simplifying the Build

This architecture is designed to be modular. Here is how you adapt it to your specific project constraints.

How to Simplify

If you do not need PWM and only want to read a sensor state as fast as possible, strip out the lgTxPwm calls and the PWM_PIN claims. Change the main loop from sleep(1) to a tight while(1) loop using lgGpioRead(gpio_handle, BUTTON_PIN). Be warned: a tight polling loop in C will max out one core of the Pi 5's Cortex-A76 CPU. Always prefer the interrupt (lgGpioSetAlertsFunc) method shown above to keep CPU usage near 0%.

How to Extend

To scale this into a full motor controller or data-logging rig:

  • Add I2C Sensors: lgpio includes lgI2cOpen() and lgI2cReadDevice(). You can poll a BME280 sensor inside the main loop every 100ms without blocking the microsecond-level button interrupts.
  • Implement Rotary Encoders: Duplicate the BUTTON_PIN setup for a second pin (GPIO 27). In the callback, read the state of the second pin to determine rotation direction. Because C executes the callback in under 5µs, you will never miss a quadrature state change, even at 3000 RPM.

For authoritative documentation on the C API functions used here, refer to the official lgpio library documentation. For hardware-level details on the Pi 5's RP1 chip and GPIO bank routing, consult the Raspberry Pi Hardware Documentation.