Part
2
  |  
The Hardware Layer
  |  
Chapter
9

Motors and Motor Drivers

Just wire the motor directly to the GPIO pin — said nobody who wants their Pi to survive the afternoon.
Reading Time
11
mins
BACK TO RASPBERRY PI MASTERCLASS

The trap is connecting a motor directly to a GPIO pin. You wire it up, call GPIO.output(pin, HIGH), and maybe the motor twitches. Maybe it doesn't spin at all. Maybe it spins for a second, then the Pi reboots. Or — in the worst case — a faint burning smell tells you the GPIO pin just died. The problem is current. A GPIO pin can source 16 milliamps. A small hobby DC motor draws 200 milliamps at no load and can spike to over an amp when it stalls. You're asking a garden hose to feed a fire hydrant.

A GPIO pin sources 16 milliamps. A DC motor draws 200 milliamps at no load and spikes past an amp at stall. Connecting them directly is asking a garden hose to feed a fire hydrant.

I've seen this pattern where someone wires a motor to a Pi for a robotics project, it works on the bench for ten minutes, and then the Pi starts rebooting randomly. The motor's current draw causes the 5V supply to sag below the Pi's minimum operating voltage, and the brownout detector resets the processor. The motor is fine. The Pi is not. The fix is always the same: put a driver between them.

The Driver Shield

Framework · The Driver Shield

Never let your Pi talk directly to a motor. Always place a driver circuit between the GPIO pins and the motor. The Pi sends low-current control signals; the driver switches high-current power from an external supply. The GPIO pin is the brain. The driver is the muscle. The external power supply is the fuel.

This separation of concerns is the same pattern you use when your web app talks to a database through a connection pool instead of opening raw sockets. The connection pool handles the heavy lifting (managing connections, timeouts, retries) while your app just says "query this." The motor driver handles the heavy lifting (switching amps of current, managing inductive kickback, heat dissipation) while your Pi just says "forward" or "stop."

The three elements are always the same:

  1. The Pi — sends control signals (HIGH/LOW on GPIO pins, or PWM for speed control). Total current draw from the Pi: negligible.
  2. The driver — receives the control signals and switches power from the external supply to the motor. It's a high-current switch controlled by a low-current signal.
  3. The external power supply — batteries, a wall adapter, or a bench supply. This provides the actual current the motor needs. The Pi's 5V rail is not this supply.
Key takeaway

The Pi is the brain, the driver is the muscle, and the external power supply is the fuel. Never skip the muscle — a GPIO pin cannot supply the current a motor demands.

The L298N Motor Driver Board

The L298N is the standard beginner motor driver for good reason: it's cheap ($3-5), it handles two DC motors simultaneously, it supports direction and speed control, and it's nearly indestructible. The board contains an L298N H-bridge integrated circuit, which is a circuit that can drive current through a motor in either direction — forward or reverse — by activating different pairs of transistors.

Here's what's on the board:

Power terminals:

  • 12V (or VIN) — connect your external motor power supply here. Despite the label, this accepts 5V to 35V. Match it to your motor's voltage rating.
  • GND — ground. This must be connected to both your external supply's ground and the Pi's ground. Common ground is non-negotiable — without it, the control signals have no reference point.
  • 5V — this is an output, not an input (when the onboard voltage regulator is enabled via the jumper). It provides 5V regulated from your 12V input. You can use it to power the Pi if your external supply is 7-12V, but I don't recommend it for beginners — a separate Pi power supply is safer.

Control pins (connect to Pi GPIO):

  • IN1, IN2 — direction control for Motor A. Set IN1=HIGH, IN2=LOW for forward. IN1=LOW, IN2=HIGH for reverse. Both LOW = stop. Both HIGH = brake (motor coasts to a stop).
  • IN3, IN4 — same, for Motor B.
  • ENA — enable/speed for Motor A. With the jumper on, the motor runs at full speed whenever IN1/IN2 are set. Remove the jumper and connect a GPIO pin here — send PWM to control speed.
  • ENB — same, for Motor B.

