Skip to content
All articles

How to Tune PID on an FRC Robot: A Practical Guide

8 min read·

Your arm slams past the setpoint and bounces. Your flywheel never quite reaches target RPM. Your drivetrain wobbles down the path like it had too much coffee. Almost every one of these is a tuning problem, not a code problem. The good news: you do not need control-theory math to fix them. You need a method, a feel for what each knob does, and the discipline to change one thing at a time. This guide gives you all three, grounded entirely in the official WPILib docs.

If you have not read the theory yet, skim our PID overview on the Programming track first. This article is about the practical part: turning a twitchy mechanism into one that hits its target and holds.

A 30-second refresher on P, I, and D

A PID controller drives an error (the difference between where you are and where you want to be, the setpoint) to zero by combining three terms. In WPILib these live in the PIDController class, and you read its output every loop with calculate().

TermGainWhat WPILib says it does
ProportionalkPPushes the output toward the reference, proportional to current error. Acts like a "software-defined spring."
IntegralkISums all past error to kill leftover steady-state error, the small gap P alone cannot close.
DerivativekDResponds to how fast the error is changing. Acts like a "software-defined damper" that slows the system as it approaches.

That spring-and-damper picture is the whole intuition. P yanks you toward the target. D pumps the brakes so you do not blow past it. I nudges away the last stubborn sliver of error. Source: WPILib's Introduction to PID.

The one rule: change one gain at a time

Before any numbers, internalize this. Tuning is a search, and if you move two knobs at once you can never tell which one helped. Set the others to zero, move one gain, watch the mechanism (a real-time plot of position or velocity vs. setpoint is gold), then move the next. Every procedure below follows this rule.

A safe tuning order

WPILib's tuning walkthroughs are consistent: feedforward first, then P, then D, then I sparingly. Here is why that order is safe.

  1. Feedforward does most of the work, so the PID has less to clean up.
  2. P gives you responsiveness once feedforward has it close.
  3. D tames the overshoot that aggressive P creates.
  4. I is a last resort for stubborn steady-state error, because it is the easiest term to misuse.

Notice I is last and smallest. WPILib is blunt about this: "integral gain is generally not recommended for FRC use." We will come back to why.

