Home › Projects › Waving Desk Robot

Waving Desk Robot: ESP32 servo basics without a library

Beginner~45 minutesNo libraryESP32 core 3.x

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.

✓ Compile-checked on 2026-09-29 with the Arduino ESP32 board package 3.3.12 (all warnings enabled, 0 warnings), board: ESP32 Dev Module.

1. Parts

PartQtyNotes
ESP32 DevKit V11
SG90 micro servo1The small blue one with plastic horns
HC-SR04 ultrasonic sensor1Optional: wave only when someone is near
1 kΩ and 2 kΩ resistors1 eachECHO divider
100–470 µF electrolytic capacitor1Recommended: steadies the servo supply
Cardboard box, popsicle sticks or a 3D-printed shellThe 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

FromToNotes
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 GNDMind the polarity; long leg is +
HC-SR04 VCC / GNDVIN / GND
HC-SR04 TRIGGPIO 18
HC-SR04 ECHO1 kΩ → GPIO 19; GPIO 19 → 2 kΩ → GND~3.3 V
One SG90 on USB power is fine; two or more need an external 5 V supply. Servos draw big current spikes that can reset the ESP32. Tie the supply's ground to the ESP32's ground.

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);
  }
}
Download the .ino file

5. Assembly tips

  1. Center first: after uploading, the servo goes to 90°. Attach the horn so the arm hangs naturally at that position.
  2. Hot-glue the servo to the side of the box and a popsicle stick to the horn.
  3. 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