The plan: a robot car

A robot car is a small wheeled robot, usually with two driven wheels and a caster, built from a chassis, two DC gear motors, a motor driver, a microcontroller board, a battery pack and a few sensors. It is the usual first robot because every part of robotics shows up in it — motors, sensors, control, power — at low voltage, low cost and low risk. With two driven wheels it steers like the differential-drive robots in warehouses.

A typical parts list:

  • a chassis kit with two DC gear motors (3–6 V) and a caster;
  • a motor driver board (see below);
  • a microcontroller board: ESP32 or Raspberry Pi Pico;
  • a battery pack for the motors (AA cells or a 2-cell LiPo) and a way to power the board;
  • a main switch, a fuse and an emergency stop;
  • an ultrasonic distance sensor such as the HC-SR04, and later wheel encoders (see Sensors for Robots).

This page is a plan, not a wiring guide: check each part's datasheet and pinout before connecting it. Everything here runs on batteries or a certified low-voltage power supply.

The power plan of a robot car. Motor power runs from the battery pack through a fuse, then a main switch and emergency stop, to the motor driver, which drives the two DC gear motors. The microcontroller board gets its own supply, from a regulator or a separate logic battery, and sends PWM and direction signals to the motor driver. Battery, regulator, board and driver share a common ground. motor power battery pack motors fuse main switch + emergency stop motor driver TB6612FNG 2 DC gear motors regulator or logic battery ESP32 or Pico or from the pack PWM, direction common ground (GND) shared by all parts
Motor power goes through a fuse close to the battery, then the main switch with the emergency stop; the board has its own supply and only sends signals to the driver. All grounds are connected, so the signals have a shared reference.

Choosing a board

For a first robot car, a microcontroller is the simplest brain: it starts in a second, controls pins with predictable timing and runs for hours on a small battery. The IoT topic compares the boards; for motors, their PWM hardware matters most:

  • ESP32: the original ESP32 has 16 LEDC PWM channels in two groups of 8, plus an MCPWM peripheral meant for motor control (brushed and brushless DC motors, RC servos). In MicroPython it offers 16 PWM channels with up to 8 different frequencies, from 1 Hz to 40 MHz, on any output pin. Wi-Fi and Bluetooth make remote control easy.
  • Raspberry Pi Pico: the RP2040 has 8 PWM slices that reach all 30 GPIO pins; the RP2350 on the Pico 2 has 12.
  • Raspberry Pi (a Linux computer): hardware PWM on GPIO12, 13, 18 and 19, and room for a camera and heavier software. With the gpiozero library, a Robot takes two Motor objects (PhaseEnableRobot is a deprecated alias). On the Pi 5, only the lgpio pin factory works, so older tutorials that use RPi.GPIO fail there; lgpio's software-timed servo pulses are meant only for testing.

A Raspberry Pi is the natural choice once you want a camera or ROS 2 on the robot. Note that ROS 2's Tier 1 platforms are Ubuntu and Windows; Raspberry Pi OS, which is based on Debian, is not (see ROS 2). Many builders combine both: a microcontroller for the motors and a Pi for vision and planning.

Choosing a motor driver

A GPIO pin delivers a few milliamps at 3.3 V; a small gear motor needs hundreds of milliamps, more when it starts, and must run in both directions. The Raspberry Pi documentation is blunt: "Do not connect motors directly to the GPIO pins, instead use an H-bridge circuit or a motor controller board". A motor driver contains one H-bridge per motor (see Motors and Drives): the board sends direction signals and a PWM signal whose duty cycle sets the speed. Three drivers appear in most kits:

Driver Type Motor supply Current per channel
TB6612FNG (Toshiba) MOSFET 2.5–13.5 V (15 V absolute max.) 1.0 A average (1.2 A absolute max.), 3.2 A peak (pulsed)
DRV8833 (TI) MOSFET 2.7–10.8 V 1.5 A RMS in the PWP and RTY packages, 500 mA RMS in the PW package; 2 A peak
L298 (ST), on "L298N" boards bipolar up to 46 V 2 A DC (4 A total)

The L298 is old and inefficient: its bipolar transistors lose a lot of voltage. Its datasheet gives a total drop of 1.80–3.2 V at 1 A and up to 4.9 V at 2 A. For the 3–6 V motors of a robot car, that is most of the battery:

V_\text{motor} = V_\text{battery} - V_\text{drop}
# What reaches the motor through an L298 driver, from a 6 V battery pack.
# Total voltage drop from the L298 datasheet (ST): 1.80-3.2 V at 1 A,
# up to 4.9 V at 2 A.
BATTERY_V = 6.0

for current_a, drop_v in ((1.0, 1.8), (1.0, 3.2), (2.0, 4.9)):
    motor_v = BATTERY_V - drop_v
    heat_w = drop_v * current_a          # power lost in the driver
    print(f"{current_a:.0f} A, drop {drop_v:.1f} V -> motor gets {motor_v:.1f} V "
          f"({motor_v / BATTERY_V:.0%}), driver turns {heat_w:.1f} W into heat")
1 A, drop 1.8 V -> motor gets 4.2 V (70%), driver turns 1.8 W into heat
1 A, drop 3.2 V -> motor gets 2.8 V (47%), driver turns 3.2 W into heat
2 A, drop 4.9 V -> motor gets 1.1 V (18%), driver turns 9.8 W into heat

A MOSFET driver such as the TB6612FNG or DRV8833 loses far less, so more of the battery reaches the motors and less becomes heat. Choose one of them for 3–6 V motors, and check that the board's current rating — for the DRV8833, which package the breakout board uses — covers your motors' start-up current.

Flyback diodes

A motor is a coil, and a coil resists sudden changes in current. When a driver switches a running motor off, the motor's current keeps flowing for a moment and produces a voltage spike that can destroy transistors. A flyback diode is a diode placed across an inductive load such as a motor or relay coil (or across the switches that drive it) that gives this current a safe path when the switch opens, so the spike cannot build up. The L298 needs them as external parts — its datasheet says "An external bridge of diodes are required" — so check whether an L298N board has them. For any other driver, check in its datasheet how it handles the current from the motor.

PWM and logic levels

The board controls each motor with two direction pins and one PWM output. The logic is small enough to test without hardware; here it is as plain Python, with the same 0–65535 range that MicroPython's duty_u16() uses:

# A Python version of the motor-command logic for one channel of a
# TB6612FNG-style driver: two direction pins (IN1, IN2) and a PWM duty.
# MicroPython's duty_u16() takes 0-65535 for 0-100% duty.
def motor_signals(speed):
    """speed from -1.0 (full reverse) to 1.0 (full forward)."""
    speed = max(-1.0, min(1.0, speed))       # clamp bad input
    in1 = 1 if speed > 0 else 0
    in2 = 1 if speed < 0 else 0
    duty = int(abs(speed) * 65535)
    return in1, in2, duty

for speed in (1.0, 0.5, 0.0, -0.25, -3.0):
    in1, in2, duty = motor_signals(speed)
    print(f"speed {speed:+.2f} -> IN1={in1} IN2={in2} duty_u16={duty}")
speed +1.00 -> IN1=1 IN2=0 duty_u16=65535
speed +0.50 -> IN1=1 IN2=0 duty_u16=32767
speed +0.00 -> IN1=0 IN2=0 duty_u16=0
speed -0.25 -> IN1=0 IN2=1 duty_u16=16383
speed -3.00 -> IN1=0 IN2=1 duty_u16=65535

The clamp on the first line matters: a bug elsewhere that asks for speed −3.0 gets full reverse, not a crash or a wrapped-around number. On the board, the same function drives real pins. The firmware below also stops the motor when commands stop arriving — a software timeout, useful when a remote control loses its connection, but not a replacement for the hardware emergency stop:

from machine import Pin, PWM
import time

# Left motor on a TB6612FNG: two direction pins, one PWM pin, standby pin.
AIN1 = Pin(25, Pin.OUT, value=0)
AIN2 = Pin(26, Pin.OUT, value=0)
# Start stopped; 1000 Hz is an example, check the driver's datasheet.
PWMA = PWM(Pin(27), freq=1000, duty_u16=0)
STBY = Pin(33, Pin.OUT)
STBY.value(1)                    # driver active

TIMEOUT_MS = 500                 # stop if no command for half a second
last_command = time.ticks_ms()

def left_motor(speed):
    speed = max(-1.0, min(1.0, speed))
    AIN1.value(1 if speed > 0 else 0)
    AIN2.value(1 if speed < 0 else 0)
    PWMA.duty_u16(int(abs(speed) * 65535))