Feedforward: the part beginners skip (and shouldn't)

PID is reactive: it only acts after error appears. Feedforward is predictive: it computes the voltage a mechanism should need before any error shows up. For velocity control especially, this is not optional. As WPILib notes, a permanent-magnet DC motor's steady-state velocity is roughly proportional to applied voltage, so a good feedforward gets a flywheel almost exactly to speed on its own, leaving PID to trim small disturbances.

WPILib provides three feedforward classes. Each gain is a real, physical voltage, all per WPILib's feedforward docs:

GainPhysical meaning
kSVolts to overcome static friction, just barely get it moving.
kVVolts to hold a constant velocity (fights back-EMF and speed-dependent friction).
kAVolts to produce a given acceleration.
kGVolts to fight gravity (arms and elevators only).

The classes and their models:

  • SimpleMotorFeedforward(kS, kV, kA) for flywheels and drivetrains: V = kS·sgn(v) + kV·v + kA·a
  • ElevatorFeedforward(kS, kG, kV, kA): adds a constant kG because gravity always pulls down.
  • ArmFeedforward(kS, kG, kV, kA): gravity varies with angle, so the term is kG·cos(θ), biggest when the arm is horizontal.

Note the argument order: the two gravity-aware classes take kG second, right after kS. Get the order wrong and your numbers go to the wrong terms. In code you add feedforward to PID each loop: motor.setVoltage(feedforward.calculate(targetVelocity) + pid.calculate(measuredVelocity, targetVelocity)).

WPILib SysId: stop guessing your feedforward

You can find kS, kV, and kA by hand, but the WPILib System Identification Tool (SysId) measures them for you. It is the right starting point for any serious mechanism. SysId works in two parts: robot-side code runs your motor through a set of tests and logs voltage, position, and velocity; then the desktop app fits a model to that data. See WPILib's SysId introduction. It supports simple motors, elevators, and arms.

You define a SysIdRoutine with two objects (per creating a routine):

  • A Config that sets the quasistatic ramp rate (default 1 V/s) and the dynamic step voltage (default 7 V).
  • A Mechanism with a voltage consumer (passes voltage to your motor controllers) and a log consumer (records the sensors).

It runs four tests, forward and reverse for each:

  • Quasistatic: voltage ramps up slowly so acceleration is negligible. This isolates kS and kV.
  • Dynamic: a constant step voltage is applied to capture acceleration behavior, giving kA.

Give SysId plenty of clear runway, because the mechanism will run on its own. The app then spits out kS, kV, kA (and kG for arms/elevators), plus diagnostic plots and even suggested feedback gains. Drop those numbers straight into your feedforward constructor.

Symptoms and fixes

This is the table to keep open during practice. Match what you see to the cause.

SymptomLikely causeFix
Fast oscillation / buzzing around setpointkP too highLower kP until oscillation stops.
Sluggish, slow to reach setpointkP too lowRaise kP.
Big overshoot then settlesNot enough dampingAdd kD.
Settles just short of target foreverSteady-state errorFirst add/fix feedforward; only then a tiny kI.
Slowly grows worse / "winds up" and lurchesIntegral windupReduce kI; use setIZone() or setIntegratorRange().
Velocity setpoint never quite reachedMissing or low kVTune feedforward kV.

A few of these deserve more than a row.

Oscillation = too much P

Crank kP and the mechanism becomes a buzzing, overshooting mess. WPILib's procedure: raise kP until oscillation just appears, then back it off until it stops. That edge is roughly your sweet spot for P.

Overshoot = needs D

Once P is responsive, you will often see it sail past the target before settling. kD is the damper: it pushes back the faster the error is closing, smoothing the approach. Add it gradually. One caveat from WPILib: for velocity control with a constant setpoint, kD is not useful (it is only needed when the setpoint is changing), so skip it on a flywheel.

Steady-state error = feedforward first, then maybe I

If the mechanism parks slightly short of target and stays there, the textbook answer is "add I." In FRC the better first answer is "fix your feedforward." A correct kV (and kG on an arm or elevator) usually eliminates the gap with no integral at all. Only if a residual error remains should you add a small kI. WPILib's guidance across its arm and flywheel walkthroughs is the same: increase integral gain only when the output gets "stuck" before converging to the setpoint.

The reason for caution is integral windup: while error persists, the integral term keeps accumulating, and if the mechanism was stalled or saturated it can build up a huge correction that overshoots wildly once it frees. WPILib's PIDController gives you two guards: setIZone() (ignore the integral unless error is small) and setIntegratorRange() (cap how much the integral can contribute). Use them whenever you use kI at all.

Position vs. velocity loops

The two big mechanism types tune differently:

  • Position loops (an arm to an angle, an elevator to a height) care about where you end up. They use the full P + D, lean on kG for gravity, and benefit from setTolerance() plus atSetpoint() to know when you have arrived. For rotating mechanisms, enableContinuousInput() lets the controller wrap angles correctly.
  • Velocity loops (a flywheel, drive wheels at a target speed) care about a steady speed. Here kV does the heavy lifting, kP trims disturbances, kD is skipped, and kI is rarely needed.

Worked intuition for three mechanisms

Flywheel (velocity). Run SysId, get kS/kV, drop them into SimpleMotorFeedforward. WPILib's flywheel order: raise kV until the wheel approaches target over time (reduce it if it overshoots), then add kP until it oscillates and back off, add a touch of kI only if it sticks below target. No kD.

Arm (position). Use ArmFeedforward and tune kG first: increase it until the arm holds its angle against gravity with almost no motor effort. Then raise kV so it tracks slow, smooth motions. Now add kP until it responds sharply to a setpoint change, kD to smooth the approach and cut overshoot, and kI only if it stops short. This is straight from WPILib's vertical arm tuning guide.

Drivetrain. Each side is a velocity loop, so treat it like the flywheel: characterize with SysId for kS/kV/kA, run SimpleMotorFeedforward, then trim with a modest kP. Accurate drive feedforward is also what makes WPILib trajectory following and tools like PathPlanner track cleanly, because the path planner already knows the velocities to command. Get this right and your autonomous gets dramatically more repeatable.

Putting it together

Tuning feels like dark art until you have a recipe. Yours is now: measure feedforward with SysId, add kP for response, kD for damping (not on velocity loops), and kI only as a guarded last resort. Change one gain at a time, watch a live plot, and let the symptom table tell you which knob to touch. Most FRC mechanisms tune in well under an hour once you stop guessing.

Ready to wire this into real subsystem code? Head to the LearnFRC Programming track to build your first feedforward-plus-PID mechanism end to end.

Keep reading

Learn every department of FRC — free

393+ structured lessons, quizzes, and team tools. Built by an FRC student, for the community.

Browse the guides