PID controller for motion stabilization
Whether it's a two-wheeled balancing robot, a hexapod leveling its body, or a drone holding altitude, most robotic stabilization problems reduce to the same task: drive an error signal to zero as fast as possible without overshooting or oscillating. The PID controller — proportional, integral, derivative — is the workhorse solution, tuned in a hundred different ways for a hundred different mechanisms.
The PID formula
Given a desired setpoint r(t) and a measured value
y(t), the error is e(t) = r(t) − y(t).
The continuous-time PID control law is:
u(t) is the control output — a motor torque, a thrust command, a servo angle — sent to the actuator. Kp, Ki, Kd are the three tuning gains that determine how aggressively the controller reacts.
Discrete-time implementation
Real controllers run at a fixed sample rate (e.g. 100–1000 Hz for a balancing robot), so the integral becomes a running sum and the derivative becomes a finite difference:
derivative = (e − e_prev) / dt
u = Kp·e + Ki·integral + Kd·derivative
e_prev = e
class PID {
constructor(kp, ki, kd) {
this.kp = kp; this.ki = ki; this.kd = kd;
this.integral = 0;
this.prevError = 0;
}
update(setpoint, measured, dt) {
const error = setpoint - measured;
this.integral += error * dt;
const derivative = (error - this.prevError) / dt;
this.prevError = error;
return this.kp * error + this.ki * this.integral + this.kd * derivative;
}
}
What each term does for stability
- P (Proportional) — reacts to the current error. Higher Kp means a stiffer, faster response, but too high causes overshoot and oscillation because the controller "punches" past the setpoint before it can react to the overshoot.
- I (Integral) — accumulates past error, eliminating steady-state error (e.g. a robot arm that settles slightly short of its target due to gravity or friction). Too much Ki causes slow oscillation as the accumulated integral overshoots and has to unwind.
- D (Derivative) — reacts to the rate of change of error, effectively damping the response before it overshoots. Essential for stabilizing anything with inertia (an inverted pendulum, a drone), but amplifies sensor noise.
Integral windup and anti-windup
If the actuator saturates (e.g. a motor is already at 100% power but the error persists), the integral term keeps accumulating — winding up — far beyond what's needed. When the error finally reverses sign, the controller keeps commanding full power in the wrong direction until the wound-up integral unwinds, causing a large overshoot.
if (|u| >= u_max AND sign(error) == sign(u)) → freeze integral accumulation
update(setpoint, measured, dt) {
const error = setpoint - measured;
const pTerm = this.kp * error;
const dTerm = this.kd * (error - this.prevError) / dt;
this.prevError = error;
// tentative output without adding new integral
let u = pTerm + this.ki * this.integral + dTerm;
const saturated = Math.abs(u) >= this.uMax;
const sameSign = Math.sign(u) === Math.sign(error);
// only integrate when NOT already saturated in the same direction
if (!(saturated && sameSign)) this.integral += error * dt;
u = pTerm + this.ki * this.integral + dTerm;
return Math.max(-this.uMax, Math.min(this.uMax, u));
}
Derivative kick and filtering
When the setpoint changes instantly (a step command), the
naïve derivative de/dt spikes momentarily because
the error itself jumps — this is called derivative
kick. The standard fix is to take the derivative of the
measurement instead of the error (the setpoint's jump
doesn't appear in the measured signal):
Because the derivative term also amplifies high-frequency sensor noise, real controllers pass it through a low-pass filter (a simple exponential moving average is often enough) before use.
Tuning: Ziegler-Nichols and manual tuning
The classic Ziegler-Nichols method: set Ki = Kd = 0, raise Kp until the system oscillates at a constant, sustained amplitude — call this the ultimate gain Ku and the oscillation period Tu. Then apply:
| Controller | Kp | Ki | Kd |
|---|---|---|---|
| P only | 0.50·Ku | — | — |
| PI | 0.45·Ku | 1.2·Kp / Tu | — |
| PID (classic) | 0.60·Ku | 2·Kp / Tu | Kp·Tu / 8 |
Ziegler-Nichols gives an aggressive starting point, often too oscillatory for delicate robotics tasks. In practice most engineers manually fine-tune from there: raise Kp until response is fast but slightly oscillatory, add Kd until oscillation damps out, then add a small Ki only if steady-state error remains.
Cascade control
Many robotic stabilization systems use nested PID loops rather than one flat controller. A balancing robot typically cascades: an outer loop controls tilt angle (setpoint = upright), whose output becomes the setpoint for an inner, faster loop controlling wheel velocity, whose output becomes motor current. The inner loop runs at a higher frequency and rejects disturbances (motor friction, uneven floor) before they ever reach the slower, more strategic outer loop.
Inner loop (velocity, ~500 Hz): torque = PID_velocity(ω_target − ω_measured)
Example: inverted pendulum balance
A cart-mounted inverted pendulum (the classic "self-balancing robot" test case) uses PID on the tilt angle to compute the cart's acceleration command:
const pid = new PID(60, 2, 8); // Kp, Ki, Kd tuned for this pendulum
function controlLoop(dt) {
const theta = pendulum.getTiltAngle(); // radians from vertical
const accelCmd = pid.update(0, theta, dt); // setpoint = 0 (upright)
cart.applyAcceleration(accelCmd);
pendulum.step(dt);
}
Because the pendulum has real inertia, the derivative term is what actually keeps it from swinging past vertical — pure P control on an inverted pendulum oscillates with growing amplitude until it falls; adding D provides the damping needed for a stable equilibrium.
🦾 Explore Robot Arm Kinematics
See feedback control concepts in action alongside forward and inverse kinematics.