A collision-avoiding rover is the ideal first mobile robot project because it combines every foundational robotics skill in a single, immediately satisfying build: wiring a DC motor driver to control two drive motors, connecting an ultrasonic distance sensor to detect obstacles, and writing a behavior loop that drives forward until an obstacle is detected then steers around it — producing a robot that navigates autonomously through any open space without any remote control or pre-programmed path.
Introduction
This is the article where everything you’ve learned about electronics, components, and microcontrollers comes together into something that actually moves, senses, and makes decisions. A collision-avoiding rover is not the most sophisticated robot in the world, but it is genuinely autonomous — it drives itself, senses its environment, and reacts to what it finds without any human input after the power switch is flipped. Watching it navigate around your kitchen table for the first time is one of those experiences in robotics that makes the hours of learning feel completely worthwhile.
More importantly, the skills you build here form the foundation for every robot that follows. The motor wiring you’ll do here is the same wiring you’ll use for a line follower, a robot arm, and a wheeled navigation platform. The sensor-to-behavior loop you’ll write is the same pattern used in sophisticated autonomous vehicles — just with more sensors and more complex responses. Getting it right on a simple project is how you get it right on complex ones.
This article is a complete, step-by-step build guide. By the end, you will have a working robot. Along the way, you will understand not just what to do but why each piece is the way it is — so you can adapt it, modify it, and use it as the starting point for your own designs.
What You Will Build
A two-wheeled differential drive rover that:
- Drives forward continuously in open space
- Detects obstacles using an HC-SR04 ultrasonic distance sensor
- Stops when an obstacle is within 20cm
- Backs up briefly, turns to one side, then resumes forward motion
- Repeats this cycle autonomously for as long as the battery lasts
Top view of completed rover:
┌──────────────────────┐
│ [HC-SR04 sensor] │ ← faces forward
│ ( ) ( ) │ eyes of the robot
│ │
│ [Arduino Uno] │
│ │
│ [L298N driver] │
│ │
[Motor]─┤ ├─[Motor]
[Wheel] │ [Battery pack] │ [Wheel]
└──────────────────────┘
(caster)
↓
front of travel
Components List
Gather these components before starting. Links to specific products aren’t provided since availability varies by region, but these are standard components available from any electronics supplier (Amazon, eBay, AliExpress, Adafruit, SparkFun, local electronics shops):
Required:
- Arduino Uno (or Arduino Nano with appropriate wiring adjustments)
- L298N motor driver module (dual H-bridge, with heatsink)
- HC-SR04 ultrasonic distance sensor
- 2× DC gear motors with wheels (yellow “TT” motors are ideal — ~200 RPM at 6V, widely available in packs)
- Robot chassis kit (acrylic or metal 2-wheel chassis, includes mounting hardware for motors and caster)
- 4× AA battery holder (6V) OR 6× AA holder (9V) OR 2S LiPo battery with JST connector
- Jumper wires (at least 20 male-to-male and 10 male-to-female)
- Small breadboard (optional — for prototyping before final wiring)
- 9V battery snap connector OR DC barrel jack (to power Arduino from same battery pack)
- USB cable (for uploading code)
Optional but recommended:
- Small power switch (to cut battery power without unplugging)
- Cable ties (for securing wires inside chassis)
- Hot glue gun (for securing sensor and components)
- Multimeter (for verifying wiring before power-up)
Total cost estimate: $15–35 USD depending on sourcing and whether you buy a chassis kit or build your own.
Why These Components?
TT gear motors: Inexpensive, widely available, and perfectly matched to small chassis. The 1:48 gear ratio reduces motor speed from ~10,000 RPM to ~200 RPM while multiplying torque — enough to drive a small robot across most surfaces reliably.
L298N motor driver: Handles up to 2A per channel and 46V maximum — far more than these small motors need, giving comfortable headroom. The onboard 5V regulator can power the Arduino when the supply voltage is 7V or higher (saves one power supply).
HC-SR04: The most common beginner ultrasonic sensor for good reason — reliable, easy to use, and accurate to ±3mm over the 2cm–400cm range. At ~$1–3 each, you can afford to have spares.
Step 1: Assemble the Chassis
Most purchased chassis kits include an instruction sheet, but the general assembly process is:
1a. Attach motors to the chassis. The TT motors fit into slots on the chassis sides and are secured with small bolts or zip ties depending on the kit. Mount one motor on each side, with both motor shafts pointing outward (wheel will attach to the shaft).
1b. Attach wheels to motor shafts. Press-fit wheels onto the motor shafts. They should click or friction-fit onto the D-shaped shaft cross-section.
1c. Install the caster wheel. The caster (a small freely-rotating ball or wheel) mounts at the front or rear of the chassis and provides the third contact point, keeping the robot level. It should be positioned at the same height as the drive wheels — adjust mounting position if needed.
1d. Identify the motor wire colors. Each TT motor has two wires (usually red and black, but not always). Note which wires belong to which motor. The direction a motor spins depends on which wire is positive and which is negative — you will determine and potentially swap this in Step 4.
Step 2: Understand the L298N Motor Driver
The L298N module is the electrical interface between the Arduino’s low-current GPIO pins and the motors’ higher current requirements. Understanding its terminals prevents wiring mistakes.
L298N module terminal layout:
┌─────────────────────────────────────┐
│ 12V GND 5V │ ← Power input / 5V output terminals
│ │
│ OUT1 OUT2 OUT3 OUT4 │ ← Motor output terminals
│ │
│ IN1 IN2 EN_A IN3 IN4 EN_B │ ← Arduino control inputs
└─────────────────────────────────────┘
Terminal functions:
12V: Motor supply voltage (6–12V from battery; label says 12V but accepts lower)
GND: Common ground (connect to battery − AND Arduino GND)
5V: Output: 5V regulated output when motor supply ≥ 7V
(Can power Arduino; short-circuits jumper when motor supply < 7V)
OUT1/OUT2: Motor A output terminals (connect to left motor)
OUT3/OUT4: Motor B output terminals (connect to right motor)
IN1, IN2: Direction control for Motor A
IN1=HIGH, IN2=LOW: Motor A forward
IN1=LOW, IN2=HIGH: Motor A reverse
IN1=HIGH, IN2=HIGH: Motor A brake (stop)
IN1=LOW, IN2=LOW: Motor A coast (stop, no braking)
IN3, IN4: Direction control for Motor B (same logic as IN1/IN2)
EN_A: Enable/speed for Motor A
Jumper installed: Motor A always at full speed
Jumper removed, PWM signal connected: speed control via PWM
EN_B: Enable/speed for Motor B (same as EN_A)
The EN_A / EN_B jumpers: For the collision-avoiding rover, we want speed control (to back up more slowly, or future enhancement). Remove the yellow jumpers from EN_A and EN_B and connect Arduino PWM pins to these terminals. If you want simplest possible wiring first (full speed only), leave jumpers installed and skip EN_A/EN_B connections.
Step 3: Wiring — Complete Connection Diagram
Wire each connection one at a time, checking it off as you go. Do not connect the battery until Step 5’s pre-power verification.
Power Connections
Battery pack (+) ──────────────────────────── L298N terminal "12V"
Battery pack (+) ──── [Power switch] ──────── (route through switch first)
Battery pack (−) ──────────────────────────── L298N terminal "GND"
L298N terminal "GND" ──────────────────────── Arduino GND pin
L298N terminal "5V" ───────────────────────── Arduino 5V pin (powers Arduino)
Note: L298N 5V output is only present when motor supply ≥ 7V.
6× AA (9V nominal): 5V output available → can power Arduino this way
4× AA (6V nominal): 5V output NOT available → power Arduino via USB or 9V snap
Alternative Arduino power: 9V battery with snap connector to Arduino Vin/GND
(Arduino's onboard regulator handles 9V → 5V)
Motor Connections
Left motor (+) wire ──────── L298N OUT1
Left motor (−) wire ──────── L298N OUT2
Right motor (+) wire ──────── L298N OUT3
Right motor (−) wire ──────── L298N OUT4
Note: "+" and "−" are provisional — actual direction determined in Step 4.
If motor spins backward when expected to go forward, swap OUT1↔OUT2
(or OUT3↔OUT4 for right motor).
Arduino to L298N Control Connections
Arduino Pin 5 ──────────────── L298N IN1 (Left motor direction 1)
Arduino Pin 6 ──────────────── L298N IN2 (Left motor direction 2)
Arduino Pin 7 ──────────────── L298N IN3 (Right motor direction 1)
Arduino Pin 8 ──────────────── L298N IN4 (Right motor direction 2)
Arduino Pin 9 ──────────────── L298N EN_A (Left motor speed, PWM)
Arduino Pin 10 ──────────────── L298N EN_B (Right motor speed, PWM)
HC-SR04 Ultrasonic Sensor Connections
HC-SR04 VCC ──────────────── Arduino 5V (or L298N 5V output)
HC-SR04 GND ──────────────── Arduino GND
HC-SR04 TRIG ──────────────── Arduino Pin 11
HC-SR04 ECHO ──────────────── Arduino Pin 12
Complete Wiring Summary Table
| Connection | From | To |
|---|---|---|
| Motor power + | Battery (+) | L298N 12V |
| Motor power − | Battery (−) | L298N GND |
| Common ground | L298N GND | Arduino GND |
| Arduino power | L298N 5V | Arduino 5V |
| Left motor A | Left motor wire 1 | L298N OUT1 |
| Left motor B | Left motor wire 2 | L298N OUT2 |
| Right motor A | Right motor wire 1 | L298N OUT3 |
| Right motor B | Right motor wire 2 | L298N OUT4 |
| Left dir 1 | Arduino Pin 5 | L298N IN1 |
| Left dir 2 | Arduino Pin 6 | L298N IN2 |
| Right dir 1 | Arduino Pin 7 | L298N IN3 |
| Right dir 2 | Arduino Pin 8 | L298N IN4 |
| Left speed | Arduino Pin 9 | L298N EN_A |
| Right speed | Arduino Pin 10 | L298N EN_B |
| Sensor power | Arduino 5V | HC-SR04 VCC |
| Sensor ground | Arduino GND | HC-SR04 GND |
| Trigger | Arduino Pin 11 | HC-SR04 TRIG |
| Echo | Arduino Pin 12 | HC-SR04 ECHO |
Step 4: Mounting the Sensor
The HC-SR04 sensor must face forward — its two cylindrical transducers (one transmits, one receives the ultrasound pulse) need a clear forward view with no chassis obstructing the beam. Mount options:
On top of the chassis at the front: Secure the sensor flat on the chassis surface with double-sided tape or hot glue, transducers facing the direction of travel. This is the simplest mounting.
Upright at the front edge: Mount the sensor vertically at the front edge of the chassis. Most TT motor chassis have holes or notches suitable for zip-tie mounting. This gives the best forward view.
On a servo for scanning (optional enhancement): A hobby servo can pan the sensor left and right, allowing the robot to scan for the best escape route when an obstacle is detected. This is a worthwhile upgrade after the basic robot is working.
Height matters: mount the sensor at a height that detects obstacles the same height as the chassis body or taller. If the sensor is mounted too high, it will miss low obstacles that the chassis would hit. A height of 5–10cm from the ground typically works well for detecting chairs, table legs, walls, and similar obstacles.
Step 5: Pre-Power Verification
Before connecting the battery, verify the most critical connections with a multimeter:
Verification checklist:
□ Continuity between L298N GND and Arduino GND → should beep
□ Continuity between L298N 12V and battery (+) → should beep
□ Continuity between L298N GND and battery (−) → should beep
□ NO continuity between L298N 12V and GND → open (not shorted)
□ Continuity from HC-SR04 VCC to Arduino 5V → should beep
□ Continuity from HC-SR04 GND to Arduino GND → should beep
Check for motor shorts (with battery disconnected):
□ Continuity from OUT1 to OUT2 → should NOT beep (not shorted)
□ Continuity from OUT3 to OUT4 → should NOT beep (not shorted)
If any short exists between power and ground, find and fix it before connecting the battery. A short will immediately discharge the battery through a very low resistance path, potentially causing the battery to heat, vent, or (with LiPo) catch fire.
Step 6: The Complete Arduino Sketch
Upload this sketch to the Arduino using the Arduino IDE before connecting the battery for the first time. This way, when the battery is connected, the robot immediately begins executing tested code rather than sitting in bootloader mode.
/*
* Collision-Avoiding Rover — Complete Sketch
* Hardware:
* - L298N motor driver
* - Left motor: IN1=5, IN2=6, EN_A=9
* - Right motor: IN3=7, IN4=8, EN_B=10
* - HC-SR04: TRIG=11, ECHO=12
*
* Behavior:
* Drive forward → obstacle detected within STOP_DISTANCE → stop →
* back up briefly → turn → resume forward
*/
// ── Pin definitions ──────────────────────────────────────────────
const int IN1 = 5; // Left motor direction 1
const int IN2 = 6; // Left motor direction 2
const int ENA = 9; // Left motor speed (PWM)
const int IN3 = 7; // Right motor direction 1
const int IN4 = 8; // Right motor direction 2
const int ENB = 10; // Right motor speed (PWM)
const int TRIG = 11; // Ultrasonic trigger
const int ECHO = 12; // Ultrasonic echo
// ── Behavior parameters ──────────────────────────────────────────
const int DRIVE_SPEED = 180; // Forward speed (0–255); start conservative
const int TURN_SPEED = 160; // Speed during turns
const int BACKUP_SPEED = 150; // Speed during backup
const float STOP_DISTANCE = 20.0; // Stop if obstacle within this many cm
const int BACKUP_TIME = 600; // ms to reverse before turning
const int TURN_TIME = 700; // ms to turn (adjust for 90° turn on your robot)
// ── Motor control functions ──────────────────────────────────────
void setMotors(int leftSpeed, bool leftForward,
int rightSpeed, bool rightForward) {
// Left motor
analogWrite(ENA, abs(leftSpeed));
digitalWrite(IN1, leftForward ? HIGH : LOW);
digitalWrite(IN2, leftForward ? LOW : HIGH);
// Right motor
analogWrite(ENB, abs(rightSpeed));
digitalWrite(IN3, rightForward ? HIGH : LOW);
digitalWrite(IN4, rightForward ? LOW : HIGH);
}
void driveForward(int speed) {
setMotors(speed, true, speed, true);
}
void driveBackward(int speed) {
setMotors(speed, false, speed, false);
}
void turnRight(int speed) {
// Left motor forward, right motor backward = pivot right
setMotors(speed, true, speed, false);
}
void turnLeft(int speed) {
// Left motor backward, right motor forward = pivot left
setMotors(speed, false, speed, true);
}
void stopMotors() {
analogWrite(ENA, 0);
analogWrite(ENB, 0);
// Direction pins don't matter at speed 0, but clean state is good practice
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
}
// ── Distance measurement ─────────────────────────────────────────
float measureDistance() {
// Send 10µs trigger pulse
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
// Measure echo pulse duration (timeout after 25ms = ~4.3m max range)
long duration = pulseIn(ECHO, HIGH, 25000);
if (duration == 0) return 999.0; // Timeout → no obstacle detected (report far)
// Sound travels at ~343 m/s = 0.0343 cm/µs
// Distance = (duration / 2) × speed_of_sound (divided by 2: out + back)
return (duration / 2.0) * 0.0343;
}
float getSmoothedDistance() {
// Average 3 readings with brief pauses between them
// HC-SR04 needs at least 60ms between measurements to prevent echo interference
float sum = 0;
for (int i = 0; i < 3; i++) {
sum += measureDistance();
delay(30);
}
return sum / 3.0;
}
// ── Setup ────────────────────────────────────────────────────────
void setup() {
// Configure motor control pins as outputs
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENA, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
pinMode(ENB, OUTPUT);
// Configure sensor pins
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
// Start with motors stopped
stopMotors();
// Serial for debugging (open Serial Monitor at 9600 baud to see distances)
Serial.begin(9600);
Serial.println(F("Collision-Avoiding Rover — Ready"));
// Brief startup pause before beginning autonomous operation
delay(2000);
}
// ── Main behavior loop ───────────────────────────────────────────
void loop() {
float distance = getSmoothedDistance();
Serial.print(F("Distance: "));
Serial.print(distance, 1);
Serial.println(F(" cm"));
if (distance > STOP_DISTANCE) {
// Path is clear — drive forward
driveForward(DRIVE_SPEED);
} else {
// Obstacle detected — execute avoidance maneuver
Serial.println(F("Obstacle! Avoiding..."));
// 1. Stop
stopMotors();
delay(200);
// 2. Back up
driveBackward(BACKUP_SPEED);
delay(BACKUP_TIME);
// 3. Stop briefly
stopMotors();
delay(100);
// 4. Turn (alternate left/right using a static variable for variety)
static bool turnDirection = true; // true = right, false = left
if (turnDirection) {
turnRight(TURN_SPEED);
} else {
turnLeft(TURN_SPEED);
}
turnDirection = !turnDirection; // Alternate next time
delay(TURN_TIME);
// 5. Stop briefly before resuming
stopMotors();
delay(100);
}
}
Understanding the Code Structure
setMotors() is the core motor function. It takes speed and direction for both motors independently, giving complete control over straight-line driving and turning. All other motion functions call this one.
measureDistance() implements the HC-SR04 protocol: send a 10µs trigger pulse, then measure how long the echo pin stays HIGH. The duration in microseconds divided by 58 gives distance in centimeters (equivalent to the × 0.0343 / 2 formula used in the code).
getSmoothedDistance() averages three readings with 30ms gaps between them. The HC-SR04 datasheet recommends at least 60ms between measurements to prevent the outgoing pulse from interfering with the echo detection — three readings with 30ms gaps gives a 90ms measurement cycle that exceeds this minimum.
The avoidance sequence (stop → back → turn → resume) is the simplest effective behavior. The alternating turn direction prevents the robot from getting stuck in a loop if it repeatedly encounters the same obstacle.
Step 7: First Power-Up and Testing
Phase 1: Stationary motor test
Upload the sketch. Open the Serial Monitor at 9600 baud. Lift the robot off the surface so the wheels spin freely. Connect the battery. The robot should:
- Print “Collision-Avoiding Rover — Ready” in Serial Monitor
- Wait 2 seconds (the startup delay)
- Begin driving motors forward (wheels spinning)
If motors don’t spin: Check EN_A and EN_B connections. Verify the jumpers are removed if PWM speed control wires are connected. Verify battery voltage with a multimeter.
If one motor spins, the other doesn’t: Check the non-spinning motor’s IN3/IN4/ENB connections. Measure voltage at OUT3/OUT4 — should see PWM if IN3=HIGH and IN4=LOW.
Phase 2: Motor direction test
Hold the robot with the front facing away from you and observe wheel rotation:
- Both wheels should spin so the robot would drive forward (away from you)
- If one or both wheels spin the wrong direction, swap that motor’s OUT1↔OUT2 (or OUT3↔OUT4) connections
Phase 3: Sensor test
Wave your hand in front of the HC-SR04. The Serial Monitor should show distances decreasing as your hand approaches. When your hand is within 20cm, the motors should stop and the avoidance sequence should trigger.
If sensor reads 999 constantly: Check TRIG and ECHO connections. Verify HC-SR04 is powered (5V/GND). The 999.0 return value indicates timeout (no echo received).
If sensor reads erratically: This is normal — some readings are noisy. The three-reading average reduces this. If still very erratic, check that nothing is directly in front of the sensor at startup.
Phase 4: Floor test
Place the robot on a smooth floor with clear space in all directions. It should drive forward, then steer around anything it encounters. Test in a room with obstacles at varying heights and angles.
Tuning for Your Specific Robot
Every robot is slightly different — motor speeds, wheel diameter, chassis weight, and surface friction all affect behavior. Expect to tune these parameters:
Speed Tuning
If the robot drives in a curve rather than straight (motors at equal speed), one motor is faster than the other. Reduce the PWM value for the faster side:
// Example: left motor slightly faster than right
const int LEFT_SPEED = 160; // Reduced to compensate
const int RIGHT_SPEED = 180; // Full speed
// In driveForward():
setMotors(LEFT_SPEED, true, RIGHT_SPEED, true);
The ideal starting speed (DRIVE_SPEED = 180) provides good torque without stressing components. Too slow (< 100) and the robot may stall on carpet or at slight inclines. Too fast (> 220) and turns become imprecise.
Turn Time Tuning
The TURN_TIME constant determines how far the robot turns when avoiding. The goal is roughly a 90° turn so the robot heads in a perpendicular direction:
Tuning process:
1. Mark the robot's starting orientation with tape on the floor
2. Trigger an avoidance maneuver manually (block the sensor)
3. Observe how far the robot turns
4. Adjust TURN_TIME:
- Robot turns less than 90°: increase TURN_TIME (try 900)
- Robot turns more than 90°: decrease TURN_TIME (try 500)
5. Repeat until turn is approximately 90°
Stop Distance Tuning
STOP_DISTANCE = 20cm works for most surfaces and speeds. On carpet (slower), you may need 15cm. At higher speeds or with heavier robots, 25–30cm gives more stopping distance.
Enhancements to Try Next
Once the basic rover works reliably, these enhancements develop important new skills:
Enhancement 1: Add a Second Sensor
A second HC-SR04 mounted on the side (left or right) can detect walls before the robot drives into them from the side. Or mount two sensors angled left and right at 45° to give better coverage:
// Three-sensor version
const int TRIG_LEFT = 11, ECHO_LEFT = 12; // 45° left
const int TRIG_FWD = A0, ECHO_FWD = A1; // Straight ahead
const int TRIG_RIGHT = A2, ECHO_RIGHT = A3; // 45° right
// Choose turn direction based on which side has more space
float distLeft = measureDistanceOnPins(TRIG_LEFT, ECHO_LEFT);
float distRight = measureDistanceOnPins(TRIG_RIGHT, ECHO_RIGHT);
if (distRight > distLeft) {
turnRight(TURN_SPEED);
} else {
turnLeft(TURN_SPEED);
}
Enhancement 2: Servo-Mounted Scanning Sensor
Mount the HC-SR04 on a servo that sweeps left and right. When the forward path is blocked, scan both sides and choose the direction with more clear space:
#include <Servo.h>
Servo scanServo;
int scanForBestDirection() {
scanServo.write(180); // Look left
delay(300);
float leftDist = measureDistance();
scanServo.write(0); // Look right
delay(300);
float rightDist = measureDistance();
scanServo.write(90); // Center
delay(200);
return (rightDist > leftDist) ? 1 : -1; // 1=turn right, -1=turn left
}
Enhancement 3: Add LEDs for Status Indication
Status LEDs make the robot’s internal state visible — useful for debugging and more satisfying to watch:
const int LED_FWD = 13; // Green: driving forward
const int LED_AVOID = A4; // Red: avoidance maneuver active
// In driveForward():
digitalWrite(LED_FWD, HIGH);
digitalWrite(LED_AVOID, LOW);
// At start of avoidance:
digitalWrite(LED_FWD, LOW);
digitalWrite(LED_AVOID, HIGH);
Enhancement 4: Serial Command Override
Add the ability to control the robot manually via Serial commands while it runs its autonomous code — useful for testing:
// In loop(), before the distance check:
if (Serial.available()) {
char cmd = Serial.read();
switch (cmd) {
case 'f': driveForward(DRIVE_SPEED); delay(500); break;
case 'b': driveBackward(BACKUP_SPEED); delay(500); break;
case 'l': turnLeft(TURN_SPEED); delay(300); break;
case 'r': turnRight(TURN_SPEED); delay(300); break;
case 's': stopMotors(); break;
}
}
Type ‘f’, ‘b’, ‘l’, ‘r’, or ‘s’ in the Serial Monitor to override autonomous behavior momentarily. This is the foundation of a teleoperation mode.
Troubleshooting Reference
| Problem | Most Likely Cause | Check / Fix |
|---|---|---|
| Robot doesn’t move at all | Battery not connected or power switch off | Verify battery voltage; check switch |
| Robot moves but immediately stops | Sensor reading obstacles at startup | Hold sensor away from obstacles; check mounting direction |
| One wheel doesn’t turn | Missing IN3/IN4 or ENB connection | Verify wiring for non-spinning motor |
| Both wheels turn same direction; robot spins in circle | One motor wired backward | Swap OUT1↔OUT2 or OUT3↔OUT4 for inverted motor |
| Robot drives in a curve | Motor speed imbalance | Reduce PWM value for faster motor |
| Sensor always reads 999 | Bad TRIG/ECHO connection or no power to sensor | Check HC-SR04 VCC, GND, TRIG pin 11, ECHO pin 12 |
| Avoidance never triggers | STOP_DISTANCE too small or sensor not working | Increase STOP_DISTANCE; verify sensor reads correctly in Serial Monitor |
| Robot gets stuck turning in place | TURN_TIME too long; turning past obstacle | Reduce TURN_TIME; check floor for obstacles on all sides |
| Arduino resets during operation | Insufficient power; battery voltage drop under load | Check battery freshness; measure voltage under load |
| Robot works on smooth floor, stops on carpet | Motor stall from friction; speed too low | Increase DRIVE_SPEED; check motors aren’t mechanically binding |
What You’ve Built and Learned
Completing this rover means you have:
- Successfully wired a motor driver IC to control two DC motors with direction and speed control
- Correctly connected and operated an HC-SR04 ultrasonic distance sensor
- Written a multi-function Arduino sketch with a sensor-behavior loop
- Debugged wiring and code through systematic testing
- Tuned behavioral parameters (speed, timing, thresholds) for your specific robot
More fundamentally, you’ve built your first complete autonomous system — one where sensing, decision-making, and action happen together in a continuous loop without human intervention. This is the fundamental architecture of every autonomous robot, from this simple rover to a self-driving car: sense the environment, evaluate it against a goal or set of rules, execute an action, and repeat.
Understanding the Physics: Why the Robot Behaves the Way It Does
The rover’s behavior emerges from simple physics and geometry. Understanding these relationships lets you predict behavior before building and explains the outcomes you observe during testing.
Differential Drive Steering
The rover uses differential drive — two independently controlled wheels on the same axle. Steering is achieved by running the wheels at different speeds, not by turning a front wheel. This is the same principle used in tanks, bulldozers, and most wheeled robots:
Differential drive motion modes:
Both wheels forward, equal speed → drive straight forward
Both wheels backward, equal speed → drive straight backward
Left wheel forward, right wheel stopped → gentle right curve
Left wheel forward, right wheel backward → pivot turn right in place
Left wheel stopped, right wheel forward → gentle left curve
Left wheel backward, right wheel forward → pivot turn left in place
Both wheels different forward speeds → gradual curve toward slower wheel
The TURN_TIME parameter in the code controls how long the pivot turn lasts. The angle turned in a pivot turn depends on the rotation speed (determined by TURN_SPEED) and duration (TURN_TIME). A rough formula:
Turn angle ≈ TURN_SPEED × TURN_TIME × wheel_speed_per_PWM_unit / wheel_base_distance
For a typical small chassis (wheel base ~13cm), TT motors:
At TURN_SPEED = 160: wheel surface speed ≈ 18 cm/s
Pivot turn speed ≈ 2 × 18 / 13 ≈ 2.77 rad/s ≈ 159°/s
TURN_TIME = 700ms → 159°/s × 0.7s ≈ 111° (close to 90°, varies with surface)
TURN_TIME = 560ms → 159°/s × 0.56s ≈ 89° (near-perfect 90° turn)
Surface matters: carpet increases friction → slower actual wheel speed → larger TURN_TIME needed
This is why TURN_TIME needs tuning — the formula above gives an approximation, but actual wheel speed versus PWM is affected by battery voltage, motor variation, and surface friction.
Ultrasonic Distance Measurement Physics
The HC-SR04 works by timing the round-trip travel of a 40kHz ultrasound pulse. Sound travels at approximately 343 m/s at room temperature (20°C). Faster at higher temperatures — an effect that can be corrected if precision matters:
Speed of sound vs. temperature:
v = 331.3 + 0.606 × T_celsius (m/s)
At 20°C: v = 331.3 + 12.1 = 343.4 m/s (0.03434 cm/µs)
At 35°C: v = 331.3 + 21.2 = 352.5 m/s (0.03525 cm/µs)
Difference: 2.6% faster at 35°C than 20°C
For a reading at 20cm from a wall:
At 20°C: actual distance = 20cm, measured correctly
At 35°C: code uses 0.0343, actual speed is 0.03525
Measured = duration × 0.03434 / 2 = duration × 0.01717
Actual = duration × 0.03525 / 2 = duration × 0.01763
Error: (0.01717 - 0.01763) / 0.01763 = -2.6% (reads shorter than actual)
At 20cm actual: measured ≈ 19.5cm → within 0.5cm → acceptable for rover
Temperature correction for precision applications:
float speedOfSound = 0.0001 * (331.3 + 0.606 * temperature_C); // cm/µs
float distanceCm = (duration / 2.0) * speedOfSound;
For a collision-avoiding rover, the 2.6% temperature error doesn’t matter — you don’t need sub-centimeter accuracy to decide whether to steer around a chair. For precision distance measurement applications (mapping, docking), temperature correction becomes relevant.
The Beam Angle and Blind Spots
The HC-SR04 transmits an ultrasound cone approximately 15° wide (±15° from the sensor axis). Objects outside this cone are not detected. This creates blind spots:
Sensor field of view (top view):
[HC-SR04]
│
← 15° ─┼─ 15° →
/ │ \
/ │ \
/ │ \
detected zone undetected zone
Objects more than 15° to either side of center won't reflect ultrasound
back to the sensor — robot appears to see "nothing" even if object is close.
This is why the rover can sometimes drive into an obstacle at an angle — the obstacle is at the edge of or outside the detection cone. The fix is adding sensors at wider angles (as described in Enhancement 1 above) or reducing robot speed so there’s more time to react when an obstacle enters the main detection cone.
Power Budget for the Rover
Understanding how much current the rover draws helps you choose an appropriate battery and predict run time.
Component current draw at operating conditions:
Arduino Uno (running sketch): ~80 mA
L298N motor driver (quiescent, motors off): ~40 mA
HC-SR04 (active sensing): ~15 mA
Two TT motors (driving forward, moderate load): ~200–400 mA total
Two TT motors (stalled): ~800–1200 mA total
L298N internal dissipation (at 6V, 400mA): ~200 mA equivalent loss
(voltage drop across L298N at 400mA)
Typical average current during operation:
Forward driving: 80 + 40 + 15 + 300 = ~435 mA
During avoidance: ~200 mA (briefer, at lower speeds)
Combined average: ~380 mA
Battery runtime estimates:
4× AA Alkaline (2500mAh, 6V nominal, ~80% efficiency in discharge):
Runtime ≈ 2500 × 0.80 / 380 ≈ 5.3 hours theoretical
Practical runtime: ~2–3 hours (voltage sag reduces efficiency)
6× AA Alkaline (2500mAh, 9V nominal):
Runtime ≈ 2500 × 0.80 / 380 ≈ 5.3 hours (same mAh, more voltage means L298N waste)
Practical runtime: ~2–3 hours
2S LiPo 1000mAh (7.4V nominal):
Runtime ≈ 1000 × 0.85 / 380 ≈ 2.2 hours at C-rate consideration
Practical runtime: ~1–1.5 hours (1000mAh is modest for this application)
2S LiPo 2200mAh (7.4V nominal):
Runtime ≈ 2200 × 0.85 / 380 ≈ 4.9 hours
Practical runtime: ~3–4 hours — best option for extended operation
Note: The L298N has ~2V voltage drop across its output stage.
At 6V input, motors receive ~4V (reduced torque vs. rated 6V).
At 9V input, motors receive ~7V (slightly over-voltage but acceptable briefly).
At 7.4V LiPo input, motors receive ~5.4V (good operating point).
Code Architecture: Why It’s Written This Way
The sketch is deliberately structured to demonstrate good robotics code architecture, not just to make the robot work. Understanding the architectural choices helps you write better code for future projects.
Functions for Every Action
Every motion (driveForward, driveBackward, turnRight, turnLeft, stopMotors) is its own function. This makes the main loop() readable — it describes behavior in terms of actions, not pin numbers. When you want to change how “turn right” works, you change it in one place and it’s correct everywhere.
Separation of Sensing and Acting
measureDistance() only measures. getSmoothedDistance() only filters. driveForward() only drives. loop() only makes decisions. Each function has one responsibility. This separation makes debugging easier — if the robot behaves wrong, you know whether to look at the sensing functions or the action functions.
Non-Blocking Is Not Used Here (On Purpose)
This beginner sketch uses delay() — it blocks the entire program during backup and turn phases. This is intentional: the delay-based approach is simpler to understand and works correctly for this simple behavior.
The consequence: during a delay(BACKUP_TIME), the sensor is not being read. If an obstacle appears behind the robot while it’s backing up, it won’t be detected. For this simple rover, this is acceptable. For more sophisticated robots, replacing delay() with millis()-based non-blocking code is the correct evolution — and the state machine pattern from article 70 is the right tool for that step.
The Static Turn Direction Variable
static bool turnDirection = true;
The static keyword inside a function means this variable persists between calls — it’s initialized only once. This makes the robot alternate turns (right, left, right, left…) which helps it find its way past obstacles that a consistent right-turn robot would circle forever. It’s a small detail that makes a meaningful behavior difference.



