Skip to content

Kinematics and gaits ​

Kinematics is the geometry of motion: given joint angles, where is the foot (forward kinematics), and given a foot position, which joint angles put it there (inverse kinematics). A gait is the rhythm in which the four feet lift, swing and push. This page works through both for the Strido leg, using the code that actually drives the robot.

In Strido

  • Leg kinematics and knee linkage: packages/strido-core/src/strido/leg.py (Python) and its C++ mirror firmware/strido_esp32s3/lib/strido_core/src/kinematics.cpp + servo_map.cpp.
  • Gait: packages/strido-core/src/strido/gait.py (TrotGait, GaitConfig, stand_pose, clamp_leg), used unchanged by gait_node (ROS 2), strido_sim.walk (MuJoCo) and as the stand pose for the learned policy.
  • Dimensions and limits: measured from the CAD into hardware/kinematics.yaml and hardware/joint_limits.yaml, then generated into packages/strido-core/src/strido/geometry.py and geometry_generated.h (CAD as code).
  • Servos: 12 × SPT5435LV-180W position servos (servos).

The leg ​

Each leg has three joints, named and signed like the Unitree Go2 so that tools and policies transfer:

JointAxisZeroPositive directionActuated by
hip (q1)+x (forward)leg verticalfoot moves to +y (left)hip servo, direct
thigh (q2)+yfemur straight downknee moves backwardsthigh servo, direct (coaxial with the femur pivot)
calf (q3)+y, relative to the femurcalf in line with femurnegative = knee bentknee servo, through two linkages

Measured dimensions (front-left leg, generated values rounded):

QuantityValue
Hip axis position in base_linkx = ±0.120 m, y = ±0.039 m
Lateral offset hip axis → leg plane (thigh_offset_y)0.038 m front, 0.040 m rear
Thigh (femur) length l10.130 m
Calf (tibia) length l2, to foot centre0.124 m
Foot sphere radius0.012 m

Full extension is l1+l2≈0.254m; the robot stands with the hip axes 0.17 m above the foot centres.

The knee linkage ​

The knee servo does not sit at the knee: it is up in the leg module. It turns an arm; a parallelogram (arm 24 mm, short link 28.3 mm, crank) copies the arm angle onto a crank coaxial with the femur pivot; a four-bar (crank 26.9 mm, long link 131 mm, tibia crank 24 mm, femur) turns the tibia. Keeping the servo mass near the body makes the leg lighter to swing.

The cost: the knee angle q3 depends on both the crank angle ψ and the thigh angle q2. Moving only the thigh servo also changes the knee angle. So software must solve the linkage:

  • crank_from_calf(leg, q2, q3) gives the crank angle the knee servo must hold (inverse linkage), by intersecting two circles: one around the femur pivot with the crank radius, one around the tibia pin with the long-link length.
  • calf_from_crank(leg, q2, ψ) is the forward linkage.

Two circles intersect in two points; the code picks the branch the CAD assembly is in (_branches), so the solver never flips to the mirror configuration.

Transmission angle is the angle between the long link and the crank (or tibia crank). Near 0° or 180° the linkage is near a singularity: a small servo motion produces a huge knee motion and the servo loses leverage. The range-of-motion sweep keeps it at or above 25°; in the standing pose it is about 84–86°, close to the ideal 90°.

Forward kinematics ​

foot_position(name, q) in leg.py:

  1. In the leg plane (u forward, v up, origin at the thigh pivot):

    pknee=(−l1sin⁡q2−l1cos⁡q2),pfoot=pknee+(−l2sin⁡(q2+q3)−l2cos⁡(q2+q3))

    The calf's absolute angle is q2+q3 because q3 is measured relative to the femur: angles of a serial chain add.

  2. Place the plane point at lateral offset thigh_offset_y, rotate it about x by the hip angle q1, then add the hip position in base_link: p=phip+Rx(q1)(u, dy, v)⊤. The general method (homogeneous transforms) is in kinematics and frames and rotations.

Example from the code: the stand pose for the front-left leg is q=(0, 0.812, −1.676)rad=(0∘, 46.5∘, −96.0∘), and forward kinematics returns the foot at (0.120, 0.077, −0.170)m: right under the hip, 0.170 m down. Check the height in the plane: v=−0.130cos⁡(0.812)−0.124cos⁡(0.812−1.676)≈−0.089−0.081=−0.170m. The y value 0.077 m is the hip's 0.039 m plus the 0.038 m leg-plane offset.

Inverse kinematics ​

inverse_kinematics(name, foot) solves the reverse, analytically (no iteration):

  1. Hip. Look at the leg from the front. The foot must lie on a circle of radius dy (thigh_offset_y) around the hip axis plus a "depth" d along the leg, by Pythagoras: d=y2+z2−dy2. The hip angle is the rotation that aligns the leg plane with the foot: q1=atan2(z,y)−atan2(−d,dy).

  2. Knee from the law of cosines. In the leg plane the foot is at (u,v)=(x,−d). The triangle thigh–calf–(pivot to foot) has sides l1, l2 and r=u2+v2, so r2=l12+l22+2l1l2cos⁡q3, which gives

    cos⁡q3=u2+v2−l12−l222l1l2,q3=−arccos⁡(cos⁡q3)

    (negative: the knee bends backwards). If |cos⁡q3|>1 the target is out of reach and the function returns None.

  3. Thigh. The direction to the foot minus the angle the bent calf adds:

    q2=atan2(−u,−v)−atan2(l2sin⁡q3, l1+l2cos⁡q3)

Then clamp_leg applies the joint limits and the knee-crank window. If the crank needed is outside its window, it is clamped and the calf angle recomputed from the clamped crank: the firmware does exactly the same in actuator space, so simulation and robot agree.

Joint limits ​

Generated from the CAD range-of-motion sweep (collisions with all parts, a mirrored partner leg with 0.3 mm clearance, transmission angle ≥ 25°, and ±85° servo travel around the assembly neutral):

JointFLFRRLRR
Hip−17.5…50°−50…17.5°−17.5…45°−45…17.5°
Thigh−40…125°−40…125°−15…125°−15…125°
Calf−150…−45°−150…−45°−150…−45°−150…−45°
Knee crank−2.5…75°−2.5…75°−2.5…75°−2.5…75°

The inward hip limits (±17.5°) are small because both front (and both rear) feet could otherwise meet under the body (ADR-005 in decisions). The front thigh sweep allowed −45°, but the servo can only reach −40° within ±85° of its 45° neutral. Limits are applied in three places (gait, policy environment, firmware) from the same generated numbers.

From angles to servo pulses ​

Joint space and servo space differ only for the knee: hip and thigh servos hold q1 and q2 directly, the knee servo holds the crank angle ψ. The firmware then maps each actuator angle θ to a pulse width tp, with calibration sign s (direction), neutral_us tneutral and reference angle θ0 (zero_rad):

tp=tneutral+s⋅2000μsπrad⋅(θ−θ0)

with per-joint calibration from flash (firmware). At the assembly reference (all servos at 1500 µs) the legs are vertical in abduction, the femur is 45° backwards and the knee crank at 35°.

Gaits: stance, swing and the gait cycle ​

A gait repeats with a period T. Each leg spends a fraction of it on the ground (stance, pushing the body) and the rest in the air (swing, moving forward to its next foothold). The stance fraction is the duty factor β=Tstance/T. Phase offsets say when each leg starts its cycle. The theory behind gaits, support polygons and Raibert foot placement is in legged locomotion.

GaitDuty factorPhase patternCharacter
Walk≥ 0.75one leg at a time (e.g. FL, RR, FR, RL)at least three feet down: statically stable, slow
Trot0.5diagonal pairs together (FL+RR, then FR+RL)two feet down: needs momentum and a short period; robust and simple

Static stability: a robot standing still does not fall if its centre of mass projects inside the support polygon formed by the feet on the ground. With three feet down (walk) that triangle can be kept under the body. With two diagonal feet (trot) the polygon is a line, so the body is briefly unsupported in roll and pitch; a 0.4 s cycle switches pairs before it tips noticeably.

Strido implements a trot only (TrotGait, PHASE_OFFSET = {"FL": 0.0, "RR": 0.0, "FR": 0.5, "RL": 0.5}). A walk would need a new phase table and a duty factor ≥ 0.75; nothing in the repo provides one.

The trot in numbers ​

GaitConfig defaults:

ParameterValue
body_height0.17 m
period0.40 s (2.5 steps per second per leg pair)
duty0.5 (0.2 s stance, 0.2 s swing)
step_height0.035 m swing apex
max_linear0.30 m/s forward, 0.15 m/s sideways
max_yaw_rate1.0 rad/s

How foot_targets() places each foot:

  1. Per-leg velocity. For body command (vx,vy,ωz), a foot at (x,y) must move at vfoot=(vx−ωzy, vy+ωzx), the planar form of v+ω×r: turning on the spot makes the left feet move backward and the right feet forward.
  2. Stride = velocity × stance time, L=vβT. At 0.15 m/s: 0.15m/s×0.2s=0.03m. At the 0.30 m/s limit: 0.06 m.
  3. Stance (phase <β): the foot slides from +L/2 to −L/2 relative to its nominal position at body speed, so the body moves forward over it.
  4. Swing: the foot returns from −L/2 to +L/2 and lifts on a half-sine, h(s)=0.035m⋅sin⁡(πs) with swing progress s∈[0,1]. Landing half a stride ahead of the hip is the Raibert-style foot placement: the touchdown point is symmetric about the hip, so the stance phase neither accelerates nor brakes the body on average.
  5. Inverse kinematics turns the four foot targets into 12 angles, then clamp_leg applies the limits.

With a zero command the feet stay at their nominal positions: the robot stands still rather than trotting in place.

gait_node adds a ramp (velocity changes at most 0.5 m/s² and 0.5 rad/s²) and a smooth 3 s stand-up from the rest pose (0.07 m: thigh 69.4°, calf −148.1°) to the stand pose.

This gait is open-loop: it does not read the IMU and makes no balance corrections. It works because the trot is short-period and the robot is light (3.01 kg). The learned policy (simulation and RL) is the closed-loop alternative: it observes the IMU.

PD control: what holds a joint at its target ​

A position servo is a small feedback controller. The usual model is a PD (proportional-derivative) law:

τ=kp(qtarget−q)−kdq˙

The P term pulls towards the target like a spring; the D term damps motion like a shock absorber. Large kp gives stiff, accurate joints but can oscillate; kd suppresses the oscillation. For a joint with inertia I the closed loop is a mass–spring–damper, Iq¨+kdq˙+kpq=kpqtarget, with natural frequency ωn=kp/I and damping ratio ζ=kd/(2kpI); see control.

In Strido:

  • On the robot the PD loop lives inside each SPT5435LV: its electronics compare the commanded pulse with an internal potentiometer and drive the motor. The ESP32 sends only targets; nothing outside the servo knows the real angle or torque.
  • In MuJoCo each servo is a position actuator with kp=25Nm/rad, kv=0.4Nms/rad and a torque limit of ±3.43Nm, the SPT5435LV stall torque at 6 V. So 0.1 rad of error gives 25×0.1=2.5Nm; beyond 3.43/25≈0.14rad the servo saturates, as a real one would.
  • In Gazebo gz_ros2_control exposes a position command interface per joint, driven by leg_controller (position_controllers/JointGroupPositionController, 200 Hz).

These gains are modelling estimates; simulation notes they should be refitted to measured servo response once a robot exists.

Check yourself ​

  1. Why does moving only the thigh servo also change the knee angle?
  2. Using the law of cosines, what is cos⁡q3 for a foot at full extension (distance l1+l2)?
  3. What is the stride length at 0.20 m/s with the default trot?
  4. Why is a trot not statically stable, and why does it still work?
  5. In MuJoCo, what torque does a joint produce at 0.05 rad error and zero velocity?
Answers
  1. The knee servo drives the tibia through a parallelogram and four-bar attached to the femur; the knee angle depends on the crank angle and the thigh angle.
  2. (l1+l2)2−l12−l22=2l1l2, so cos⁡q3=1 and q3=0: the leg is straight (and the calf limit −45∘ forbids it).
  3. 0.20m/s×0.2s=0.04m.
  4. Only two diagonal feet support the body, so the centre of mass is not inside a support polygon; the short 0.4 s period switches pairs before the body tips far.
  5. 25Nm/rad×0.05rad=1.25Nm (below the 3.43 N·m limit).

Further reading ​

MIT licence. Mechanical design derived from Yertle by Jerome Graves. CAD, simulation and native tests are not hardware evidence — see Verification.