Episode 6: Let the Wookiee Win ā R2 Moves!
"Would Somebody Get This Big Walking Carpet Out of My Way?" š¦¾
Han Solo enters the workshop with the energy of someone who has been waiting since Episode 1 for this moment.
HAN: "Okay, look. I've put up with the blinking LED. The beeps were cute. The screen ā fine, useful. But I need this droid to actually MOVE. Because right now he's sitting there like a very opinionated lamp and I am not impressed."
R2-D2 beeps at high volume and at length.
HAN: "Yeah yeah, I know, you're very capable. Prove it. Episode 6. Let's go."
Somewhere, a Wookiee roars enthusiastically.
HAN: "Chewie agrees. Let the droid move."
šļø SIPOC ā The Motion System
| Suppliers | Inputs | Process | Outputs | Customers |
|---|---|---|---|---|
| You (the maker) | "Add servo for dome rotation on GPIO14, L298N motor driver for wheels on GPIO pins 25-27" | Codey writes ESP32-S3 LEDC servo code + L298N motor control, generates motion.h | Dome that sweeps left and right, wheels that drive forward/back/turn | R2-D2 ā who finally lives up to his reputation |
| ESP32-S3 LEDC PWM | 50Hz PWM signal, 3.3V | Drives servo signal line (with level shifter) | Dome servo at 0°, 45°, 90°, 135°, 180° | SG90 servo ā which rotates R2's dome |
| L298N Motor Driver | 5Vā12V external power, direction + PWM signals from ESP32-S3 | H-bridge controls current direction to each DC motor | Forward / backward / left turn / right turn | Two DC motors ā which drive R2's wheels |
| Codey Wiring Diagram | Full component list including motor power requirements | Draws color-coded diagram with separate power rail for motors | Two-rail wiring diagram: logic (3.3V) + motor power | You ā who correctly separate the motor power from the logic power |
The Components š§
Yoda examines the motor driver with a long, thoughtful look.
YODA: "Moving parts, added today we are. Careful, one must be. Motors, hungry for current they are. Directly from the ESP32-S3, power them you must not ā destroyed, your microcontroller would be."
| Component | Quantity | Notes |
|---|---|---|
| ESP32-S3 N16R8 | 1 | Our brain from Episode 5 |
| SG90 micro servo | 1 | For dome rotation ā 5V, 3-wire |
| L298N dual H-bridge motor driver | 1 | Drives two DC motors, needs separate 5Vā12V |
| DC gear motors with wheels | 2 | 3Vā6V, ~200mA stall current each |
| 74AHCT125 level shifter | 1 | Servo signal: 3.3V ā 5V |
| 9V battery or 2S LiPo | 1 | Motor power supply ā separate from logic |
| Jumper wires | 12 | Several gauge sizes |
| USB cable | 1 |
YODA: "Two power rails, we shall have. Logic power: USB 5V ā 3.3V for ESP32-S3. Motor power: separate battery for L298N and motors. Cross them, you must not."
The ESP32-S3 and Servo Control: LEDC at 50Hz āļø
C-3PO raises a cautionary finger.
C-3PO: "I must explain the servo PWM situation. The Arduino Servo library used the Timer1 hardware. On the ESP32-S3, we use the LEDC peripheral ā the same one we used for the buzzer tone in Episode 5, but configured at 50Hz for servo control. A standard hobby servo expects a pulse between 0.5ms and 2.5ms at a 50Hz frequency. Codey handles this calculation automatically."
HAN: "Three-PO. The servo. Just the servo."
C-3PO: "Yes. Moving on."
In Agent mode:
We're continuing R2-D2 on ESP32-S3 N16R8.
Add motion systems:
1. SG90 servo for dome rotation:
- Signal on GPIO14 (through 74AHCT125 level shifter)
- 5V power from VIN rail
- Create these movements:
a. idle_scan: slow sweep 60°ā120° and back, 3 second period
b. alert_snap: snap to face sensor direction
c. full_sweep: 0° to 180° and back when "happy"
2. L298N motor driver for two DC wheels:
- Motor A: IN1=GPIO25, IN2=GPIO26, ENA=GPIO27 (PWM)
- Motor B: IN3=GPIO32, IN4=GPIO33, ENB=GPIO34 (PWM)
- Create: forward(), backward(), turnLeft(), turnRight(), stop()
- Speed: 0-255 mapped to LEDC duty cycle
3. Behavior: when distance < 30cm ā stop + snap dome toward sensor
When motion detected ā sweep dome toward source
Create motion.h file for all motion code.
Keep existing animations.h, sensors.h, display.h, sounds.h unchanged.
Generated motion.h
// motion.h ā R2-D2 Motion System
// Dome servo + DC wheel motors via L298N
// ESP32-S3 N16R8
#pragma once
// āā SERVO (dome rotation) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
#define SERVO_PIN 14 // GPIO14 ā through 74AHCT125 to servo
#define SERVO_CHANNEL 1 // LEDC channel (channel 0 used by buzzer)
// Servo pulse widths in microseconds
#define SERVO_MIN_US 500 // 0°
#define SERVO_MID_US 1500 // 90° (centre)
#define SERVO_MAX_US 2500 // 180°
// LEDC config for servo: 50Hz, 16-bit resolution (allows fine pulse control)
#define SERVO_FREQ 50
#define SERVO_RES 16 // 16-bit ā 0ā65535
int currentDomeAngle = 90; // Start centred
int targetDomeAngle = 90;
int domeSweepDir = 1; // +1 or -1 for idle sweep
unsigned long lastServoMs = 0;
void initServo() {
ledcSetup(SERVO_CHANNEL, SERVO_FREQ, SERVO_RES);
ledcAttachPin(SERVO_PIN, SERVO_CHANNEL);
// Centre the dome on startup
int pulse = map(90, 0, 180, SERVO_MIN_US, SERVO_MAX_US);
int duty = (int)((float)pulse / 20000.0f * 65535.0f);
ledcWrite(SERVO_CHANNEL, duty);
Serial.println("Dome servo centred.");
}
void setDomeAngle(int angle) {
angle = constrain(angle, 0, 180);
int pulse = map(angle, 0, 180, SERVO_MIN_US, SERVO_MAX_US);
int duty = (int)((float)pulse / 20000.0f * 65535.0f);
ledcWrite(SERVO_CHANNEL, duty);
currentDomeAngle = angle;
}
// Idle: slow sweep 60° ā 120°
void domeIdleSweep() {
unsigned long now = millis();
if (now - lastServoMs < 30) return;
lastServoMs = now;
currentDomeAngle += domeSweepDir;
if (currentDomeAngle >= 120) domeSweepDir = -1;
if (currentDomeAngle <= 60) domeSweepDir = 1;
setDomeAngle(currentDomeAngle);
}
// Snap to angle quickly (alert behaviour)
void domeSnap(int angle) {
setDomeAngle(angle);
}
// Full sweep 0°ā180°ā0° (happy behaviour)
unsigned long fullSweepMs = 0;
int fullSweepAngle = 0;
int fullSweepDir = 1;
bool fullSweepDone = false;
void domeSweepFull() {
unsigned long now = millis();
if (now - fullSweepMs < 12) return;
fullSweepMs = now;
fullSweepAngle += fullSweepDir * 2;
if (fullSweepAngle >= 180) { fullSweepDir = -1; }
if (fullSweepAngle <= 0) { fullSweepDone = true; }
setDomeAngle(fullSweepAngle);
}
// āā L298N MOTOR DRIVER āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
#define MOTOR_A_IN1 25
#define MOTOR_A_IN2 26
#define MOTOR_A_EN 27 // PWM channel 2
#define MOTOR_B_IN3 32
#define MOTOR_B_IN4 33
#define MOTOR_B_EN 34 // PWM channel 3
#define MOTOR_CHANNEL_A 2
#define MOTOR_CHANNEL_B 3
#define MOTOR_FREQ 5000 // 5kHz PWM for motors
#define MOTOR_RES 8 // 8-bit ā 0ā255
void initMotors() {
// Direction pins
pinMode(MOTOR_A_IN1, OUTPUT);
pinMode(MOTOR_A_IN2, OUTPUT);
pinMode(MOTOR_B_IN3, OUTPUT);
pinMode(MOTOR_B_IN4, OUTPUT);
// PWM channels for enable pins
ledcSetup(MOTOR_CHANNEL_A, MOTOR_FREQ, MOTOR_RES);
ledcSetup(MOTOR_CHANNEL_B, MOTOR_FREQ, MOTOR_RES);
ledcAttachPin(MOTOR_A_EN, MOTOR_CHANNEL_A);
ledcAttachPin(MOTOR_B_EN, MOTOR_CHANNEL_B);
// Start stopped
ledcWrite(MOTOR_CHANNEL_A, 0);
ledcWrite(MOTOR_CHANNEL_B, 0);
Serial.println("Motor driver online.");
}
void motorForward(uint8_t speed = 180) {
digitalWrite(MOTOR_A_IN1, HIGH); digitalWrite(MOTOR_A_IN2, LOW);
digitalWrite(MOTOR_B_IN3, HIGH); digitalWrite(MOTOR_B_IN4, LOW);
ledcWrite(MOTOR_CHANNEL_A, speed);
ledcWrite(MOTOR_CHANNEL_B, speed);
}
void motorBackward(uint8_t speed = 180) {
digitalWrite(MOTOR_A_IN1, LOW); digitalWrite(MOTOR_A_IN2, HIGH);
digitalWrite(MOTOR_B_IN3, LOW); digitalWrite(MOTOR_B_IN4, HIGH);
ledcWrite(MOTOR_CHANNEL_A, speed);
ledcWrite(MOTOR_CHANNEL_B, speed);
}
void motorTurnLeft(uint8_t speed = 150) {
// Left motor backward, right motor forward = left turn
digitalWrite(MOTOR_A_IN1, LOW); digitalWrite(MOTOR_A_IN2, HIGH);
digitalWrite(MOTOR_B_IN3, HIGH); digitalWrite(MOTOR_B_IN4, LOW);
ledcWrite(MOTOR_CHANNEL_A, speed);
ledcWrite(MOTOR_CHANNEL_B, speed);
}
void motorTurnRight(uint8_t speed = 150) {
// Left motor forward, right motor backward = right turn
digitalWrite(MOTOR_A_IN1, HIGH); digitalWrite(MOTOR_A_IN2, LOW);
digitalWrite(MOTOR_B_IN3, LOW); digitalWrite(MOTOR_B_IN4, HIGH);
ledcWrite(MOTOR_CHANNEL_A, speed);
ledcWrite(MOTOR_CHANNEL_B, speed);
}
void motorStop() {
ledcWrite(MOTOR_CHANNEL_A, 0);
ledcWrite(MOTOR_CHANNEL_B, 0);
digitalWrite(MOTOR_A_IN1, LOW); digitalWrite(MOTOR_A_IN2, LOW);
digitalWrite(MOTOR_B_IN3, LOW); digitalWrite(MOTOR_B_IN4, LOW);
}
void initMotion() {
initServo();
initMotors();
}
Updated r2d2-main.ino loop
// Loop addition ā reactive motion
void loop() {
if (!bootDone) { showBootScreen(); return; }
float dist = readDistance();
bool motion = checkMotion();
// āā Dome behaviour āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
if (dist < 30.0f) {
domeSnap(90); // face forward, something is ahead
motorStop(); // stop wheels ā obstacle!
} else if (motion) {
domeSnap(45); // snap left ā motion detected
} else {
domeIdleSweep(); // gentle idle scan
}
// āā Display āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
if (dist < 15.0f) showAlertScreen();
else if (motion) showMotionScreen();
else showIdleScreen(dist);
// āā Dome lights āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
updateAnimationsSensors(dist, motion);
}
The Wiring Diagram ā Two Power Rails š§
C-3PO studies the diagram, then exhales slowly.
C-3PO: "Codey has correctly separated the power rails. The logic rail ā 3.3V from the ESP32-S3 regulator ā powers only the microcontroller and the OLED. The motor rail ā the external 9V battery through the L298N's onboard 5V regulator ā powers the motors and the servo. This is exactly correct. Mixing them would cause voltage drops that would reset the ESP32-S3 every time a motor starts."
R2-D2 Motion System ā Wiring Diagram (ESP32-S3 N16R8)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
POWER RAIL 1 ā LOGIC (USB 5V ā ESP32-S3 regulator):
USB 5V āāāā ESP32-S3 VIN
ESP32 3V3 āā OLED VCC
ESP32 GND āā OLED GND, logic GND rail
POWER RAIL 2 ā MOTOR POWER (9V battery):
9V Bat (+) āā L298N: VS (motor supply)
9V Bat (ā) āā L298N: GND (common ground with ESP32 GND)
L298N: 5V āā Servo VCC (5V regulated output)
L298N: 5V āā 74AHCT125: VCC
L298N: GND āā Servo GND, 74AHCT125 GND
ā ļø IMPORTANT: ESP32 GND and battery GND must be connected together!
SERVO (dome rotation):
ESP32 GPIO14 āāā 74AHCT125 A1 input (3.3V signal in)
74AHCT125 Y1 āāā Servo Signal wire (orange/yellow) ā 5V signal out
L298N 5V āāā Servo VCC (red wire)
L298N GND āāā Servo GND (brown/black wire)
MOTORS via L298N:
ESP32 GPIO25 āāāā L298N IN1 (Motor A direction)
ESP32 GPIO26 āāāā L298N IN2 (Motor A direction)
ESP32 GPIO27 āāāā L298N ENA (Motor A PWM speed)
ESP32 GPIO32 āāāā L298N IN3 (Motor B direction)
ESP32 GPIO33 āāāā L298N IN4 (Motor B direction)
ESP32 GPIO34 āāāā L298N ENB (Motor B PWM speed)
L298N OUT1 āāāā DC Motor A: terminal 1
L298N OUT2 āāāā DC Motor A: terminal 2
L298N OUT3 āāāā DC Motor B: terminal 1
L298N OUT4 āāāā DC Motor B: terminal 2
NeoPixel (from Episode 5):
ESP32 GPIO6 āā 74AHCT125 A2 āā NeoPixel DIN
Color code:
RED = 5V / VIN power
ORANGE = 9V battery supply
BLACK = GND
PURPLE = 3.3V logic
GREEN = NeoPixel data (level-shifted)
BLUE = Servo signal (level-shifted)
YELLOW = Motor direction signals
WHITE = Motor PWM (speed) signals
Connection Table:
āāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā From ā To ā
āāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā 9V Battery (+) ā L298N: VS ā
ā 9V Battery (ā) ā L298N: GND + ESP32 GND (common) ā
ā L298N: 5V out ā Servo VCC (red), 74AHCT125 VCC ā
ā L298N: GND ā Servo GND, 74AHCT125 GND ā
ā ESP32 GPIO14 ā 74AHCT125 A1 ā servo signal (5V) ā
ā ESP32 GPIO6 ā 74AHCT125 A2 ā NeoPixel DIN (5V) ā
ā ESP32 GPIO25 ā L298N IN1 ā
ā ESP32 GPIO26 ā L298N IN2 ā
ā ESP32 GPIO27 (PWM) ā L298N ENA ā
ā ESP32 GPIO32 ā L298N IN3 ā
ā ESP32 GPIO33 ā L298N IN4 ā
ā ESP32 GPIO34 (PWM) ā L298N ENB ā
ā L298N OUT1,OUT2 ā DC Motor A ā
ā L298N OUT3,OUT4 ā DC Motor B ā
āāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
HAN: "Two power rails. That's important. I've seen people blow a microcontroller trying to power motors directly off the GPIO pins. The L298N's internal regulator gives you the 5V for the servo and the level shifter. Clean."
R2-D2 beeps approvingly.
HAN: "Yeah, even I have to admit that's well done."
Compile and Watch the Dome Spin š
ā Compilation successful
Board: ESP32-S3 N16R8
Sketch: r2d2-main.ino + 5 headers
Binary: 498,244 bytes (7.2% of 16MB Flash)
RAM: Used 34,128 bytes (10.4% of 327KB)
The dome servo sweeps slowly between 60° and 120°. R2-D2 scans the room.
Wave your hand in front of the HC-SR04. The dome snaps to 90°. The wheels stop. The OLED shows "TOO CLOSE!"
Han Solo watches the dome turn. A long pause.
HAN: "...Okay. That's actually pretty good."
R2-D2 beeps.
HAN: "I said it was pretty good. Don't push it."
Save Milestone š©
Milestone: "R2-D2 Motion Systems ā Episode 6 Complete"
Five systems running. Dome rotating. Wheels ready. The droid is almost complete.
What's Next: R2 Gets His Voice Back š
Obi-Wan speaks with warmth.
OBI-WAN: "The dome spins. The lights glow. The screen projects. The wheels wait. But something is still missing ā the true voice. Not just beeps from a buzzer, but the full audio character of the galaxy's most beloved droid. In Episode 7, we add the DFPlayer Mini and a speaker. R2-D2 will play actual audio files. And the Live Serial Monitor will show us exactly what is happening."
R2-D2 emits a long, heartfelt whistle that says everything.
š Resources
- ESP32 LEDC (servo): randomnerdtutorials.com/esp32-servo-motor-web-server
- L298N motor driver: Search "L298N Arduino ESP32 tutorial"
- 74AHCT125 level shifter: Search "74AHCT125 3.3V 5V logic level converter"
- Codey Online: codey.online
š¤ R2D2 Creation with Codey ā building the galaxy's greatest droid, one episode at a time. May the Force ā and the cloud compiler ā be with you.
Top comments (0)