Waving Desk Robot: ESP32 servo basics without a library
Servos are the joints of robot arms and legs. Instead of hiding the details behind a library, this project generates the servo signal directly with the ESP32 PWM hardware — so you actually understand how the angle is set.
1. Parts
| Part | Qty | Notes |
|---|---|---|
| ESP32 DevKit V1 | 1 | |
| SG90 micro servo | 1 | The small blue one with plastic horns |
| HC-SR04 ultrasonic sensor | 1 | Optional: wave only when someone is near |
| 1 kΩ and 2 kΩ resistors | 1 each | ECHO divider |
| 100–470 µF electrolytic capacitor | 1 | Recommended: steadies the servo supply |
| Cardboard box, popsicle sticks or a 3D-printed shell | The robot's body and arm |
2. How a servo works
An SG90 expects one pulse every 20 ms (50 Hz). The pulse width sets the angle: about 0.5 ms is 0°, about 2.4 ms is 180°, linear in between.
Set LEDC to 50 Hz with 14-bit resolution (0–16383). One 20,000 µs period maps to 16383, so:
duty = pulse (µs) ÷ 20000 × 16383
That's all servoWrite() does. Every SG90 is slightly different — if it buzzes at the ends of its travel, raise PULSE_MIN_US and lower PULSE_MAX_US.
3. Wiring
| From | To | Notes |
|---|---|---|
| SG90 orange (signal) | ESP32 GPIO 23 | |
| SG90 red (power) | ESP32 VIN (~5 V on USB) | Not 3.3 V — too weak |
| SG90 brown (ground) | ESP32 GND | |
| Capacitor | + to VIN, − to GND | Mind the polarity; long leg is + |
| HC-SR04 VCC / GND | VIN / GND | |
| HC-SR04 TRIG | GPIO 18 | |
| HC-SR04 ECHO | 1 kΩ → GPIO 19; GPIO 19 → 2 kΩ → GND | ~3.3 V |
4. Code
No ultrasonic sensor? Set USE_SENSOR to false and it waves every 5 seconds.
/*
* TinyBot Forge — Waving Desk Robot (servo basics)
* https://tinybotforge.com/en/
*
* No servo library: the ESP32 LEDC peripheral generates the 50 Hz servo signal directly.
* Hardware: ESP32 DevKit V1, SG90 servo, HC-SR04 (optional: wave only when someone is near)
* Toolchain: Arduino IDE 2.x + ESP32 board package 3.x
*/
const int PIN_SERVO = 23;
const int PIN_TRIG = 18;
const int PIN_ECHO = 19; // HC-SR04 ECHO needs a 1k/2k voltage divider
const bool USE_SENSOR = true; // no sensor? set to false and it waves every 5 s
// SG90: 20 ms period (50 Hz); ~0.5 ms pulse = 0°, ~2.4 ms = 180°
const int SERVO_FREQ = 50;
const int SERVO_BITS = 14; // 14 bit -> 0-16383
const int PULSE_MIN_US = 500;
const int PULSE_MAX_US = 2400;
const float WAVE_TRIGGER_CM = 40.0;
void servoWrite(int angle) {
angle = constrain(angle, 0, 180);
int us = map(angle, 0, 180, PULSE_MIN_US, PULSE_MAX_US);
// pulse width to duty: duty = us / 20000 * 16383
uint32_t duty = (uint32_t)us * ((1UL << SERVO_BITS) - 1) / 20000UL;
ledcWrite(PIN_SERVO, duty);
}
// Smooth move one degree at a time: looks natural and avoids current spikes
void servoSweep(int from, int to, int stepDelayMs) {
int step = (to > from) ? 1 : -1;
for (int a = from; a != to; a += step) {
servoWrite(a);
delay(stepDelayMs);
}
servoWrite(to);
}
float readDistanceCm() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
unsigned long us = pulseIn(PIN_ECHO, HIGH, 25000UL);
return us == 0 ? 999.0 : us / 58.0;
}
void wave() {
servoSweep(90, 150, 4); // raise arm
for (int i = 0; i < 3; i++) { // wave three times
servoSweep(150, 110, 3);
servoSweep(110, 150, 3);
}
servoSweep(150, 90, 6); // lower arm
}
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
ledcAttach(PIN_SERVO, SERVO_FREQ, SERVO_BITS);
servoWrite(90);
delay(500);
}
void loop() {
if (USE_SENSOR) {
float cm = readDistanceCm();
Serial.printf("distance %.1f cm\n", cm);
if (cm < WAVE_TRIGGER_CM) {
wave();
delay(1500); // pause so it doesn't wave nonstop
}
delay(100);
} else {
wave();
delay(5000);
}
}
5. Assembly tips
- Center first: after uploading, the servo goes to 90°. Attach the horn so the arm hangs naturally at that position.
- Hot-glue the servo to the side of the box and a popsicle stick to the horn.
- Mount the HC-SR04 on the front — its two cylinders make great eyes.
6. Troubleshooting
The servo jitters
Usually a weak supply. Add the capacitor or use external 5 V, and check the ground connection.
Why not just use the ESP32Servo library?
It does the same math and is more convenient. Writing it yourself once means you'll understand servo signals on any board.
It doesn't reach a full 180°
Many SG90s only travel 160–170°. Adjust the pulse limits to find yours.
Free: ESP32 Robot Wiring Cheat Sheet
One printable page: safe ESP32 pins, L298N, HC-SR04, servo wiring and the most common fixes.
Download the PDF