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.
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:
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 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:
Control pins (connect to Pi GPIO):
Motor terminals:
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.
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.
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.
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.
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.
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()
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 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.
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.
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.
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.
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.
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.
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.
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.
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.