If you are writing bare-metal or low-latency control loops for the Raspberry Pi, Python's gpiozero will eventually bottleneck your system. For hard real-time motor control, sensor polling, or custom communication protocols, Raspberry Pi C programming is mandatory. However, the transition to Raspberry Pi OS Bookworm and the Raspberry Pi 5's new RP1 southbridge architecture has completely broken legacy C libraries.
To program GPIO in C on a modern Raspberry Pi (Pi 4 on Bookworm or Pi 5), you must use the lgpio library. Below is a complete, bench-tested guide to building a hardware PWM motor controller using a TB6612FNG driver, including the exact pin mappings, compilable C code, and the specific debugging steps for the RP1 architecture.
The 2026 Landscape: Choosing the Right C GPIO Library
Before writing a single line of code, you must select the correct library. If you copy-paste a tutorial from 2021 using wiringPi, your code will fail to compile or segfault on a Pi 5. The Raspberry Pi 5 routes its GPIO through an external RP1 southbridge chip, changing how memory-mapped I/O works.
| Library | Pi 5 (RP1) Support | Avg. Toggle Latency | Status & Architecture |
|---|---|---|---|
| lgpio | Native / Full | ~1 µs | Current Standard. Uses Linux libgpiod backend. Required for Pi 5 and Bookworm. |
| pigpio | Broken / Partial | ~2 µs | Deprecated for Pi 5. Relies on /dev/mem access which RP1 blocks. Still works on Pi 4 Bullseye. |
| wiringPi | None | ~3 µs | Dead. Abandoned in 2019. Do not use for new projects. |
| sysfs | Native | ~500 µs | Legacy Kernel. Too slow for PWM or fast polling. Deprecated by the Linux kernel. |
For this project, we are targeting the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm (64-bit), utilizing lgpio for direct, low-latency hardware control.
Hardware BOM and Pin Mapping
We are skipping the ancient L298N bipolar motor driver. It drops nearly 2V across its internal Darlington pairs and wastes power as heat. Instead, we use the TB6612FNG, a dual MOSFET H-bridge that offers higher efficiency and a much smaller footprint.
Parts List
- Microcontroller: Raspberry Pi 5 (4GB RAM)
- Motor Driver: SparkFun TB6612FNG Breakout (Part # ROB-14451)
- Motor: Pololu 6V N20 DC Gearmotor (Part # 3074, 100:1 ratio)
- Power Supply: 2S LiPo Battery (7.4V nominal) or 6V bench supply for the motor VM rail
- Passives: 10kΩ pull-up resistor for the STBY pin (optional but recommended for clean boot states)
Pin Mapping Table
The TB6612FNG requires logic voltage (VCC), motor voltage (VM), ground, direction pins, a standby pin, and a PWM signal. Below is the exact physical wiring map.
| TB6612FNG Pin | Raspberry Pi 5 GPIO (BCM) | Physical Pin # | Function / Notes |
|---|---|---|---|
| VCC | 3.3V Power | 1 | Logic level power (Do not connect to 5V) |
| GND | GND | 9 | Common ground with Pi and Motor Supply |
| PWMA | GPIO 12 | 32 | Hardware PWM0 signal for Motor A speed |
| AIN1 | GPIO 5 | 29 | Direction control bit 1 |
| AIN2 | GPIO 6 | 31 | Direction control bit 2 |
| STBY | GPIO 13 | 33 | Standby (Active HIGH to enable driver) |
| VM | N/A (External) | N/A | Connect to 6V-7.4V Motor Power Supply (+) |
VM pin and tie the grounds together.
The C Code: Hardware PWM and Direction Control
The following C program initializes the GPIO pins, enables the TB6612FNG, and ramps the motor speed up and down using hardware PWM. It includes robust error handling for the lgpio API calls.
Prerequisite: Install the lgpio development headers on your Pi:
sudo apt update && sudo apt install liblgpio-dev
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <lgpio.h>
// --- Pin Definitions (BCM Numbering) ---
#define PWM_PIN 12 // HW PWM0
#define AIN1_PIN 5
#define AIN2_PIN 6
#define STBY_PIN 13
// --- Hardware Constants ---
// CRITICAL: Pi 5 uses gpiochip4 for the RP1 southbridge.
// Pi 4 and older use gpiochip0.
#define GPIO_CHIP 4
#define PWM_FREQ 1000.0 // 1kHz PWM frequency
int main() {
// 1. Open the GPIO Chip
int h = lgGpiochipOpen(GPIO_CHIP);
if (h < 0) {
fprintf(stderr, "Fatal: Failed to open GPIO chip %d. Error: %s\n",
GPIO_CHIP, lguErrorText(h));
return EXIT_FAILURE;
}
// 2. Claim GPIO pins as outputs
int claims[] = {AIN1_PIN, AIN2_PIN, STBY_PIN};
if (lgGpioClaimOutputs(h, 0, claims, 3) < 0) {
fprintf(stderr, "Fatal: Failed to claim direction/standby pins.\n");
lgGpiochipClose(h);
return EXIT_FAILURE;
}
// 3. Enable the Motor Driver (Pull STBY HIGH)
lgGpioWrite(h, STBY_PIN, 1);
// Set initial direction: Forward (AIN1=HIGH, AIN2=LOW)
lgGpioWrite(h, AIN1_PIN, 1);
lgGpioWrite(h, AIN2_PIN, 0);
printf("Motor controller initialized. Ramping speed...\n");
// 4. Ramp up PWM duty cycle from 0% to 100%
for (int duty = 0; duty <= 100; duty += 5) {
// lgTxPwm(handle, gpio, freq, duty, offset, cycles)
// cycles=0 means run continuously
int pwm_status = lgTxPwm(h, PWM_PIN, PWM_FREQ, (float)duty, 0, 0);
if (pwm_status < 0) {
fprintf(stderr, "PWM Error on duty %d%%: %s\n", duty, lguErrorText(pwm_status));
break;
}
printf("Duty Cycle: %d%%\n", duty);
usleep(200000); // 200ms delay
}
// 5. Hold at max speed for 2 seconds
sleep(2);
// 6. Stop the motor and disable driver
lgTxPwm(h, PWM_PIN, PWM_FREQ, 0.0, 0, 0); // Kill PWM
lgGpioWrite(h, STBY_PIN, 0); // Enter standby mode
printf("Motor stopped. Cleaning up...\n");
lgGpiochipClose(h);
return EXIT_SUCCESS;
}
Compilation and Execution
Save the file as motor_ctrl.c. Compile it by linking the lgpio library:
gcc -o motor_ctrl motor_ctrl.c -llgpio
Execute the binary:
./motor_ctrl
Debugging: Fatal Errors and the RP1 Architecture
When migrating to C on the Pi 5, you will almost certainly hit a wall during your first compile or execution. Here is the exact troubleshooting decision tree for the most common fatal errors.
Error 1: lgGpiochipOpen: error -3 (No such file or directory)
This is the most common error when running legacy code on a Pi 5. The code is attempting to open gpiochip0, which does not handle the main GPIO header on the Pi 5.
- Verify the Chip Index: Run
ls /dev/gpiochip*in your terminal. On a Pi 4, the main header is/dev/gpiochip0. On a Pi 5, the RP1 southbridge exposes the main header as/dev/gpiochip4. Update your#define GPIO_CHIPaccordingly. - Check User Permissions: The
lgpiolibrary requires access to the character device. Ensure your user is in thegpiogroup:sudo usermod -aG gpio $USER, then log out and log back in. - Check Pin Multiplexing Conflicts: If a pin is claimed by the kernel for I2C or UART,
lgGpioClaimOutputswill fail. Runraspi-configand ensure I2C/Serial console are disabled if you are using those specific pins.
Error 2: lgTxPwm: error -41 (GPIO not available for PWM)
Cause: You are trying to use hardware PWM on a pin that does not support it. The Raspberry Pi has specific PWM channels. GPIO 12 and 13 share PWM0, while GPIO 18 and 19 share PWM1.
Fix: Change your PWM_PIN definition to 12, 13, 18, or 19. If you must use an arbitrary pin, switch from lgTxPwm (hardware) to lgTxServo or software-timed pulses, though this introduces microsecond-level jitter.
Error 3: Motor whines but doesn't spin, or Pi reboots randomly
Cause: This is not a software error; it is a hardware brownout. The TB6612FNG is pulling too much stall current through a weak power supply, or you forgot to tie the Pi GND to the Motor Driver GND.
Fix: Measure the voltage at the VM pin with a multimeter while the motor is commanded to start. If it drops below 4.5V, your power supply is inadequate. Upgrade to a supply rated for at least 2x the motor's stall current.
Extending and Simplifying the Build
Once you have the baseline C code running, you can adapt the architecture based on your project's final requirements.
How to Simplify (The Python Fallback)
If you realize your control loop only needs to update every 50 milliseconds (e.g., a simple roving robot avoiding walls), drop C and use Python. The overhead of maintaining C toolchains, handling manual memory cleanup, and dealing with lgpio chip indices isn't worth it for slow loops. Python's gpiozero library handles the Pi 4 vs Pi 5 chip differences automatically under the hood via lgpio bindings.
How to Extend (Closed-Loop PID Control)
To turn this open-loop speed controller into a precise closed-loop system, add a quadrature encoder to the motor shaft.
- Hardware: Add a magnetic encoder (like the Pololu #4752) to the back of the N20 motor.
- Software: Use the
lgGpioSetAlerts()function in C. This registers an interrupt callback that fires on every rising edge of the encoder pulse, allowing you to calculate exact RPM without blocking your main control thread. - Control: Feed the RPM delta into a discrete PID controller written in C, adjusting the
lgTxPwmduty cycle dynamically to maintain a setpoint regardless of battery voltage sag or mechanical load.
By mastering lgpio and the TB6612FNG, you bypass the abstraction layers of Python and gain direct, microsecond-accurate control over your Raspberry Pi's physical environment.






