To control DC motors with a Raspberry Pi, you cannot wire them directly to the GPIO pins. The Pi's GPIO pins max out at 16mA per pin and lack the voltage to drive inductive loads. You must use a motor driver IC like the TB6612FNG to handle the high current, while the Pi sends 3.3V logic and PWM signals to dictate speed and direction. This guide targets the Raspberry Pi 4 Model B (and Pi 5) running Raspberry Pi OS (Bookworm), using the modern gpiozero library.

Build Profile:
Difficulty: Intermediate (Requires basic breadboarding and Linux terminal familiarity)
Time to Complete: 45 minutes
Target Board: Raspberry Pi 4 Model B / Raspberry Pi 5 (64-bit Bookworm OS)

Why You Need a Motor Driver (And Which One to Pick)

DC motors generate back-EMF (voltage spikes) when they spin down, which will instantly fry your Pi's CPU if connected directly. A motor driver acts as a high-current buffer, using internal flyback diodes to safely dissipate these spikes. While the ancient L298N is still sold everywhere, it uses bipolar junction transistors (BJTs) that waste up to 2V as heat. In 2026, the MOSFET-based TB6612FNG is the standard for hobbyists, dropping only ~0.5V and supporting much higher PWM frequencies.

Motor Driver Comparison Matrix (2026 Hobbyist Standards)
Driver IC Max Continuous Current Voltage Drop Max PWM Freq Avg Price (USD)
TB6612FNG (Recommended) 1.2A (3.2A peak) ~0.5V 100 kHz $4.50 - $6.00
L298N (Legacy) 2.0A (3.0A peak) ~2.0V 25 kHz $3.00 - $5.00
DRV8871 3.6A ~0.4V 50 kHz $7.00 - $9.00
BTS7960 (High Power) 27A (43A peak) ~0.2V 25 kHz $12.00 - $18.00

Parts List & TB6612FNG Pin Mapping

