Servos
A hobby servo is a small gearmotor with its own position controller: you send it a pulse width, it turns its output shaft to the matching angle and holds it there. Strido uses twelve of them, one per joint, and everything about how the robot moves starts with how these servos are driven.
In Strido
- Part: SPT5435LV-180W digital servo, 13 bought (12 + 1 spare), BOM line
servo_spt5435lvinhardware/bom.yaml. - Joints: hip (abduction), thigh and knee crank on each leg, FL, FR, RL, RR. PCA9685 channels 4–15 (
hardware/electronics.yaml,servos.channels). - Signal: PCA9685 PWM driver at 200 Hz, 500–2500 µs, 3.3 V logic level, enabled through the XIAO D3 → PCA9685 OE line.
- Power: the 6.0 V servo rail from two Hobbywing UBEC-10A, six servos each, through the WAGO buses in the front and rear cavities.
- Horns: 25T aluminium disc horns (8) for hip and thigh, 25T aluminium clamping arm horns (4) for the knee crank.
- Code:
firmware/strido_esp32s3/lib/strido_core/src/servo_map.cpp(angle → pulse),strido-cli center/calibrate.
What is inside a hobby servo
A servo packs four things into a 40.5 × 20 × 40.5 mm case (72 g for the SPT5435LV):
- DC motor. The SPT5435LV uses a cored (iron-armature) motor.
- Gearbox. All-metal gears trade speed for torque. The output shaft carries a 25-tooth spline (Futaba 25T) and runs in two ball bearings (2BB).
- Potentiometer. It sits on the output shaft and turns shaft angle into a voltage, so the servo always knows where its output is.
- Control board. It compares the commanded angle (from the pulse width) with the measured angle (from the potentiometer) and drives the motor to close the gap. This is a closed position loop inside the servo.
The control loop never leaves the servo. The robot only commands angles and cannot read them back, so /joint_states reports the angles the firmware commanded after clamping, not measured ones (docs/software.md).
Analogue vs digital
An analogue servo updates its motor drive at the rate the pulses arrive. A digital servo has a microcontroller that samples the pulse and re-drives the motor at a much higher internal rate. The result is a stiffer hold, a smaller dead band and faster response. The cost is a higher idle current: the SPT page lists 100 mA quiescent. For a walking robot, the stiff hold is what keeps a leg from sagging under load.
The pulse-width signal (PWM)
The servo reads the width of a repeating logic pulse, not its frequency or duty cycle:
| Pulse width | SPT5435LV-180W shaft |
|---|---|
| 500 µs | one end (−90° from neutral) |
| 1500 µs | neutral point (centre) |
| 2500 µs | other end (+90°) |
Inside that window the mapping is linear: the angle
The radian form of kUsPerRad in servo_map.h.
The pulse repeats at a fixed rate. Classic RC gear sends one pulse every 20 ms (50 Hz). The SPT5435LV accepts up to 330 Hz, and Strido runs at 200 Hz (kServoFrequencyHz), one new pulse every
In general PWM terms, a pulse of width
Buy the right angle variant
The same pulse range means different angles on different variants. On a 270° servo, 500–2500 µs would cover 270°, and every joint mapping would be wrong. The BOM requires the -180W variant; confirm the angle with any non-SPT seller (hardware/bom.yaml, note on servo_spt5435lv).
Resolution and dead band
The dead band is the smallest change in pulse width the servo reacts to. The SPT page gives 4 µs, about 0.36°.
The PCA9685 that generates the pulses divides each period into 4096 steps (12-bit):
At 200 Hz one step is
At 50 Hz one step would already be wider than the dead band; at 200 Hz the driver resolves about three steps inside it. The higher frame rate buys both faster updates and finer positioning.
Signal level
The SPT page specifies a PWM signal level of 3.3–5.0 V. The PCA9685 runs from the XIAO's 3.3 V regulator, so its pulses are exactly 3.3 V high: in spec, but with no margin. docs/verification/electronics.md flags this check as "pass, no margin". If a servo jitters, measure the XIAO 3V3 rail first.
Torque, speed and current
| Specification (SPT spec page) | 4.8 V | 6.0 V |
|---|---|---|
| Stall torque | 29 kg·cm | 35 kg·cm |
| Speed (no load) | 0.16 s / 60° | 0.14 s / 60° |
| Rated current | 1.4 A | |
| Stall (blocking) current | 3.5 A |
Stall torque in SI units
Hobby servos quote torque in kg·cm: the weight of a mass in kilograms hanging from an arm in centimetres. Torque is force times lever arm,
Stall torque is the most the servo can push when it is not moving at all. It is a limit, not a working point. In MuJoCo the standing knee needs about 0.7 N·m, roughly a fifth of stall (docs/electronics.md). That margin is what lets the robot trot, recover from a stumble and carry its 3.07 kg (docs/design.md).
Current follows torque
A DC motor's torque is proportional to its current,
A servo pushing into a hard stop, or fighting another leg, sits at stall current and heats up fast. Two rules in the docs follow from this:
- Never insert the arming key while position commands are running: all twelve servos would jump at once.
- Never command all legs into hard stops.
The firmware also limits joint speed to 6 rad/s (max_joint_speed_rad_s in safety.h), below the servo's no-load speed at 6 V,
The 6 V servo rail
The SPT5435LV is rated 4.8–6.0 V only. The 2S LiPo swings from 8.4 V (full) to 6.4 V (cut-off), which is always above that range. A connection straight to the pack would over-volt every servo. Two Hobbywing UBEC-10A regulators therefore step the pack down to 6.0 V:
- The UBEC's output jumper stays open: that selects 6.0 V. The 7.4 V setting would destroy the servos.
- Measure every UBEC unloaded before connecting servos and accept only 5.8–6.0 V (electronics › wiring harness, step 2).
- Under load near an empty pack, the rail is expected to sag to about 5.4 V (estimate in
hardware/electronics.yaml). The servo is still in range but weaker: torque falls roughly with voltage (29 kg·cm at 4.8 V vs 35 kg·cm at 6 V).
The PCA9685 carries only the signal. Servo current flows UBEC → WAGO bus → the servo's red/brown wires, and never through a logic board. The V+ pin on the PCA9685 header stays unused. More detail is in Power.
Horns and the 25T spline
The horn couples the servo's output spline to the robot's structure. Strido uses aluminium horns because printed parts bolted to a plastic horn would strip the spline or flex.
| Horn | Qty | Joint | Fixed with |
|---|---|---|---|
| 25T aluminium disc horn | 4 | thigh: femur and femur servo connector, 4 × M3 × 20 | M3 × 8 centre screw |
| 25T aluminium disc horn | 4 | hip: leg module, 2 × M3 × 8 | M3 × 8 centre screw |
| 25T aluminium clamping arm horn | 4 | knee drive crank | M3 × 8 centre screw |
25T means 25 teeth on the spline. Each tooth is
Screws that go from steel into the aluminium horn get one drop of medium thread locker: metal-to-metal joints vibrate loose otherwise (docs/assembly/README.md).
Centring and calibration
Step 1: centre before you fit a horn
Before any horn goes on, every servo is driven to 1500 µs, its neutral:
strido-cli --port <usb port> center # 1500 µs on every channelWith the shaft at neutral, each horn goes on at the assembly reference pose:
| Joint | Reference at 1500 µs |
|---|---|
| Hip | leg vertical |
| Thigh | femur 45° backwards |
| Knee crank | 35° |
These are the neutral_hip, neutral_thigh and neutral_crank values in the generated geometry (0, 0.785 and 0.611 rad). Because the spline only allows 14.4° steps, pick the tooth that comes closest; the remainder is fixed in software.
Step 2: trim in firmware
Each joint has a ServoCalibration record stored in the ESP32 flash (Preferences):
| Field | Meaning |
|---|---|
direction | +1 or −1: mirrored servos on the left and right turn the opposite way for the same joint motion |
neutral_us | pulse that puts the joint at its reference angle (default 1500) |
zero_rad | the reference angle itself |
min_us, max_us | hard pulse window |
The firmware converts an actuator angle to a pulse with:
with neutral_us, the sign direction, zero_rad; the result is clamped to [min_us, max_us].
Worked example: to move a thigh with direction = +1 from its 45° reference to 60°:
strido-cli calibrate lets you nudge each neutral_us in ±5 µs or ±25 µs steps until the joint sits exactly at its reference, then saves to flash. The robot should be on a stand with its legs free. A half-tooth error (7.2°) is about 80 µs, well within the trim range. Calibration also absorbs small per-chip differences in the PCA9685 clock, which scale every pulse.
Raw pulses bypass joint limits
strido-cli center and pulse send raw pulse widths. They skip the joint and knee-crank limits from the CAD range-of-motion sweep. Use them only while horns are off or the legs are free to move.
Check yourself
- Why can the robot not report the true angle of a joint?
- What pulse width does the firmware send for a joint 10° past its reference, with
direction = −1andneutral_us = 1500? - Why is the servo rail regulated to 6.0 V instead of taken straight from the 7.4 V pack?
- A horn ends up visibly crooked even after centring. How big can the error be before you should move it one tooth?
- Why does each UBEC carry a stall warning in
docs/verification/electronics.md?
Answers
- The position feedback (potentiometer) is used only inside the servo. Nothing reads it back, so the firmware reports the angles it commanded.
; ; with direction the pulse is . - The SPT5435LV is rated 4.8–6.0 V only; the pack is 6.4–8.4 V in use and would over-volt it.
- Up to half a tooth, 7.2° (≈ 80 µs), is a job for
strido-cli calibrate. Beyond that, move the horn one tooth (14.4°). - Six stalled servos draw
, above the UBEC's 15 A peak rating.
Further reading
- SPT5435LV-180W specification (SPT Servo)
- PCA9685 datasheet (NXP, Adafruit mirror)
- Adafruit 16-channel PWM/servo driver guide
- Servo spline reference (ServoCity)
- Related pages: Power, Microcontrollers, Mechanics, Electronics, wiring and power
- Theory: Control (the PD loop inside the servo), Statics and dynamics (joint torques)