PID tuning for robot motion: a practical method

Tune P, I and D in the right order, read a step response instead of guessing, add anti-windup and feedforward, and know when the mechanics are the real problem.

Reference
RE-1410
Published
Read
6 min

About the author · Sources, testing and AI policy

Laptop showing a step response plot beside a single-joint servo test rig with a weight

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.

TermPhysical meaningToo muchToo little
PStiffness: a virtual spring to the targetOscillation at the natural frequencySluggish, steady-state error under load
IRemoves persistent offsetOvershoot, windup, slow limit cyclesDroop, never quite reaches the target
DDamping: resists velocity errorAmplifies encoder noise, buzzingOvershoot, 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 seeDiagnosisAction
Slow rise, no overshootNot enough stiffnessRaise P
Overshoot then ringingUnderdampedRaise D or lower P
Constant offset at restProportional droop, frictionAdd feedforward, then a little I
High frequency buzz at standstillD on noisy velocityFilter the derivative, lower D
Slow oscillation around the targetIntegral windupClamp the integral, add anti-windup
Saturates then lagsOutput limit reachedCheck current limit and gearing
Different behaviour loaded vs unloadedMechanical compliance or weak driveFix 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

Pass it onWhatsAppLinkedIn

Keep reading

Continue learning

The Build Note

One short email when something is worth your bench time.

A new build sheet, a part price that moved, or a mistake worth avoiding. No daily drip, no recycled news.

One short email when a build or a price is worth your time. Unsubscribe in one click.