To build a reliable Raspberry Pi RC car, skip the ancient L298N H-bridge and use a TB6612FNG dual motor driver. The L298N uses bipolar junction transistors (BJTs) that drop up to 2V across the chip, robbing your motors of torque and wasting battery as heat. The TB6612FNG uses MOSFETs, dropping that loss to roughly 0.5V and giving you precise speed control via hardware PWM.

This guide targets the Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (Bookworm). While the Pi 5 is faster, its higher idle power draw (2.7W vs 1.2W on the Pi 4) and strict 5V/5A USB-C PD requirement make it a poor fit for lightweight, battery-powered RC platforms unless you are willing to carry heavy buck converters. We will use a low-latency UDP socket architecture for control, bypassing the lag inherent in HTTP-based web interfaces.

Difficulty Rating: Intermediate (Requires basic Linux networking, Python, and soldering)
Estimated Time: 3-4 hours
Estimated Cost: $85 - $110 USD

Spec Sheet & Parts List

The biggest mistake in Raspberry Pi RC builds is under-gearing the motors or using unprotected lithium cells. Below is the exact bill of materials needed for a balanced, 2WD platform that can handle outdoor terrain without browning out the Pi.

Component Exact Model / Variant Key Specification Est. Cost (2026)
Compute Raspberry Pi 4 Model B (4GB) 1.5GHz Quad-core, 1.2W idle draw $55.00
Motor Driver Pololu TB6612FNG Breakout 1.2A continuous, 3.2A peak, MOSFET $12.50
Motors (x2) Pololu Micro Metal Gearmotor MP (30:1) 6V nominal, 1000 RPM no-load, 1.5A stall $18.00
Power Supply 2S 18650 Holder + DW01 BMS Module 7.4V nominal, Over-discharge protection $15.00
Voltage Regulator Pololu D24V50F5 Step-Down (5V, 5A) Switching regulator, low dropout $14.00

Note on Motors: Avoid the cheap yellow "TT" gearmotors (1:48 ratio). They spin too fast with almost zero stall torque, making RC control impossible. The 30:1 Micro Metal Gearmotors provide the low-end torque needed for steering and hill climbing.

Pin Mapping & Wiring the TB6612FNG

The Raspberry Pi 4 has four hardware PWM pins: GPIO 12, 13, 18, and 19. We will use GPIO 18 and 19 for the motor speed control to ensure smooth, jitter-free acceleration. The TB6612FNG requires a logic HIGH on the STBY (Standby) pin to operate; if you forget to wire this, the motors will never spin.

TB6612FNG Pin Raspberry Pi 4 GPIO Function
PWMAGPIO 18 (Pin 12)Hardware PWM0 for Motor A speed
AIN1GPIO 23 (Pin 16)Motor A Direction Logic 1
AIN2GPIO 24 (Pin 18)Motor A Direction Logic 2
STBYGPIO 25 (Pin 22)Chip Enable (Must be HIGH)
BIN1GPIO 17 (Pin 11)Motor B Direction Logic 1
BIN2GPIO 27 (Pin 13)Motor B Direction Logic 2
PWMBGPIO 19 (Pin 35)Hardware PWM1 for Motor B speed
GNDAny GND PinCommon Ground (Critical)
Wiring Warning: Never power the Pi directly from the 7.4V 2S lithium pack. You must use a switching step-down regulator (like the Pololu D24V50F5) set to exactly 5.1V to feed the Pi's 5V and GND GPIO header pins. Feeding 5V into the 3.3V pin will instantly destroy the SoC.

Python Control Code (UDP Socket Server)

For an RC car, HTTP requests (Flask/Django) introduce 50-150ms of latency per command, making the car feel sluggish. We use a raw UDP socket server. The Pi listens for single-byte characters ('F' for forward, 'B' for back, 'L' for left, 'R' for right, 'S' for stop) sent from a client script on your phone or laptop.

This code targets the gpiozero library using the lgpio pin factory, which is the default in Raspberry Pi OS Bookworm. It includes explicit pin definitions and error handling for socket binding and GPIO cleanup.

import socket
import sys
from gpiozero import PWMOutputDevice, DigitalOutputDevice
from gpiozero.exc import PinFactoryFallback

# --- PIN DEFINITIONS ---
# Motor A (Left)
PWMA = PWMOutputDevice(18, frequency=1000)
AIN1 = DigitalOutputDevice(23)
AIN2 = DigitalOutputDevice(24)

# Motor B (Right)
PWMB = PWMOutputDevice(19, frequency=1000)
BIN1 = DigitalOutputDevice(17)
BIN2 = DigitalOutputDevice(27)

# Standby Pin (Must be HIGH to enable the TB6612FNG)
STBY = DigitalOutputDevice(25)

UDP_IP = '0.0.0.0'
UDP_PORT = 5005
BUFFER_SIZE = 1024

def stop_motors():
    PWMA.value = 0
    PWMB.value = 0
    AIN1.off(); AIN2.off()
    BIN1.off(); BIN2.off()

def move_forward(speed=1.0):
    STBY.on()
    AIN1.on(); AIN2.off()
    BIN1.on(); BIN2.off()
    PWMA.value = speed
    PWMB.value = speed

def move_backward(speed=1.0):
    STBY.on()
    AIN1.off(); AIN2.on()
    BIN1.off(); BIN2.on()
    PWMA.value = speed
    PWMB.value = speed

