| Item | Value |
|---|---|
| Model | Analog micro servo — Tower Pro SG90 recommended (presets) |
| Count | 5 |
| Driver | Adafruit PCA9685, I2C 0x40 |
| Servo supply | PCA9685 V+ ← drawing: USB 5V → PCA9685 5V (same servo rail as V+; not VCC) |
| PWM frequency | 50 Hz (pwm.setPWMFreq(50)) |
| Channels | 0, 1, 2, 3, 4 (SERVO_HEAD…SERVO_BODY in SERVO_SPECS) |
Warning
Never power the servos from the ESP32 3.3 V regulator.
Manufacturer pulse window used in firmware:
| Constant | Value |
|---|---|
SERVO_MIN_US |
800 µs |
SERVO_MAX_US |
2200 µs |
| Neutral (typical) | ~1500 µs (90° in the firmware 0–180 map) |
angleToPulse() in src/hardware/servo_wrapper.cpp maps firmware angle 0–180° linearly onto 800–2200 µs, then onto PCA9685 12-bit counts assuming a 20 000 µs period (rounded to nearest count):
counts = round(pulse_us * 4096 / 20000)
Effective angular spacing is ~0.63° per count (~288 distinct positions over 180°). writeAngle() skips redundant I2C writes when the rounded count is unchanged. The PCA9685 keeps emitting that last pulse until a later write — skipping I2C does not turn the servo off.
Datasheet travel over 800–2200 µs is approximately 130°, while many sellers list 0–180°. Firmware still uses a 0–180 mathematical scale. That scale is not a promise of mechanical 180° in the robot.
Verified in include/pins.h and include/servos.h:
| Constant | Value | Role |
|---|---|---|
SERVO_STEP_MS |
10 | Live update / interpolation step (ms) |
SERVO_ANGLE_DEADBAND_DEG |
0.32 | Stop threshold (~half PWM count) |
SERVO_PWM_RELEASE_DELAY_MS |
2000 | After idle/sleep park, hold PWM this long then full-off |
SERVO_MAX_SPEED_DEG_S |
140.0 | Smooth rate for POST /test/servo |
SERVO_BOOT_SPEED_DEG_S |
35.0 | Boot centering and sleep-pose moves |
On every boot (and after each flash reset):
- OE — GP5 drives PCA9685 OE (active LOW). Firmware holds OE HIGH (outputs off) from early
setup, then LOW after PWM is written (PCA9685_OE_WIRED = trueininclude/pins.h). - Early init — PCA9685 is probed and configured immediately after
Wire.begin, before OLED, settings, or Wi-Fi. - Last-pose restore — NVS key
spose(namespacete) holds the last commanded angles (5× degrees). While OE is still HIGH, firmware writes those pulses (calibrated mid fallback if missing), then OE goes LOW. That cuts the common flash snap when the robot was already near the saved pose. - Smooth boot moves —
centerAllServos()and sleep-inertia pose useservoMoveAllSmoothTo()atSERVO_BOOT_SPEED_DEG_S(35°/s), not instant snaps.
Rapid flash/upload can still yank joints if the body drooped after PWM limp (software pose ≠ physical). Mitigations:
| Tool | Role |
|---|---|
POST /servos/park / serial servos_park |
Smooth mid, flush spose, keep PWM until reboot |
PlatformIO pre:upload |
scripts/park_servos_before_upload.py shells tiny-engineer park --transport serial; warns and continues if the robot is offline |
welcome=0 |
Quieter post-boot motion while iterating (optional) |
| Unplug servo 5 V / V+ | Absolute safety when pose is unknown |
Analog servos hunt while a 50 Hz pulse is present. Firmware does not use OE for this: after animation none parks (awake rest or device-sleep chin-down) and every joint is still for SERVO_PWM_RELEASE_DELAY_MS (2000 ms), it writes PCA9685 full-off (setPWM(channel, 0, 4096)) on channels 0–4. SIG stays low; the servo goes limp (no holding torque). Head pitch can droop; hands-down usually rests on the desk.
Skipped when POST /servos/park armed a until-reboot hold, or in setup AP.
The next animation, /test/servo, or /setup/servo rewrite the last commanded pulse before moving. Leaving none restores all five channels at their last angles first.
Setup AP (wifiProvisioningMode) never releases PWM, so “Move all to 90°” / horn install still holds.
Firmware uses two complementary control paths:
| Mode | API | Use |
|---|---|---|
| Choreographed | setNormPosition() + time easing (anim::easedLerp, anim::EasedMove) |
Welcome raise/wiggle, thinking head/neck — pose in −1..1, mapped to saved min/max each frame |
| Discrete | setNormTarget() + update() slew |
Typing, reading, ring, transitions — rate-limited chase to a mapped pose |
Helpers live in src/animation/util.cpp. Blocking test moves (moveTo) also use cubic easing.
SERVO_MAX_SPEED_DEG_S (140°/s) is ~28% of PowerHD HD-1370A unloaded max (~500°/s @ 4.8 V) — smoother under load while staying responsive for hand taps. Other presets (SG90, FS0307, ES9051) still use this firmware cap.
Bring-up motion (runServoTest): each joint uses its saved min/max (n in −1..1):
- All channels → mid (
n = 0) - Smooth mid → +0.5 (75% of span)
- Smooth +0.5 → −0.5 (25% of span)
- Smooth −0.5 → mid
This is a wiring/power test, not a pose library.
| # | Range | Meaning | Current status |
|---|---|---|---|
| 1 | Electrical PWM | Pulse widths the servo electronics accept (~800–2200 µs @ 50 Hz) | In firmware |
| 2 | Nominal manufacturer angle | Marketing / datasheet travel (0–180° or ~130° over full pulse — sources disagree) | Do not trust for installed mechanics |
| 3 | Mechanical safe range | Per-joint min/max after horns and linkages | Stock defaults in SERVO_SPECS (include/servos.h); saved to NVS in the setup AP wizard. See robot-movement.md |
Animations author poses in −1..1 (min / mid / max of range 3) and map through servoNormToDeg. POST /test/servo still takes electrical degrees and clamps to range 3. Setup AP POST /setup/servo uses range 1 (0–180°) to find limits.
Blind 0–180° on the assembled robot can stall gears, tear horns, or brown out the 5 V rail. Stay inside the saved min/max (stock SERVO_SPECS until you calibrate in setup AP).
See robot-movement.md for layout, axis directions, and per-servo safe angles (include/servos.h).
Related: main-control-board.md, power.md, testing.md. Advanced harness: wiring.md.