Before wiring, ensure you have the exact components. The TB6612FNG requires a separate power supply for the motors; do not power motors from the Pi's 5V rail.

  • Microcontroller: Raspberry Pi 4 Model B (4GB) or Raspberry Pi 5
  • Motor Driver: TB6612FNG Breakout Board (Pololu #713 or SparkFun #14451)
  • Power Supply: 6V to 12V DC bench supply or battery pack (capable of 2A+)
  • Motors: 1x or 2x standard DC gearmotors (e.g., TT motors, 3V-6V nominal)
  • Wiring: 22 AWG solid core jumper wires for breadboard, 18 AWG stranded for motor terminals

Pin Mapping Table (BCM Numbering)

The Raspberry Pi uses BCM (Broadcom) GPIO numbering in Python. We map the Pi's 3.3V logic pins to the TB6612FNG's input and PWM pins. We specifically use GPIO 12 for PWM because it supports hardware PWM on the Pi, preventing CPU stuttering.

TB6612FNG Pin Raspberry Pi Pin (BCM) Pi Physical Pin # Function
VCC 3.3V Power 1 Logic power for the IC
GND GND 6 Shared logic ground
AIN1 GPIO 17 11 Motor A Direction 1
AIN2 GPIO 27 13 Motor A Direction 2
PWMA GPIO 12 32 Motor A Speed (Hardware PWM)
STBY 3.3V Power 1 Standby (Tied HIGH to enable)
VM External PSU (+) N/A Motor high-side voltage

Step-by-Step Wiring Procedure

SAFETY WARNING: Always disconnect the external motor power supply before changing jumper wires on the breadboard. Accidentally shorting the VM (12V) pin to a Pi GPIO pin will instantly destroy the Pi's SoC.
  1. Establish the Common Ground: Connect a jumper wire from the Raspberry Pi's GND (Physical Pin 6) to the GND rail on your breadboard. Connect the TB6612FNG's GND pin to the same breadboard GND rail. Finally, connect the negative terminal of your external motor power supply to this shared GND rail. Without a shared ground, the Pi's 3.3V logic signals have no reference voltage and the motor will not spin.
  2. Wire Logic Power & Standby: Connect Pi 3.3V (Physical Pin 1) to the TB6612FNG VCC pin. Also, jumper the TB6612FNG STBY (Standby) pin directly to VCC. This forces the chip to remain active. (You can wire STBY to a GPIO pin if you want software-controlled sleep modes, but tying it HIGH is best for basic setups).
  3. Connect Direction & PWM Pins: Wire AIN1 to GPIO 17, AIN2 to GPIO 27, and PWMA to GPIO 12, referencing the table above.
  4. Wire the Motor Power: Connect the positive terminal of your external power supply to the TB6612FNG VM pin. Connect your DC motor wires to the A01 and A02 output terminals. Polarity doesn't matter here; if the motor spins the wrong way, simply swap the two wires in the terminal block.

Complete Python Control Code (gpiozero)

Legacy tutorials often use the RPi.GPIO library, which is deprecated and throws memory errors on modern Raspberry Pi OS (Bookworm). We use gpiozero, which handles hardware PWM cleanly and manages pin cleanup automatically on exit.

Ensure gpiozero is installed via your terminal: sudo apt install python3-gpiozero

import signal
import sys
from time import sleep
from gpiozero import OutputDevice, PWMOutputDevice

# ==========================================
# PIN DEFINITIONS (BCM Numbering)
# ==========================================
PIN_AIN1 = 17    # Direction control 1
PIN_AIN2 = 27    # Direction control 2
PIN_PWMA = 12    # Speed control (Hardware PWM capable)

# ==========================================
# DEVICE INITIALIZATION
# ==========================================
# We use explicit OutputDevices instead of the gpiozero.Motor class
# to maintain direct control over the TB6612FNG's dedicated PWMA pin.
ain1 = OutputDevice(PIN_AIN1)
ain2 = OutputDevice(PIN_AIN2)

# Initialize PWM at 1kHz. The TB6612FNG handles up to 100kHz, 
# but 1kHz-5kHz is the sweet spot to avoid audible motor whine.
pwma = PWMOutputDevice(PIN_PWMA, frequency=1000)

def set_motor(direction: str, speed: float):
    """
    Sets motor direction and speed.
    :param direction: 'forward', 'backward', or 'stop'
    :param speed: Float between 0.0 (off) and 1.0 (100% duty cycle)
    """
    # Clamp speed to valid range
    speed = max(0.0, min(1.0, speed))
    
    if direction == 'forward':
        ain1.on()
        ain2.off()
        pwma.value = speed
    elif direction == 'backward':
        ain1.off()
        ain2.on()
        pwma.value = speed
    elif direction == 'stop':
        ain1.off()
        ain2.off()
        pwma.value = 0
    else:
        raise ValueError("Direction must be 'forward', 'backward', or 'stop'")

def graceful_exit(sig, frame):
    """Safely stops the motor and cleans up GPIO on Ctrl+C."""
    print("\n[INFO] Interrupt received. Stopping motor and cleaning up...")
    set_motor('stop', 0)
    pwma.close()
    ain1.close()
    ain2.close()
    sys.exit(0)

# Catch Ctrl+C to prevent motors from running away after script crashes
signal.signal(signal.SIGINT, graceful_exit)

if __name__ == "__main__":
    try:
        print("[INFO] Starting motor sequence...")
        
        # Ramp up speed forward
        for speed in [0.25, 0.5, 0.75, 1.0]:
            print(f"Moving forward at {speed*100}% speed")
            set_motor('forward', speed)
            sleep(1.5)
            
        # Hard stop
        set_motor('stop', 0)
        sleep(1)
        
        # Reverse at half speed
        print("Moving backward at 50% speed")
        set_motor('backward', 0.5)
        sleep(2)
        
        # Brake (Shorting the motor terminals by setting both HIGH)
        print("Active braking...")
        ain1.on()
        ain2.on()
        pwma.value = 1.0
        sleep(0.5)
        
        set_motor('stop', 0)
        print("[INFO] Sequence complete.")
        
    except Exception as e:
        print(f"[ERROR] Unexpected failure: {e}")
        graceful_exit(None, None)

Debugging: Exact Errors & The First 3 Things to Check

When motors fail to spin or the Pi crashes, beginners often blame the code. 90% of the time, it is a hardware or OS-level configuration issue. If your script fails, check these three things first:

  1. The Shared Ground Loop: Use your multimeter in continuity mode. Check resistance between the Pi's GND pin and the TB6612FNG GND pin. It should read < 1 ohm. If they aren't tied together, the 3.3V logic signal is floating, and the H-bridge won't trigger.
  2. Pi Brownout vs. Motor Draw: If the Raspberry Pi reboots the moment the motor starts spinning, your external power supply is inadequate, or you accidentally wired the motor's VM pin to the Pi's 5V rail. DC motors draw 10x their rated current at startup (stall current). Use a dedicated PSU.
  3. PWM Pin Capability: If the motor whines loudly or stutters at low speeds, you might be using a GPIO pin that only supports software PWM. Ensure PWMA is wired to GPIO 12, 13, 18, or 19, which are tied to the Pi's hardware PWM clocks.

Common Error Strings & Ranked Causes

Error String: RuntimeError: No access to /dev/mem. Try running as root!
Context: This occurs if you copy-pasted legacy RPi.GPIO code from an old forum and try to run it on Raspberry Pi OS Bookworm without sudo.
  • Cause 1 (Most Likely): Using deprecated RPi.GPIO instead of gpiozero. Fix: Rewrite using gpiozero as shown above.
  • Cause 2: Running an outdated script on Bookworm. Fix: Run with sudo python3 script.py (though this is bad practice for security).
Error String / Warning: gpiozero.exc.PWMSoftwareFallback: Falling back to software PWM
Context: gpiozero attempts to use hardware PWM but falls back to software if the pin doesn't support it or if the hardware channels are occupied.
  • Cause 1: You wired PWMA to a non-hardware pin (like GPIO 21). Fix: Move the wire to GPIO 12 or 18.
  • Cause 2: Audio output is enabled, which hogs the hardware PWM channels on older Pi OS versions. Fix: Disable audio in /boot/config.txt by adding dtparam=audio=off.

How to Extend or Simplify the Build

Depending on your project goals, you may want to abstract the wiring or add precision control.

To Simplify: Use an I2C Motor HAT

If breadboarding 15 jumper wires feels tedious, or you need to control 4+ motors, switch to an I2C-based HAT like the Adafruit DC and Stepper Motor HAT. It uses a PCA9685 PWM driver chip, meaning you only need 4 wires (Power, GND, SDA, SCL) to control up to 4 DC motors or 2 steppers. The trade-off is a higher cost (~$25) and the need to manage I2C addresses if you stack multiple boards.

To Extend: Add Quadrature Encoders for PID Control

The TB6612FNG controls power, not speed. If your robot needs to drive in a perfectly straight line, you must add optical or magnetic quadrature encoders to the motor shafts. By reading the encoder pulses via Pi GPIO interrupts, you can implement a PID (Proportional-Integral-Derivative) control loop in Python. This allows the Pi to dynamically adjust the PWM duty cycle to maintain exact RPMs, even when driving up an incline or over carpet. For real-time encoder reading without OS-level latency, consider offloading the PID loop to an Arduino or Teensy and sending high-level velocity commands to it via UART from the Raspberry Pi.