def turn_left(speed=0.5):
    STBY.on()
    AIN1.off(); AIN2.on() # Left motor backward
    BIN1.on(); BIN2.off() # Right motor forward
    PWMA.value = speed
    PWMB.value = speed

def turn_right(speed=0.5):
    STBY.on()
    AIN1.on(); AIN2.off() # Left motor forward
    BIN1.off(); BIN2.on() # Right motor backward
    PWMA.value = speed
    PWMB.value = speed

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # Allow rapid restart without 'Address already in use' errors
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    try:
        sock.bind((UDP_IP, UDP_PORT))
        print(f'UDP server listening on {UDP_IP}:{UDP_PORT}')
        STBY.on() # Wake up the motor driver
        
        while True:
            data, addr = sock.recvfrom(BUFFER_SIZE)
            cmd = data.decode('utf-8').strip().upper()
            
            if cmd == 'F': move_forward(1.0)
            elif cmd == 'B': move_backward(1.0)
            elif cmd == 'L': turn_left(0.6)
            elif cmd == 'R': turn_right(0.6)
            elif cmd == 'S': stop_motors()
            else: stop_motors() # Failsafe for garbage data
            
    except KeyboardInterrupt:
        print('\nShutting down...')
    except Exception as e:
        print(f'Server error: {e}')
    finally:
        stop_motors()
        STBY.off()
        sock.close()
        sys.exit(0)

if __name__ == '__main__':
    main()

Debugging: When the Motors Won't Spin

Embedded hardware rarely works on the first boot. If your Raspberry Pi RC car is unresponsive, do not blindly rewrite your code. Follow this diagnostic path.

The First Three Things to Check When It Fails

  1. STBY Pin Logic Level: Use a multimeter to measure voltage between the STBY pin on the TB6612FNG and GND. It must read ~3.3V when the script is running. If it reads 0V, the chip is in sleep mode. Check your GPIO 25 wiring.
  2. Common Ground: The Pi's GND, the motor driver's GND, and the battery pack's negative terminal must share a common ground bus. Without this, the 3.3V logic signals from the Pi have no reference point and the MOSFETs will not switch.
  3. VM Voltage Under Load: Measure the voltage at the VM (Motor Power) pins on the driver while the motors are commanded to spin. If it drops below 4.5V, your battery pack's internal resistance is too high, or your BMS is tripping the over-current protection.

Ranked Causes for Specific Error Strings

Error 1: lgpio.error: 'gpiochip0' or RuntimeError: Failed to allocate GPIO

  • Cause: Raspberry Pi OS Bookworm switched from rpigpio to lgpio for GPIO access. Your user account lacks permissions to access the character device, or another process is holding the pin.
  • Fix: Ensure your user is in the gpio group (sudo usermod -aG gpio $USER), reboot, and ensure no background scripts (like a previous crashed instance of this code) are holding the pins. Use killall python3 to clear ghosts.

Error 2: OSError: [Errno 98] Address already in use

  • Cause: The UDP socket from a previous run didn't close cleanly, leaving the port bound in a TIME_WAIT state.
  • Fix: The provided code includes sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) which prevents this. If you stripped that line out, put it back. Alternatively, wait 60 seconds for the OS to release the port.

Error 3: Motors whine or click at low speeds but don't turn

  • Cause: The PWM frequency is too low, or the duty cycle is below the motor's stall threshold. DC motors require a minimum voltage to overcome static friction.
  • Fix: In the code, frequency=1000 (1kHz) is set. If you changed this to 50Hz, the motor will whine audibly. Keep it at 1kHz. If it still won't move at 0.2 duty cycle, implement a "kickstart" in your code: send 1.0 duty cycle for 50ms, then drop to your target low speed.

Extending vs. Simplifying the Build

Depending on your budget and use case, you may want to alter the complexity of this Raspberry Pi RC platform.

How to Simplify

  • Drop WiFi for Tethered Control: If you are building an indoor rover and don't want to deal with network latency or battery weight, strip the 18650 pack and power the Pi via a long, 18 AWG tethered USB-C cable. Use a standard USB gamepad and the evdev Python library to read joystick inputs directly. This eliminates the need for a client script entirely.
  • Use a Pi Zero 2 W: If weight is your primary constraint, swap the Pi 4 for a Pi Zero 2 W. It draws roughly 0.7W at idle and costs around $15. The Python code above will run unmodified, though you will need to remap the GPIO pins to match the Zero's 40-pin header layout.

How to Extend

  • Add Traction Control (IMU): Wire an MPU6050 accelerometer/gyroscope via I2C (SDA to GPIO 2, SCL to GPIO 3). By reading the Z-axis acceleration, you can detect when a wheel is slipping (high RPM, low acceleration) and dynamically reduce the PWM duty cycle to that specific motor until grip is restored.
  • Implement Video Telemetry: Mount a Raspberry Pi Camera Module 3. Use libcamera-vid to stream an RTSP feed over the same WiFi network. To prevent video lag from starving your UDP control packets, ensure your router supports QoS (Quality of Service) and prioritize UDP port 5005 traffic over the video stream.
Further Reading:
- gpiozero Official Documentation (Pin factory configurations and PWM devices)
- Pololu TB6612FNG Motor Driver Carrier (Datasheet and wiring diagrams)
- Raspberry Pi 4 Datasheet (Power consumption and GPIO current limits)