Motor terminals:

  • OUT1, OUT2 — connect Motor A here.
  • OUT3, OUT4 — connect Motor B here.
The common ground rule

The Pi's GND and the external power supply's GND must be connected. If they're not, the Pi's GPIO signals have no common reference voltage with the driver, and the driver interprets random noise as control signals. I've seen motors spin wildly, reverse randomly, or not respond at all because someone forgot this single wire. One jumper from the Pi's GND pin to the driver's GND terminal. Always.

Wiring the Circuit

Here's the complete wiring for a single DC motor controlled by the Pi through an L298N:

Pi to driver (control signals):

Pi GPIO 17  →  L298N IN1
Pi GPIO 27  →  L298N IN2
Pi GPIO 18  →  L298N ENA  (remove the ENA jumper first)
Pi GND      →  L298N GND

External power to driver:

Battery (+)  →  L298N 12V terminal
Battery (-)  →  L298N GND terminal

Driver to motor:

L298N OUT1  →  Motor wire A
L298N OUT2  →  Motor wire B

The motor wire polarity doesn't matter for the initial setup. If the motor spins the wrong direction when you call "forward," just swap the two motor wires at OUT1/OUT2. That's easier than changing the code.

Controlling a Motor with gpiozero

gpiozero's Motor class is the high-level interface. It takes two GPIO pins (forward and backward) and gives you methods for direction and speed:

from gpiozero import Motor
from time import sleep

# Motor A: forward on GPIO 17, backward on GPIO 27, enable on GPIO 18
motor = Motor(forward=17, backward=27, enable=18)

# Full speed forward
motor.forward()
print("Forward at full speed")
sleep(3)

# Full speed reverse
motor.backward()
print("Reverse at full speed")
sleep(3)

# Stop
motor.stop()
print("Stopped")
sleep(1)

# Half speed forward (speed is 0.0 to 1.0)
motor.forward(speed=0.5)
print("Forward at 50% speed")
sleep(3)

# Quarter speed reverse
motor.backward(speed=0.25)
print("Reverse at 25% speed")
sleep(3)

motor.stop()
print("Done")

The speed parameter uses PWM on the enable pin. motor.forward(speed=0.5) sets IN1=HIGH, IN2=LOW (direction), and sends a 50% duty cycle PWM signal to ENA (speed). The motor receives full voltage during the HIGH portion and zero during the LOW portion, resulting in approximately half the rotational speed.

motor.forward(speed=0.5) is one line of Python that orchestrates three GPIO pins — two for direction, one for PWM speed control — through a high-current driver board powered by an external supply. That's three layers of hardware abstraction in a single method call.

Speed Control: The PWM Connection

Motor speed control is the same PWM concept from Chapter 8, applied to the enable pin instead of an LED. The duty cycle determines the average voltage the motor receives, which determines the speed:

from gpiozero import Motor
from time import sleep

motor = Motor(forward=17, backward=27, enable=18)

# Accelerate from stop to full speed
print("Accelerating...")
for speed in range(0, 101, 5):
    motor.forward(speed=speed / 100)
    print(f"  Speed: {speed}%")
    sleep(0.2)

sleep(1)

# Decelerate from full speed to stop
print("Decelerating...")
for speed in range(100, -1, -5):
    motor.forward(speed=speed / 100)
    print(f"  Speed: {speed}%")
    sleep(0.2)

motor.stop()
print("Stopped")

This produces a smooth ramp-up and ramp-down. The motor starts slowly, reaches full speed, pauses, then slows back down. The acceleration curve is linear because the duty cycle steps are equal, but real applications often use logarithmic or S-curve acceleration profiles for smoother mechanical behavior.

Minimum duty cycle

Most DC motors won't spin below about 20-30% duty cycle. Below that threshold, the PWM pulses don't provide enough energy to overcome static friction and the motor's electromagnetic inertia. The motor hums but doesn't rotate. If you need precise low-speed control, consider a geared motor — the gear reduction trades top speed for torque, allowing the motor to spin at lower effective speeds.

