Most robot motion problems are not PID problems. They are mechanical compliance, encoder noise or a missing feedforward term, and no amount of gain tuning will fix them.
This guide explains what each term actually does, the order to tune in, how to read a step response instead of guessing, and the two implementation details that separate a controller that works from one that oscillates on every power-up.
What the three terms do
PID is a statement about error. Proportional output pushes harder the further you are from the target. Integral output accumulates the error that proportional cannot remove. Derivative output resists fast change, which damps the overshoot the other two create.
| Term | Physical meaning | Too much | Too little |
|---|---|---|---|
| P | Stiffness: a virtual spring to the target | Oscillation at the natural frequency | Sluggish, steady-state error under load |
| I | Removes persistent offset | Overshoot, windup, slow limit cycles | Droop, never quite reaches the target |
| D | Damping: resists velocity error | Amplifies encoder noise, buzzing | Overshoot, ringing after a move |
Think of a robot joint as a mass on a spring. P sets the spring stiffness, D is the damper, and I slowly winds the spring anchor until the mass sits exactly on target. Increase stiffness and you get a faster response and more ringing; that trade is physical, not a software setting.
Tune the loops in the right order
Industrial drives run three nested loops: current inside velocity inside position. You cannot tune the outer loop while the inner loop is unstable, and you cannot tune velocity while current limiting is engaging constantly.
- Current loop: usually configured by the drive from motor parameters; verify it does not trip at rated load.
- Velocity loop: tune for a clean step response with no overshoot beyond a few percent.
- Position loop: tune last, with velocity feedforward from the trajectory.
- Each outer loop should be at least five times slower than the loop inside it.
On a hobby setup where the driver has no current loop, start with velocity and accept that current is whatever the motor draws. That is also why a hobby robot with heavy payloads benefits more from a better driver than from better gains.
Read a step response, do not guess
Command a small step, log target and actual at a fixed rate, and look at the shape. A step of 5 degrees on a joint is enough to reveal everything. Do this before changing a single gain.
| What you see | Diagnosis | Action |
|---|---|---|
| Slow rise, no overshoot | Not enough stiffness | Raise P |
| Overshoot then ringing | Underdamped | Raise D or lower P |
| Constant offset at rest | Proportional droop, friction | Add feedforward, then a little I |
| High frequency buzz at standstill | D on noisy velocity | Filter the derivative, lower D |
| Slow oscillation around the target | Integral windup | Clamp the integral, add anti-windup |
| Saturates then lags | Output limit reached | Check current limit and gearing |
| Different behaviour loaded vs unloaded | Mechanical compliance or weak drive | Fix mechanics before gains |
Measure rise time, overshoot and settling time from the log, not from feel. A number you can compare across changes is worth more than an opinion about how it sounded.
Implementation details that decide stability
Two implementation choices cause most real-world instability. Derivative on error, which produces a huge kick when the setpoint steps, and an integral term with no limit, which winds up during every long move.
class PID:
def __init__(self, kp, ki, kd, out_min, out_max):
self.kp, self.ki, self.kd = kp, ki, kd
self.out_min, self.out_max = out_min, out_max
self.i_term = 0.0
self.prev_meas = None
def update(self, setpoint, meas, dt):
error = setpoint - meas
# derivative on measurement, not on error
d_term = 0.0
if self.prev_meas is not None and dt > 0:
d_term = -self.kd * (meas - self.prev_meas) / dt
self.prev_meas = meas
p_term = self.kp * error
# clamp the integral before adding, not after
i_next = self.i_term + self.ki * error * dt
i_next = max(self.out_min, min(self.out_max, i_next))
out = p_term + i_next + d_term
out = max(self.out_min, min(self.out_max, out))
# stop integrating when the output is saturated in the same direction
if not (out == self.out_max and error > 0) and not (out == self.out_min and error < 0):
self.i_term = i_next
return out- Run the controller at a fixed period; variable dt changes the meaning of ki and kd.
- Filter the derivative term with a first-order filter, typically 10 to 50 Hz, rather than filtering everything.
- Reset the integral on enable, and when the axis is outside a sane position band.
- Clamp the output to what the drive can actually deliver, including current limits.
- Log setpoint, measurement, output and saturation flag.
Feedforward does most of the work
On an arm, gravity holds the joint off target every time. Adding a gravity feedforward term, sized from the link mass and geometry, removes the steady error before the integral term has to fight it. On a mobile robot, a velocity feedforward proportional to the commanded speed does the same for tracking lag.
# trajectory-style control: feedforward plus feedback
u = gravity_term(angle) + kp * (target - actual) + kd * (0 - velocity)With feedforward, the feedback gains only correct the remainder. That means lower gains, less overshoot and less heat in the motor. If your gains are enormous, the missing term is usually feedforward.
A tuning sequence that works on real hardware
- Disable I and D. Raise P until the response is quick but just starts to ring.
- Back P off by about 30 percent. Add D until the ringing is gone.
- Add feedforward for gravity or velocity, then re-check the step.
- Add I only if a steady error remains, with a limit at a fraction of the output range.
- Test with the real payload, at the real speed, on the real surface.
- Repeat at the extremes of travel, because friction and gravity vary along the path.
Tuning on a bench with no load produces numbers that are wrong in both directions: too aggressive for a heavy payload, too slow for an empty one. The final gains belong on the machine, with the parts it will actually handle.
When PID is the wrong tool
If the mechanism has backlash, a flexible coupling or a frame that twists, the controller is chasing a resonance it cannot damp. If the encoder resolution is coarser than the accuracy you want, gains cannot create information. If the drive saturates for most of the move, the limit is electrical.
These are the cases where a mechanical fix beats an afternoon of gain tuning, and recognizing them early saves the afternoon.
Related: PID control for line followers, servo jitter fixes, differential drive odometry, servo drive selection, wheel encoder guide.
In what order should I tune P, I and D?
P first for speed, then D for damping, then I only if a steady error remains. Tuning I first hides the problem and makes windup more likely.
Why does my motor buzz when I increase D?
The derivative term amplifies encoder or velocity noise. Filter the measurement, compute the derivative on the measurement rather than the error, and back off on D.
Is a PID enough for a robot arm joint?
Usually not on its own. Gravity compensation and inertia feedforward do most of the work, and PID corrects the remainder. Without feedforward, the integral term fights gravity continuously.
How often should the control loop run?
At least ten times faster than the fastest dynamic you need to control. Small joints often run at 1 kHz, mobile robot velocity loops at 50 to 200 Hz, and position loops slower still.
What is integral windup?
The integral term keeps accumulating while the output is saturated, so it overshoots badly when the error finally reverses. Clamp the integral and stop integrating while saturated in the same direction.
A flexible chassis makes PID tuning impossible, which is why stiffness comes first.
Sources and further reading
- WPILib advanced controls — practical PID and feedforward for robots.
- ros2_control documentation — controller structure and hardware interfaces.
- MathWorks Control System Toolbox — step response and stability references.


