Robotics · Control Theory
📅 July 2026 ⏱ ≈ 12 min read 🎯 Intermediate · Last updated: 9 July 2026

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.

TL;DR: This article walks through the PID control law in continuous and discrete form, why an unchecked integral term "winds up" during actuator saturation and needs anti-windup clamping, how derivative kick is avoided by differentiating the measurement instead of the error, Ziegler-Nichols tuning, nested (cascade) control loops, and a JavaScript inverted-pendulum balancing example.

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) = Kp·e(t) + Ki·∫₀ᵗ e(τ)dτ + Kd·de(t)/dt

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:

integral += e · dt
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

Damping ratio intuition: increasing Kd is like adding a shock absorber; increasing Kp is like adding a stiffer spring. A stable, responsive stabilizer needs the right balance of both — too stiff with no damping oscillates forever, too much damping with no stiffness never returns to setpoint quickly.

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.

Clamping anti-windup: stop integrating once output saturates
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):

derivative = −(y − y_prev) / dt // derivative on measurement, negated

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:

ControllerKpKiKd
P only0.50·Ku
PI0.45·Ku1.2·Kp / Tu
PID (classic)0.60·Ku2·Kp / TuKp·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.

Don't deliberately induce sustained oscillation on real hardware near mechanical limits — for anything with real torque (a robot arm, a drone), simulate the tuning process first, or use a safer method like relay feedback / step-response tuning.

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.

Outer loop (angle, ~50 Hz): ω_target = PID_angle(0 − θ)
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.

Open simulation →