def on_command(speed):           # called by your remote-control code
    global last_command
    last_command = time.ticks_ms()
    left_motor(speed)

while True:
    if time.ticks_diff(time.ticks_ms(), last_command) > TIMEOUT_MS:
        left_motor(0)
    time.sleep_ms(20)

MicroPython and the other ways to program these boards are explained in Firmware and Embedded Programming, including the watchdog timer that restarts a board whose code hangs.

Logic levels and the ultrasonic sensor

The ESP32, the Pico and the Raspberry Pi all use 3.3 V logic levels; Raspberry Pi inputs are "3.3V-tolerant" only. The popular HC-SR04 ultrasonic sensor runs on 5 V, and its echo output is a 5 V signal. Connect it through a level shifter or a voltage divider of two resistors:

V_\text{out} = V_\text{in} \cdot \dfrac{R_2}{R_1 + R_2} \qquad 5\,\text{V} \cdot \dfrac{3300}{2200 + 3300} = 3.0\,\text{V}
# A voltage divider for the HC-SR04's 5 V echo pin, so a 3.3 V input
# never sees more than it tolerates. R1 is the upper resistor (from echo),
# R2 the lower one (to ground); the input sits between them.
ECHO_V = 5.0
LIMIT_V = 3.3

def divider(v_in, r1, r2):
    return v_in * r2 / (r1 + r2)

for r1, r2 in ((1_000, 2_000), (2_200, 3_300), (1_000, 1_000)):
    v_out = divider(ECHO_V, r1, r2)
    verdict = "ok" if v_out <= LIMIT_V else "too high"
    print(f"R1 {r1:5} ohm, R2 {r2:5} ohm -> {v_out:.2f} V  {verdict}")
R1  1000 ohm, R2  2000 ohm -> 3.33 V  too high
R1  2200 ohm, R2  3300 ohm -> 3.00 V  ok
R1  1000 ohm, R2  1000 ohm -> 2.50 V  ok

The popular 1 kΩ / 2 kΩ pair lands just above 3.3 V; 2.2 kΩ / 3.3 kΩ leaves a margin. Do not go too low either: the input must still read the divided signal as a logic high. The 1 kΩ / 1 kΩ pair gives 2.5 V, just above the ESP32's minimum of about 2.48 V (0.75 × 3.3 V), which leaves no margin; check the threshold in the board's datasheet. How the sensor turns an echo time into a distance is on Sensors for Robots.

Power for motors and logic

Motors are electrically noisy and draw a large current when they start or stall. If the board shares a weak supply with them, every start pulls the voltage down for a moment. The usual plan, shown in the diagram above:

  • Separate supplies for motors and logic: a battery pack for the motor driver, and a separate battery or a good regulator for the board.
  • A common ground: connect the ground of the motor supply, the driver and the board, so that the PWM and direction signals have the same reference on both sides.
  • A main switch and a fuse in the motor supply, so a short circuit or a stalled motor cannot overheat wires or the battery.

Brownouts

A brownout is a short dip of the supply voltage below the level a circuit needs to work reliably, without a full power cut. Microcontrollers watch for it because code running on too little voltage can misbehave in unpredictable ways. The ESP32 has a brownout detector that "can trigger a system reset if the supply voltage goes below a safe level"; it then prints:

Brownout detector was triggered

On a robot car, the motors' start-up current is often the cause: the board resets every time the car starts moving. The fix is the power plan above — separate supplies, a regulator with enough current, a charged battery — not switching the detector off. More causes and measurements are on Power and Batteries.

Emergency stop

An emergency stop is a control that any person can operate to stop a machine quickly in a dangerous situation, and that keeps it stopped until it is deliberately reset. On a robot car it is a switch within easy reach that cuts the power to the motors in hardware — not a button in an app, and not a line of code that could hang with the rest of the program. Fit it before the first motor turns; in the diagram it sits next to the main switch, between the fuse and the driver; put the fuse as close to the battery as possible, so every wire after it is protected. Industrial robots take the same idea much further (see Robot Safety and Standards).

LiPo batteries

