Detailed content
📝 Plan the rover on paper
Before cutting the chassis, answer three questions on paper: what components the robot has, which pin each wire connects to e how much current everything draws. The answers become the block diagram, pin map, and power budget below. Module 1.5 already used these same pins for the motors and servo, so you can use the sketches from there to test them here.
🌱 New here?
A block diagram shows the system components as boxes and what passes between them as arrows, without worrying about wires. The pin map shows which ESP32 GPIO connects to each component's terminal. The power budget adds up each component's current draw so you can choose a battery and regulator with enough headroom.
How to read this map: the green arrows show information (distance comes from the sensor, the angle goes to the servo, and PWM goes to the driver). The amber dashed arrows show power, which comes from one place at the bottom. The motors get battery voltage through VM; everything else gets 5 V from the regulator. It's the sense, think, act cycle from Module 1.1, now with real components.
Pin map (used throughout this module)
| ESP32 pin | TB6612FNG | DRV8833 | Purpose |
|---|---|---|---|
| GPIO 25 | PWMA | free | left motor speed |
| GPIO 26 · 27 | AIN1 · AIN2 | AIN1 · AIN2 | left motor direction (and PWM on the DRV8833) |
| GPIO 33 | PWMB | free | right motor speed |
| GPIO 32 · 4 | BIN1 · BIN2 | BIN1 · BIN2 | right motor direction (and PWM on the DRV8833) |
| GPIO 13 | STBY | nSLEEP / EEP | enables the driver (high level) |
| GPIO 18 | HC-SR04 TRIG | triggers the sound pulse | |
| GPIO 19 | HC-SR04 ECHO, through the voltage divider (1 kΩ from ECHO to the pin; 2 kΩ from the pin to GND) | converts the 5 V echo to 3.3 V | |
| GPIO 23 | servo, orange wire | turns the sensor | |
| GPIO 0 (BOOT) | button already on the board | pauses and resumes | |
| 3V3 | VCC | not used | driver logic |
| 5V / VIN | 5 V buck output, also connected to the HC-SR04's VCC and the servo's red wire | powers the electronics | |
| GND | Common GND: driver, buck converter, battery (−), HC-SR04, and servo's brown wire | single reference point | |
| Power budget | While moving (approx.) | Peak (approx.) |
|---|---|---|
| ESP32 without Wi-Fi | 50 to 100 mA | 250 mA with Wi-Fi |
| HC-SR04 | 15 mA | 15 mA |
| SG90 servo | almost nothing while idle | a few hundred mA while moving |
| 2 TT motors | 300 to 500 mA (combined) | close to 1 A each, at startup or when stalled |
| Conclusion | a buck rated for at least 2 A for the 5 V; a battery that can handle 2 A or more peak. Measure yours with a multimeter: the numbers vary by model. | |
Why learn this
Planning prevents you from having to redo the wiring or replace parts halfway through. Engineers who build commercial robots create exactly these three documents before the first prototype; with them in hand, you can also ask for help on a forum in a way that helps someone understand your robot.
Key concepts
🔧 Assemble the chassis and wiring
The chassis can be made of thick cardboard, MDF, or acrylic: what matters is that it is rigid and has room for everything to be secured. Motors at the back, a caster wheel at the front (or the other way around, if your kit comes that way), battery low and near the wheel axle, ESP32 and breadboard on top, sensor at the front with a clear field of view.
How to read this map: each horizontal green line connects a GPIO to the driver input at the same height, so you can check each wire one by one with your finger. The circles on the right are the motors. At the bottom, the amber line goes to the servo, and the sensor wires pass through the voltage divider: ECHO comes out of the HC-SR04 at 5 V, passes through the 1 kΩ resistor, and reaches GPIO 19 at about 3.3 V, because the 2 kΩ resistor diverts the rest to GND. The power connections are shown in the pin map and in the module 1.5 figure.
Assembly steps
Solder wires and capacitors to the motors
Two wires per motor (red and black) and a 100 nF ceramic capacitor across the terminals. Do this before mounting them: they are hard to reach afterward.
Mount the motors, wheels, and caster wheel
Keep the motors parallel and aligned, using M3 screws or cable ties. Turn the wheels by hand: they must not scrape against the chassis.
Battery, switch, and buck
Place the battery holder with BMS underneath, near the axle; put the switch on the positive wire. Use a multimeter to adjust the buck to 5.0 V before connecting any board to it.
Breadboard, ESP32, and driver
Use double-sided tape to attach the breadboard; place the ESP32 and driver on it. Make the common GND rail first, then add the signal wires, following the table.
Servo and sensor at the front
Attach the servo at the front, with the HC-SR04 secured to its arm, about 8 to 10 cm above the ground and pointing straight ahead. The sensor’s “eyes” must not see the chassis itself.
✓ Wiring that lasts
- ✓Use consistent colors: red = positive, black = GND, other colors = signal.
- ✓Strain relief: secure the wire with a cable tie before the terminal so a tug does not reach the solder joint.
- ✓Use only the wire length you need, with no extra loops near the wheels.
✗ Intermittent failure guaranteed
- ✗A loose jumper on the breadboard atop a vibrating chassis.
- ✗A loose battery: it shifts the center of mass and pulls on the wires at every turn.
- ✗HC-SR04 5 V ECHO connected directly to GPIO, without the voltage divider.
Why learn this
Loose wiring is the biggest source of intermittent failures in mobile robots: the problem appears only when the robot vibrates, disappears on the workbench, and takes an entire afternoon to track down. Assembling everything neatly (GND first, wires secured, weight low) prevents this from the start.
Key concepts
🧪 Test each part separately
If you upload the final code and the robot does not move, the problem could be in any of ten places. By testing one part at a time, in the right order, each test depends only on the ones that have already passed. It is the hardware equivalent of a unit test in programming.
How to read this map: Start at the bottom left. Power (amber) comes first because if it is wrong, all the other tests give false results. Test the motors one at a time so you know which one spins backward. The “everything on the ground” step only happens in topic 4.
Workbench checklist (step 1, with the switch on and no USB)
- ✓Battery: between 7 and 8.4 V with two 18650 cells (or close to 5 V with four fully charged NiMH cells).
- ✓Buck output: 5.0 V. ESP32 3V3 pin: about 3.3 V.
- ✓Continuity (beep) between the GND on the ESP32, driver, and sensor, and the battery negative terminal.
Goal (step 2): see the distance in Serial Monitor and confirm that the ECHO voltage divider works. This also runs in Wokwi, which has the HC-SR04.
// Isolated HC-SR04 test (ESP32, esp32 package 3.x)
const int PINO_TRIG = 18, PINO_ECHO = 19; // ECHO through 1k/2k divider
void setup() {
Serial.begin(115200);
pinMode(PINO_TRIG, OUTPUT); digitalWrite(PINO_TRIG, LOW);
pinMode(PINO_ECHO, INPUT);
}
void loop() {
digitalWrite(PINO_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PINO_TRIG, LOW);
unsigned long us = pulseIn(PINO_ECHO, HIGH, 30000UL); // waits at most 30 ms
if (us == 0) Serial.println("no echo (nothing up to ~4 m, or ECHO wire disconnected)");
else Serial.printf("distance: %.1f cm\n", us / 58.0); // 58 us per cm, round trip
delay(100);
}
How to check
- ✓Hold your hand 20 cm from the sensor: Serial shows values between 18 and 22 cm, changing as you move your hand closer or farther away.
- ✓Steps 3 and 4: upload the motores-teste.ino from module 1.5 (same pins) with the robot suspended; with the DRV8833, use the sketch’s test menu in topic 4. Step 5: the servo-olhar.ino.
- ✗Always “no echo”? Check for 5 V at the sensor’s VCC and check the voltage divider resistors (1 kΩ on the sensor side).
Why learn this
When you test everything at once, you don’t know which part failed. With the ladder, each problem shows up on its own, at its own step, and the fix takes minutes. This habit applies to any robot, from a rover to an AI-powered arm.
Key concepts
🧠 Obstacle avoidance logic with a state machine
A “if the wall is close, turn” inside the loop() works until the robot needs to do two things in sequence (stop, reverse, look, turn). Then the code becomes a tangle of if e delay. The solution is a state machine: the robot is always in a named state, does only what that state tells it to, and changes state when a condition occurs.
🌱 New here?
A state is a behavior mode (MOVE, STOP...). A transition is the rule that takes it from one state to another (“distance less than 25 cm”). In C++, enum creates the list of state names, and switch selects the code block for the current state. millis() tells you how long the board has been powered on; with it, the robot measures how long it has been in a state without blocking with delay().
How to read this map: each capsule is a state and each arrow is a transition, with its condition written beside it. Notice that only one condition depends on the sensor (dist < 25 cm); the others depend on time. The cyan arrow closes the loop. In the code below, each capsule is a case of the switch.
Goal: full rover. It starts paused: the board’s BOOT button toggles between paused and moving, and while paused, you can run tests through the Serial Monitor (s, e, d, c). Two lines at the top select the driver (TB6612FNG or DRV8833) and whether a servo is present. First install the ESP32Servo library (module 1.5).
// Obstacle-avoiding rover - Robotics from Zero to Expert, project 1.6
// ESP32 + HC-SR04 + 2 DC motors (TB6612FNG or DRV8833) + optional servo
// esp32 package 3.x (ledcAttach / ledcWrite) and ESP32Servo library
#include <ESP32Servo.h>
// ---------- choose the hardware ----------
#define USAR_DRV8833 0 // 0 = TB6612FNG, 1 = DRV8833
#define USAR_SERVO 1 // 0 = fixed sensor, facing forward
// ---------- pins (map from topic 1) ----------
const int PINO_PWMA = 25, PINO_AIN1 = 26, PINO_AIN2 = 27; // left motor
const int PINO_PWMB = 33, PINO_BIN1 = 32, PINO_BIN2 = 4; // right motor
const int PINO_STBY = 13; // STBY (TB6612) or nSLEEP (DRV8833)
const int PINO_TRIG = 18, PINO_ECHO = 19; // ECHO through 1k/2k divider
const int PINO_SERVO = 23;
const int PINO_BOTAO = 0; // board BOOT button: pause / resume
// ---------- adjustments (topic 5) ----------
const int PWM_FREQ = 20000, PWM_BITS = 8;
const int VEL_MAX = 180; // limit: around 6 V average with 2x 18650 (4x AA NiMH: 255)
const int VEL_MIN = 90; // TT motor dead zone
const int VEL_FRENTE = 170, VEL_GIRO = 160;
const int AJUSTE_ESQ = 0, AJUSTE_DIR = 0; // add +10 to the weaker motor
const float DIST_PARAR_CM = 25.0;
const unsigned long T_PARAR = 200, T_RE = 350, T_GIRO_90 = 380; // em ms
#if USAR_SERVO
Servo cabeca;
#endif
enum Estado { ANDAR, PARAR, RE, OLHAR, VIRAR };
const char* NOME[] = {"ANDAR", "PARAR", "RE", "OLHAR", "VIRAR"};
Estado estado = PARAR;
unsigned long inicioEstado = 0, ultimaLeitura = 0, tGiro = T_GIRO_90;
int ladoGiro = 1; // +1 = right, -1 = left
bool pausado = true; // starts stopped: press BOOT to move
float hist[3] = {400, 400, 400}; int iHist = 0; // last 3 readings
// ---------- motors ----------
void motor(int in1, int in2, int pinoPwm, int vel) {
vel = constrain(vel, -VEL_MAX, VEL_MAX);
int forca = abs(vel);
if (forca > 0 && forca < VEL_MIN) forca = VEL_MIN;
#if USAR_DRV8833
ledcWrite(in1, vel > 0 ? forca : 0); // DRV8833: PWM on both input pins
ledcWrite(in2, vel < 0 ? forca : 0);
#else
digitalWrite(in1, vel > 0 ? HIGH : LOW); // TB6612: direction on IN, power on PWM
digitalWrite(in2, vel < 0 ? HIGH : LOW);
ledcWrite(pinoPwm, forca);
#endif
}
void andar(int esq, int dir) { // -255 to 255 for each wheel
if (esq != 0) esq += (esq > 0) ? AJUSTE_ESQ : -AJUSTE_ESQ;
if (dir != 0) dir += (dir > 0) ? AJUSTE_DIR : -AJUSTE_DIR;
motor(PINO_AIN1, PINO_AIN2, PINO_PWMA, esq);
motor(PINO_BIN1, PINO_BIN2, PINO_PWMB, dir);
}
// ---------- sensor ----------
float medirCm() {
digitalWrite(PINO_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PINO_TRIG, LOW);
unsigned long us = pulseIn(PINO_ECHO, HIGH, 30000UL);
return (us == 0) ? 400.0 : us / 58.0; // no echo = nothing nearby (around 4 m)
}
float mediana3(float a, float b, float c) { return max(min(a, b), min(max(a, b), c)); }
float distanciaAndando() { // 1 new reading + the 2 previous ones
hist[iHist] = medirCm(); iHist = (iHist + 1) % 3;
return mediana3(hist[0], hist[1], hist[2]);
}
float distanciaParado() { // 3 new readings, 60 ms apart
float a = medirCm(); delay(60);
float b = medirCm(); delay(60);
return mediana3(a, b, medirCm());
}
// ---------- state machine ----------
void mudar(Estado novo) {
estado = novo; inicioEstado = millis();
Serial.printf("-> %s\n", NOME[novo]);
}
void escolherLado() { // if the servo looks the wrong way, swap 20 and 160
#if USAR_SERVO
cabeca.write(20); delay(400); float dDir = distanciaParado();
cabeca.write(160); delay(600); float dEsq = distanciaParado();
cabeca.write(90); delay(300);
Serial.printf("right %.0f cm | left %.0f cm\n", dDir, dEsq);
ladoGiro = (dDir >= dEsq) ? 1 : -1;
tGiro = (max(dDir, dEsq) < DIST_PARAR_CM) ? 2 * T_GIRO_90 : T_GIRO_90; // dead end: turn around
#else
ladoGiro = -ladoGiro; // without a servo: alternate sides
tGiro = T_GIRO_90;
#endif
}
void testar(char c) { // isolated tests through the Serial Monitor
if (c == 's') Serial.printf("distance: %.1f cm\n", distanciaParado());
if (c == 'e') { Serial.println("left motor, forward"); andar(VEL_FRENTE, 0); delay(1000); andar(0, 0); }
if (c == 'd') { Serial.println("right motor, forward"); andar(0, VEL_FRENTE); delay(1000); andar(0, 0); }
#if USAR_SERVO
if (c == 'c') { Serial.println("servo: right, left, forward"); cabeca.write(20); delay(600); cabeca.write(160); delay(800); cabeca.write(90); }
#endif
}
void setup() {
Serial.begin(115200);
pinMode(PINO_BOTAO, INPUT_PULLUP);
pinMode(PINO_STBY, OUTPUT); digitalWrite(PINO_STBY, HIGH);
#if USAR_DRV8833
ledcAttach(PINO_AIN1, PWM_FREQ, PWM_BITS); ledcAttach(PINO_AIN2, PWM_FREQ, PWM_BITS);
ledcAttach(PINO_BIN1, PWM_FREQ, PWM_BITS); ledcAttach(PINO_BIN2, PWM_FREQ, PWM_BITS);
#else
pinMode(PINO_AIN1, OUTPUT); pinMode(PINO_AIN2, OUTPUT);
pinMode(PINO_BIN1, OUTPUT); pinMode(PINO_BIN2, OUTPUT);
ledcAttach(PINO_PWMA, PWM_FREQ, PWM_BITS); ledcAttach(PINO_PWMB, PWM_FREQ, PWM_BITS);
#endif
andar(0, 0);
pinMode(PINO_TRIG, OUTPUT); digitalWrite(PINO_TRIG, LOW); pinMode(PINO_ECHO, INPUT);
#if USAR_SERVO
ESP32PWM::allocateTimer(3); // servo on a timer separate from the motors
cabeca.setPeriodHertz(50);
cabeca.attach(PINO_SERVO, 500, 2400);
cabeca.write(90);
#endif
Serial.println("Rover ready and PAUSED. Press BOOT to move.");
Serial.println("Paused, test: s=sensor e=left motor d=right motor c=servo");
}
void loop() {
static bool botaoAntes = HIGH;
bool botao = digitalRead(PINO_BOTAO);
if (botaoAntes == HIGH && botao == LOW) { // just pressed
pausado = !pausado; andar(0, 0); delay(250); // 250 ms filters out button bounce
Serial.println(pausado ? "PAUSADO" : "ANDANDO");
if (!pausado) { hist[0] = hist[1] = hist[2] = 400; mudar(ANDAR); }
}
botaoAntes = botao;
if (pausado) { if (Serial.available()) testar(Serial.read()); return; }
unsigned long agora = millis(), noEstado = agora - inicioEstado;
switch (estado) {
case ANDAR:
andar(VEL_FRENTE, VEL_FRENTE);
if (agora - ultimaLeitura >= 60) { // HC-SR04: one reading every 60 ms
ultimaLeitura = agora;
if (distanciaAndando() < DIST_PARAR_CM) mudar(PARAR);
}
break;
case PARAR:
andar(0, 0);
if (noEstado >= T_PARAR) mudar(RE);
break;
case RE:
andar(-VEL_GIRO, -VEL_GIRO);
if (noEstado >= T_RE) { andar(0, 0); mudar(OLHAR); }
break;
case OLHAR:
escolherLado(); // robot stopped for about 1.5 s
mudar(VIRAR);
break;
case VIRAR:
andar(VEL_GIRO * ladoGiro, -VEL_GIRO * ladoGiro);
if (noEstado >= tGiro) {
andar(0, 0);
hist[0] = hist[1] = hist[2] = 400; // discard old readings
mudar(ANDAR);
}
break;
}
}
How to check
- ✓When it starts, the Serial Monitor displays “Rover ready and PAUSED”. Enter s, e, d, and c: each part responds on its own (this is step 6 of the ladder, still on the workbench).
- ✓With the robot on the floor and BOOT pressed, it moves straight. When it reaches a box, the Serial Monitor displays “-> STOP”, “-> REVERSE”, “-> LOOK”, the two distances, and “-> TURN”, and the robot continues toward the more open side.
- ✗Servo shaking or motor beeping only after you connect the servo? That’s a PWM timer conflict: check the line allocateTimer(3) and update ESP32Servo.
- ✗Compilation error in ledcAttach? The installed esp32 package is older than 3.x: update it in Boards Manager.
💡 Test without motors in Wokwi
Wokwi simulates the ESP32, HC-SR04, servo, and button, but not the H-bridge. To test the logic without the robot, connect LEDs (with resistors) to pins 25 and 33 in place of the motors, drag the virtual sensor’s distance control, and watch the states in Serial.
Why learn this
State machines are how real robots organize behavior, from vacuum cleaners to self-driving cars. To add a new behavior (for example, “honk while reversing”), you change just one state without breaking the others. In Track 2, the same idea expands to parallel tasks with FreeRTOS.
Key concepts
🩺 Fine-tuning and common problems
Every first rover needs adjustments: the motors are never identical, the floor changes the friction, and the battery voltage drops with use. That’s what the numbers in the “adjustments” block at the top of the sketch are for. Use the Serial Monitor as a dashboard: it tells you what state the robot was in when the problem occurred.
| Symptom | Likely cause | Fix |
|---|---|---|
| Pulls to one side | a weaker motor or a wheel rubbing | AJUSTE_ESQ or AJUSTE_DIR in increments of 10; check the wheels |
| Restarts when reversing or turning | brownout: reversing the motor draws a current spike | charged battery, capacitors, 2 A buck converter, common GND; confirm with the reset-reason.ino from 1.5 |
| Doesn’t see the wall | sensor tilted, too high, or a soft/slanted surface that scatters the echo | sensor level, 8–10 cm above the floor; increase DIST_PARAR_CM |
| Stops by itself in the middle of the room | brief false reading (noise on the sensor wire) | keep the sensor wires away from the motor wires; check the voltage divider; the median filter already removes isolated spikes |
| Turns too far or not far enough | T_GIRO_90 depends on the floor and battery | adjust in 50 ms steps and test on the floor where it will run |
| Nothing moves | it is paused, or STBY/nSLEEP is low | press BOOT; measure 3.3 V at STBY |
✓ Adjust methodically
- ✓Change one number at a time and note the before and after.
- ✓Always test on the same floor with a charged battery.
- ✓Read Serial with the robot connected to the computer by a long cable, or suspended.
✗ Guesswork adjustments
- ✗Changing three values at once and not knowing which one fixed it.
- ✗Blaming the code before measuring the battery.
- ✗Removing the limit VEL_MAX with 2× 18650 cells to “get more power”.
Why learn this
Knowing how to diagnose is the skill that stays with you after the rover goes on the shelf. The sequence “symptom, state in Serial, likely cause, one change, new test” is the same one you’ll use to tune the PID in Track 2 and navigation in Track 4.
Key concepts
🚀 Next steps: line, buzzer, and Wi-Fi
The rover is ready, but the chassis can do much more. Three small upgrades, each adding a new state or sensor, show how the same robot can grow with code and parts, without starting over. Each one also opens a module in Track 2.
How to read this map: the green block is what you already have; the cyan blocks are independent additions that you can make in any order. None requires replacing the chassis, motors, or state machine: each adds a sensor, a state, or another task.
Point the two infrared modules in the kit (module 1.4) at the floor, one on each side of the black line. Create the FOLLOW state: if the left sensor sees black, turn left. In Track 2, the 5-sensor array and PID make this smooth.
A passive buzzer on an unused pin (25 and 33 are free with the DRV8833; with the TB6612, pin 17 works on the DevKit WROOM-32). In API 3.x, ledcAttach on the pin and ledcWriteTone(pino, 2000) play a beep; turn it on in the REVERSE state and turn it off with tone 0.
The ESP32 creates a page with forward, reverse, and stop buttons, served to your phone. Module 2.1 shows you how, with one important safety rule: if the connection drops, the robot stops by itself.
💡 Document your rover
Record a short video of the robot avoiding obstacles, take a photo of the wiring, and save the sketch with your tuning values. Use “My Journey” to mark what you’ve completed and export your progress. This record is the start of your robotics portfolio, and it’s what a teacher asks for in a project-based assessment.
Why learn this
Watching the robot grow in small steps teaches you the way all robotics work: start with a working, tested base, then add things you can verify one at a time. That’s exactly how Track 2 turns this rover into a truly autonomous robot.
Key concepts
✅ Module summary
To go further
- LEDC (PWM) in Arduino-ESP32 — ledcAttach, ledcWrite, and ledcWriteTone in API 3.x
- ESP32Servo library — examples and compatibility notes
- HC-SR04 on Wokwi — test the sensor in your browser
- Arduino-ESP32 (Espressif) — complete reference for package 3.x
- TB6612FNG (Pololu) — driver current and voltage limits
Next track
Track 2 · Autonomous robot. Starts with module 2.1, phone control over Wi-Fi and BLE, then continues with IMU, encoders, PID, and telemetry through to a line follower.