Direction Control Without gpiozero

If you need more control over the timing or want to understand what gpiozero does underneath, here's the same motor control with RPi.GPIO:

import RPi.GPIO as GPIO
from time import sleep

# Pin assignments
IN1 = 17
IN2 = 27
ENA = 18

GPIO.setmode(GPIO.BCM)
GPIO.setup(IN1, GPIO.OUT)
GPIO.setup(IN2, GPIO.OUT)
GPIO.setup(ENA, GPIO.OUT)

# Create PWM on the enable pin at 1kHz
pwm_speed = GPIO.PWM(ENA, 1000)
pwm_speed.start(0)

def motor_forward(speed_percent):
    """Drive motor forward at the given speed (0-100)."""
    GPIO.output(IN1, GPIO.HIGH)
    GPIO.output(IN2, GPIO.LOW)
    pwm_speed.ChangeDutyCycle(speed_percent)

def motor_backward(speed_percent):
    """Drive motor backward at the given speed (0-100)."""
    GPIO.output(IN1, GPIO.LOW)
    GPIO.output(IN2, GPIO.HIGH)
    pwm_speed.ChangeDutyCycle(speed_percent)

def motor_stop():
    """Stop the motor."""
    GPIO.output(IN1, GPIO.LOW)
    GPIO.output(IN2, GPIO.LOW)
    pwm_speed.ChangeDutyCycle(0)

try:
    motor_forward(75)
    print("Forward at 75%")
    sleep(3)

    motor_stop()
    print("Stopped")
    sleep(1)

    motor_backward(50)
    print("Backward at 50%")
    sleep(3)

    motor_stop()
    print("Done")

finally:
    pwm_speed.stop()
    GPIO.cleanup()
✕ gpiozero Motor
  • Three-line setup
  • Speed as 0.0-1.0 float
  • Auto cleanup
  • Good for most projects
✓ RPi.GPIO manual
  • Manual pin setup and PWM creation
  • Speed as 0-100 integer
  • Must call cleanup()
  • Needed for custom PWM frequency or multi-motor coordination

Back-EMF and Flyback Protection

Motors are inductors — coils of wire wrapped around a magnetic core. When you switch a motor off, the collapsing magnetic field generates a voltage spike in the opposite direction. This is called back-EMF (electromotive force), and it can be several times higher than the supply voltage. A 6V motor can generate a 40V spike when abruptly disconnected.

The L298N board has built-in flyback diodes that clamp these spikes and protect the driver circuit. If you're ever building a custom driver circuit without a pre-built board — driving a relay, solenoid, or motor through a bare MOSFET transistor — you must add flyback diodes yourself. A 1N4007 diode placed in reverse across the motor terminals (cathode to positive, anode to negative) absorbs the spike.

Relay modules need this too

Relay coils are inductors. Switching a relay off generates the same back-EMF spike as a motor. Most relay modules have the flyback diode built in, but cheap modules sometimes omit it. Check the board or add your own. A single voltage spike through an unprotected GPIO pin will kill it.

Without flyback protection, the voltage spike travels back through the driver, through the wiring, and can reach the Pi's GPIO pins. One spike won't always kill the pin — but repeated spikes will. I've seen this pattern where a Pi controls a relay for weeks and then one GPIO pin stops responding. The pin isn't burned in the obvious way. It's been gradually degraded by cumulative inductive spikes that chipped away at the transistor's gate oxide, one switching event at a time. Flyback diodes cost pennies and prevent this entirely. Add them to every inductive load, every time, without exception.

A Complete Motor Control Script

Here's a practical script that ties together direction control, speed ramping, and clean shutdown — the kind of thing you'd use as the motor module in a robotics project:

from gpiozero import Motor
from time import sleep
import sys