A LiPo battery is a lithium polymer battery: a kind of lithium-ion battery in a soft pouch instead of a metal can, popular in robots and drones because it is light and can deliver large currents. Each cell is 3.7 V nominal and 4.2 V when full; a pack of two cells in series (2S) is 7.4 V nominal. Many first builds use a pack of AA cells instead, which avoids LiPo handling altogether; if you choose LiPo, follow the rules university safety offices give for them:

  • Do not discharge a cell below about 3.0 V; best practice is to stop at about 3.5 V.
  • Store packs at about 3.8 V per cell.
  • Charge only with a balancing charger made for LiPo packs.
  • Never charge unattended or overnight, and charge in a LiPo bag or a metal or ceramic container, away from anything that burns.
  • Set swollen or damaged packs apart and do not use or charge them (see also battery safety).

Balance charging

In a pack of several cells in series, the cells never age exactly alike, and a charger that only watches the total voltage can push one cell above 4.2 V while another is still low. Balance charging is charging a multi-cell lithium pack with a charger that measures every cell through the pack's separate balance connector and evens out their voltages, so no single cell is overcharged. A LiPo pack therefore has two plugs: the thick main lead and the small balance lead, and both go into the charger.

A power budget

How long does the car drive? The energy budget method from the IoT topic works here too. A 2S pack of 2200 mAh stores

E = Q \cdot V = 2.2\,\text{Ah} \times 7.4\,\text{V} = 16.28\,\text{Wh}

and with an average current I and only 80% of the capacity used, to stay above the stop voltage, it runs for

t = \dfrac{0.8 \cdot Q}{I} = \dfrac{0.8 \cdot 2200\,\text{mAh}} {1050\,\text{mA}} \approx 1.68\,\text{h} \approx 101\,\text{min}
# Energy and run-time budget for a robot car on a 2-cell (2S) LiPo pack.
CELLS = 2
CAPACITY_MAH = 2200
NOMINAL_V, FULL_V, STORAGE_V, STOP_V = 3.7, 4.2, 3.8, 3.5   # per cell

print(f"pack: nominal {CELLS * NOMINAL_V:.1f} V, full {CELLS * FULL_V:.1f} V, "
      f"storage {CELLS * STORAGE_V:.1f} V, stop at {CELLS * STOP_V:.1f} V")
print(f"energy: {CAPACITY_MAH / 1000 * CELLS * NOMINAL_V:.2f} Wh")

# Average currents: assumed values for this example; measure your own.
loads_ma = {"left motor": 400, "right motor": 400, "board and sensor": 250}
total_ma = sum(loads_ma.values())
USABLE = 0.8          # plan with 80% of the capacity, to stay above STOP_V
minutes = USABLE * CAPACITY_MAH / total_ma * 60
print(f"average current {total_ma} mA -> about {minutes:.0f} minutes of driving")
pack: nominal 7.4 V, full 8.4 V, storage 7.6 V, stop at 7.0 V
energy: 16.28 Wh
average current 1050 mA -> about 101 minutes of driving

A 7.4 V pack suits a TB6612FNG (2.5–13.5 V) or a DRV8833 (up to 10.8 V; a full 2S pack is 8.4 V), but it is more than 3–6 V motors are made for: limit the PWM duty cycle in the firmware, or choose motors rated for the pack. The stop voltage is a job for the firmware too — measure the pack voltage through a divider and stop driving at 7.0 V.

Testing motion code safely

A robot is the place where a bug moves furniture. A few habits keep the first runs boring:

  • Wheels off the ground first. Put the car on a box so the wheels spin freely, and check direction and speed of each motor before it drives.
  • Stop within reach. Keep a hand near the emergency stop for every first run of new code, and start with low speeds.
  • A safe default. Motors off at power-up, after a reset and when commands stop, as in the firmware above.
  • Never run untested motion code. Code an AI assistant wrote for you can look right and still drive the wrong way at full speed. Read it, test the pure logic first (as in the Python example above), and never run it on real hardware without a stop within reach (see Risks of Vibe Coding).
  • Handle boards with care. Follow the usual ESD precautions, and disconnect the battery before changing wires.

Next steps

Once the car drives and stops reliably, each next step is a page of this topic:

  • Add wheel encoders and keep track of where the car is with odometry.
  • Drive straight and follow a line with feedback control (see Motion Control and PID).
  • Plan a route around obstacles (see Path Planning).
  • Connect the car to ROS 2. micro-ROS runs a ROS 2 client on microcontrollers and officially supports the ESP32 (with FreeRTOS and ESP-IDF) and the Raspberry Pi Pico. Its ESP-IDF component has branches for Humble, Jazzy, Kilted and Rolling, but no Lyrical branch yet (as of October 2026), so pair it with one of those distributions.