A wheeled robot’s relationship with the physical world begins at the motor. Every high-level navigation algorithm ultimately resolves to voltages applied to motor windings. Getting this layer right — understanding why motors behave the way they do and how to command them reliably — makes every layer above it easier.
A brushed DC motor converts electrical power to mechanical rotation. Three quantities govern its behaviour:
Torque $\tau$ is proportional to armature current $I$: $\tau = K_t \cdot I$
where $K_t$ is the torque constant (units: N·m/A).
Back-EMF is a voltage generated by the spinning motor that opposes the supply voltage. It is proportional to rotational speed $\omega$: $V_{back} = K_e \cdot \omega$
where $K_e$ is the back-EMF constant. For a well-designed motor, $K_t \approx K_e$ in SI units.
Speed-torque curve. At a fixed supply voltage $V$, the armature current is: $I = (V - K_e \omega) / R_a$
where $R_a$ is armature resistance. Substituting into the torque equation gives the linear speed-torque relationship: maximum speed (no-load) at zero torque, and maximum torque (stall) at zero speed. A practical operating point sits between these extremes.
The implication for control: the motor speed we command via duty cycle is not the motor speed we get. Load torque, battery voltage droop, and winding resistance all shift the actual speed. This is why Chapter 4 introduces PID feedback.
An H-bridge is a circuit of four switches arranged in an H shape that allows current to flow through the load (motor) in either direction. The L298N and TB6612FNG chips both implement H-bridges.
| IN1 | IN2 | Motor state |
|---|---|---|
| H | L | Forward |
| L | H | Reverse |
| L | L | Coast (free-spin) |
| H | H | Brake (short winding) |
The enable (EN) pin controls the overall gate. Driving EN with a PWM signal at a fixed frequency while holding IN1/IN2 for direction gives proportional speed control.
PWM frequency choice. At very low frequencies (< 100 Hz), you can hear the motor switching (audible whine). At very high frequencies (> 20 kHz), switching losses in the driver increase and the driver may overheat. A good compromise for small gear-motors is 1–5 kHz using hardware PWM, or 50–200 Hz using software PWM (which is acceptable for low-speed manoeuvring but not for smooth audio-quiet operation).
The Pi has two hardware PWM channels on GPIO12 and GPIO13, available via the pigpio or gpiozero libraries. Hardware PWM is generated by dedicated hardware with microsecond precision. Software PWM uses a thread to toggle a pin, which introduces jitter proportional to OS scheduling latency — acceptable at low duty cycles and low frequencies.
The Motor class wraps all of this:
class Motor:
def set_speed(self, speed: float) -> None:
"""Set motor speed. speed in [-1.0, 1.0]."""
speed = max(-1.0, min(1.0, speed))
if abs(speed) < self.deadband:
self.brake()
return
if speed >= 0:
GPIO.output(self.in1, GPIO.HIGH)
GPIO.output(self.in2, GPIO.LOW)
else:
GPIO.output(self.in1, GPIO.LOW)
GPIO.output(self.in2, GPIO.HIGH)
self.pwm.ChangeDutyCycle(abs(speed) * 100.0)
The DifferentialDrive class maps a desired linear velocity $v$ (m/s) and angular velocity $\omega$ (rad/s) to left and right motor speeds:
See code/02_differential_drive.py
Every motor has a dead-band: a range of duty cycles near zero where the applied torque is insufficient to overcome static friction. Below the dead-band threshold, the motor does not move at all. Above it, motion begins abruptly. For a typical N20 gear-motor on a wooden floor, the dead-band duty cycle is 15–30%.
This non-linearity causes problems for low-speed precision manoeuvring and for PID controllers whose output sits near zero. Practical solutions:
Even above the dead-band, the speed-vs-duty-cycle relationship is not linear. A 50% duty cycle does not produce half the no-load speed. The non-linearity arises from the motor’s speed-torque curve and from load variation.
A simple piecewise correction maps commanded normalised speed $u \in [0,1]$ to duty cycle $d$: $d = d_{min} + (1 - d_{min}) \cdot u^\gamma$
where $d_{min}$ is the dead-band duty cycle and $\gamma \approx 0.7$–$1.3$ is fitted to measured data. The calibration script (described next) measures this curve and saves the parameters.
Calibration has two goals:
The interactive calibration script code/02_calibrate_motors.py steps through duty cycles from 0% to 100% in 5% increments. At each step it holds the speed for one second and prompts you to enter the measured wheel speed (from a tachometer, timing marks, or an encoder if fitted). It then fits a correction curve and saves the parameters to a JSON file that DifferentialDrive reads at startup.
See code/02_calibrate_motors.py
A simple correction factor approach — if the left wheel is 8% faster than the right at matched commands — applies a scale factor of $0.92$ to the left motor permanently:
# In RobotConfig
left_speed_scale: float = 0.92
right_speed_scale: float = 1.00
Two safety patterns belong in every motor driver:
Always-stop on exit. Register a cleanup function with Python’s atexit module so that if the program crashes or the user hits Ctrl-C, the motors stop immediately:
import atexit
def _emergency_stop():
motor_left.brake()
motor_right.brake()
GPIO.cleanup()
atexit.register(_emergency_stop)
Current-limit via fault pin. The TB6612FNG has a STBY (standby) pin and can be configured with current-sense resistors. The L298N has a current-sense output. If you detect a stall condition (duty cycle > 80%, speed feedback ≈ 0 for more than 500 ms), cut power and raise an exception. This prevents winding burnout.
With the Motor, DifferentialDrive, and calibration in place, higher-level code can write:
drive = DifferentialDrive(config)
drive.move(linear_mps=0.2, angular_radps=0.0) # straight ahead at 0.2 m/s
time.sleep(2.0)
drive.stop()
The move method converts the desired body velocity into left/right speeds using the kinematic equations derived in Chapter 3, applies the calibration correction factors, and writes the resulting duty cycles to the motor PWM channels. Every subsequent chapter will call drive.move() as the primary actuation interface; the hardware details are now safely encapsulated.
Navigation: