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++ mirrorfirmware/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 bygait_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.yamlandhardware/joint_limits.yaml, then generated intopackages/strido-core/src/strido/geometry.pyandgeometry_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:
| Joint | Axis | Zero | Positive direction | Actuated by |
|---|---|---|---|---|
| hip (q1) | +x (forward) | leg vertical | foot moves to +y (left) | hip servo, direct |
| thigh (q2) | +y | femur straight down | knee moves backwards | thigh servo, direct (coaxial with the femur pivot) |
| calf (q3) | +y, relative to the femur | calf in line with femur | negative = knee bent | knee servo, through two linkages |
Measured dimensions (front-left leg, generated values rounded):
| Quantity | Value |
|---|---|
Hip axis position in base_link | x = ±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 l1 | 0.130 m |
| Calf (tibia) length l2, to foot centre | 0.124 m |
| Foot sphere radius | 0.012 m |
Full extension is
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
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:
In the leg plane (
forward, up, origin at the thigh pivot): The calf's absolute angle is
because is measured relative to the femur: angles of a serial chain add. Place the plane point at lateral offset
thigh_offset_y, rotate it aboutby the hip angle , then add the hip position in base_link:. 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
Inverse kinematics
inverse_kinematics(name, foot) solves the reverse, analytically (no iteration):
Hip. Look at the leg from the front. The foot must lie on a circle of radius
( thigh_offset_y) around the hip axis plus a "depth"along the leg, by Pythagoras: . The hip angle is the rotation that aligns the leg plane with the foot: . Knee from the law of cosines. In the leg plane the foot is at
. The triangle thigh–calf–(pivot to foot) has sides , and , so , which gives (negative: the knee bends backwards). If
the target is out of reach and the function returns None.Thigh. The direction to the foot minus the angle the bent calf adds:
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):
| Joint | FL | FR | RL | RR |
|---|---|---|---|---|
| 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 direction), neutral_us zero_rad):
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
| Gait | Duty factor | Phase pattern | Character |
|---|---|---|---|
| Walk | ≥ 0.75 | one leg at a time (e.g. FL, RR, FR, RL) | at least three feet down: statically stable, slow |
| Trot | 0.5 | diagonal 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:
| Parameter | Value |
|---|---|
body_height | 0.17 m |
period | 0.40 s (2.5 steps per second per leg pair) |
duty | 0.5 (0.2 s stance, 0.2 s swing) |
step_height | 0.035 m swing apex |
max_linear | 0.30 m/s forward, 0.15 m/s sideways |
max_yaw_rate | 1.0 rad/s |
How foot_targets() places each foot:
- Per-leg velocity. For body command
, a foot at must move at , the planar form of : turning on the spot makes the left feet move backward and the right feet forward. - Stride = velocity × stance time,
. At 0.15 m/s: . At the 0.30 m/s limit: 0.06 m. - Stance (phase
): the foot slides from to relative to its nominal position at body speed, so the body moves forward over it. - Swing: the foot returns from
to and lifts on a half-sine, with swing progress . 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. - Inverse kinematics turns the four foot targets into 12 angles, then
clamp_legapplies 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:
The P term pulls towards the target like a spring; the D term damps motion like a shock absorber. Large
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
, and a torque limit of , the SPT5435LV stall torque at 6 V. So 0.1 rad of error gives ; beyond the servo saturates, as a real one would. - In Gazebo
gz_ros2_controlexposes apositioncommand interface per joint, driven byleg_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
- Why does moving only the thigh servo also change the knee angle?
- Using the law of cosines, what is
for a foot at full extension (distance )? - What is the stride length at 0.20 m/s with the default trot?
- Why is a trot not statically stable, and why does it still work?
- In MuJoCo, what torque does a joint produce at 0.05 rad error and zero velocity?
Answers
- 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.
, so and : the leg is straight (and the calf limit forbids it). . - 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.
(below the 3.43 N·m limit).