Lesson 4 of 5 · 24 min
Gaits and timing
With inverse kinematics you can place one foot anywhere. Walking is the art of choosing where each of the four feet is at every instant, so that the body keeps moving forward without falling. That choice is called a gait: a repeating pattern in which each foot alternates between stance (on the ground, pushing the body) and swing (in the air, moving forward to the next landing spot).
Describing a gait with two numbers
Any periodic gait can be described by a few quantities:
- Period
T: the time of one full cycle for one foot. - Duty factor
beta: the fraction of the cycle a foot spends on the ground,beta = t_stance / T. - Phase offset of each leg: how far into the cycle that leg is compared to a reference leg, as a fraction from 0 to 1.
The duty factor tells you how many feet are down on average: 4 x beta. At beta = 0.75 it is 3 feet, at beta = 0.5 it is 2 feet.
The three gaits to build
| Gait | Duty factor | Front left | Front right | Rear left | Rear right | Feet down |
|---|---|---|---|---|---|---|
| Walk (crawl) | 0.75 | 0.00 | 0.50 | 0.75 | 0.25 | 3 always |
| Trot | 0.55 | 0.00 | 0.50 | 0.50 | 0.00 | 2 (sometimes 4) |
| Bound | 0.40 | 0.00 | 0.00 | 0.50 | 0.50 | 2 front or 2 rear, with a flight phase |
Read the table row by row. In the walk, the feet land one after another around the body: front left, rear right, front right, rear left, each a quarter of a cycle apart. In the trot, diagonal pairs move together. In the bound, the two front legs act as one, then the two rear legs.
The duty factor and speed are linked. The foot must travel one stride length S backwards relative to the body during stance, so the body speed is:
v = S / t_stance = S / (beta x T)
Worked example with S = 40 mm:
- Walk,
T = 1.2 s,beta = 0.75:t_stance = 0.9 s, sov = 40 / 0.9 = 44 mm/s. - Trot,
T = 0.6 s,beta = 0.5:t_stance = 0.3 s, sov = 40 / 0.3 = 133 mm/s.
The trot is three times faster for the same stride. The swing foot in the trot has 0.5 x 0.6 = 0.3 s to lift and land. At a 50 Hz control tick, that is 15 updates for the swing, enough for a smooth arc.
Static stability and the support polygon
Place the feet at the corners of a rectangle 200 mm long and 120 mm wide, with the centre of mass above the middle. Use coordinates (x forward, y left) in millimetres:
- Front left
(100, 60), front right(100, -60) - Rear left
(-100, 60), rear right(-100, -60)
In a walk, lift the front left foot. The remaining three feet form a triangle: front right, rear left, rear right. One side of that triangle is the line from rear left to front right, and it runs exactly through the origin. The centre of mass projected on the floor sits right on the edge, with zero stability margin.
The cure is to shift the body into the triangle before the lift. The stability margin is the perpendicular distance from the centre of mass to the nearest edge. Moving the body 25 mm toward the rear right foot, away from the lifted leg, gives a margin of about 25 mm, a comfortable amount on a robot this size.
The trot has no support polygon at its core: two diagonal feet make only a line, and the robot relies on forward momentum and a short stance time to stay up. That is why a duty factor slightly above 0.5 helps. A value of 0.55 means all four feet are briefly down together, 0.05 x 0.6 s = 30 ms per transition, giving a safety moment at each swap.
Foot trajectory
A foot path needs a stance part (straight back along the ground) and a swing part (an arc forward). Use a half sine for the lift so the foot leaves and lands with zero vertical speed:
const float STRIDE = 40.0; // stride length, mm
const float LIFT = 25.0; // swing height, mm
const float HEIGHT = 120.0; // standing height, mm
// phase is 0..1, duty is the stance fraction
void footTarget(float phase, float duty, float &x, float &z) {
if (phase < duty) { // stance: foot slides backward
float u = phase / duty;
x = STRIDE / 2 - STRIDE * u;
z = HEIGHT;
} else { // swing: foot arcs forward
float u = (phase - duty) / (1.0 - duty);
x = -STRIDE / 2 + STRIDE * u;
z = HEIGHT - LIFT * sin(PI * u);
}
}
Non-blocking timing with millis()
You could write the gait with delay(), but then nothing else can run during a step: no IMU reads, no serial commands, no sensors. The fix is the pattern from the Arduino course: compare millis() with a stored time, and only act when a tick has passed.
const unsigned long TICK_MS = 20; // 50 Hz control loop
unsigned long nextTick = 0;
unsigned long gaitStart = 0;
float periodMs = 600.0; // one gait cycle, trot
float duty = 0.55;
// FL FR RL RR
float offset[4] = {0.0, 0.5, 0.5, 0.0};
void setup() {
// ... pwm.begin() etc. from lesson 2
gaitStart = millis();
nextTick = gaitStart;
}
void loop() {
unsigned long now = millis();
if ((long)(now - nextTick) >= 0) { // safe across millis() rollover
nextTick += TICK_MS; // no drift: add, do not reset
float t = fmod((now - gaitStart) / periodMs, 1.0);
for (int leg = 0; leg < 4; leg++) {
float phase = fmod(t + offset[leg], 1.0);
float x, z;
footTarget(phase, duty, x, z);
LegAngles a = legIK(x, 0.0, z);
writeLeg(leg, a); // applies sign, offset, PCA9685
}
}
// Other tasks run here every pass: read IMU, parse serial, etc.
}
Two details matter. The nextTick += TICK_MS line adds a fixed step instead of using now, so timing errors do not accumulate. And the subtraction cast to long stays correct when millis() wraps around after about 49.7 days.
Check yourself
A trot has stride length 50 mm, period 0.8 s and duty factor 0.5. What is the body speed?
Check yourself
Why does a crawl walk with the body centred over a rectangle of feet still tip when a leg lifts?