class MotorController:
    """Wrapper for a single DC motor with acceleration control."""

    def __init__(self, forward_pin, backward_pin, enable_pin):
        self.motor = Motor(
            forward=forward_pin,
            backward=backward_pin,
            enable=enable_pin
        )
        self.current_speed = 0.0

    def accelerate_to(self, target_speed, direction="forward", step=0.05, delay=0.05):
        """Ramp to target speed with controlled acceleration."""
        drive = self.motor.forward if direction == "forward" else self.motor.backward

        current = self.current_speed
        while abs(current - target_speed) > step:
            if current < target_speed:
                current += step
            else:
                current -= step
            current = max(0.0, min(1.0, current))
            drive(speed=current)
            sleep(delay)

        drive(speed=target_speed)
        self.current_speed = target_speed

    def emergency_stop(self):
        """Immediate stop — no deceleration."""
        self.motor.stop()
        self.current_speed = 0.0

    def graceful_stop(self, step=0.05, delay=0.05):
        """Decelerate to zero, then stop."""
        self.accelerate_to(0.0, step=step, delay=delay)
        self.motor.stop()


if __name__ == "__main__":
    mc = MotorController(
        forward_pin=17,
        backward_pin=27,
        enable_pin=18
    )

    try:
        print("Accelerating forward to 80%...")
        mc.accelerate_to(0.8, direction="forward")
        sleep(2)

        print("Decelerating to 30%...")
        mc.accelerate_to(0.3, direction="forward")
        sleep(2)

        print("Graceful stop...")
        mc.graceful_stop()
        sleep(1)

        print("Accelerating backward to 60%...")
        mc.accelerate_to(0.6, direction="backward")
        sleep(2)

        print("Emergency stop!")
        mc.emergency_stop()

    except KeyboardInterrupt:
        print("\nInterrupted — emergency stop")
        mc.emergency_stop()

    print("Done.")

The MotorController class adds what raw motor control lacks: acceleration curves and a distinction between emergency stops (immediate) and graceful stops (decelerated). In a real robot, slamming from full speed to zero puts mechanical stress on gears, wheels, and chassis. Controlled deceleration extends the life of every mechanical component in the system.

Key takeaway

Motor control is three layers: the Pi sends direction and speed signals through GPIO, the driver board switches high current from an external power supply, and the motor converts electrical energy to rotation. Skip any layer and something breaks — either the Pi, the motor, or both.

What to Do Monday Morning

Get an L298N driver board and a DC motor

These are $3-5 each from any electronics supplier. Get a 6V DC hobby motor and a 4xAA battery holder (6V total). Don't use the Pi's 5V pin to power the motor — even through the driver. Use the batteries.

Wire the circuit

Connect GPIO 17 to IN1, GPIO 27 to IN2, GPIO 18 to ENA (with the jumper removed). Connect the battery pack to the driver's 12V and GND terminals. Connect the Pi's GND to the driver's GND. Connect the motor to OUT1 and OUT2. Triple-check the common ground connection before powering anything on.

Run the basic forward/backward script

Use the gpiozero Motor example. Confirm the motor spins forward, stops, spins backward, and stops. If the direction is backward, swap the motor wires at OUT1/OUT2. Don't change the code — rewiring is faster and builds the habit of debugging hardware at the hardware level.

Run the speed ramp

Execute the acceleration/deceleration loop. Watch the motor speed up and slow down smoothly. Try different step sizes and delays to feel the difference between abrupt and gradual acceleration. A step of 0.01 with a delay of 0.02 gives silky-smooth ramping.

Test the MotorController class

Run the complete motor control script. Test both graceful_stop() and emergency_stop(). Listen to the motor — a graceful stop should sound like a car coasting to a halt, while an emergency stop should sound like slamming the brakes. If both sound the same, your step size is too large.

Add a button for emergency stop

Combine the motor controller with a button from Chapter 7. Wire a button to GPIO 22 and set button.when_pressed = mc.emergency_stop. Now you have a physical kill switch — the first piece of real safety engineering in your hardware toolkit.

Motor control is where software meets mechanical engineering — and the driver board is the translator that keeps the Pi alive through the conversation.