System Identification with SysId
Every gain value in this unit — kS, kV, kA, kP — was produced by someone. Either they guessed and iterated, or they measured. SysId lets you measure, and measurement wins matches.
By the end of this lesson, you will:
- Explain what system identification is and why empirically derived gains outperform manually tuned ones
- Describe the physics model SysId fits to the data and what kS, kV, and kA represent in that model
- Add a
SysIdRoutineto a subsystem and bind its test commands to controller buttons for data collection - Run both the quasistatic and dynamic tests correctly, export the log file, and open it in WPILib's analysis tool
- Identify the signs of clean vs. contaminated data and know which physical conditions cause bad results
- Place the analyzed gains into
Constants.javaand connect them to the appropriate feedforward class
Why Measurement Beats Guessing
Across the previous lessons in Unit 8, you've built feedforward controllers, PID controllers, motion profiles, and Motion Magic configurations. Every single one of those tools has constants at its core — kS, kV, kA, kP. And at some point, someone has to put real numbers into those constants.
The way most teams first encounter this is by guessing. Start at 0.1, deploy, watch the mechanism, try 0.2. It can eventually work, but the result is rarely optimal, takes a long time under build-season pressure, and has no guarantee it reflects the mechanism's actual physics. When the mechanism gets heavier in week 3 because the mechanical team added a brace, the guesses stop being valid.
System identification — SysId — takes a different approach. Instead of asking "what gain produces behavior I like," it asks "what physical model best explains the data this mechanism actually produced." The mechanism runs controlled test patterns, the data is captured, and a regression finds the gains that minimize the error between the model and reality. The result is a set of constants grounded in the mechanism's true physics.
Teams like 2910 and 254 characterize every motorized mechanism that needs closed-loop control. The payoff is compounding: a characterized drivetrain lets PathPlanner produce accurate autonomous trajectories. A characterized shooter wheel lets the feedforward predict the exact voltage for 4000 RPM without needing PID to compensate for model error. A characterized arm lets Motion Magic or ProfiledPIDController follow the profile accurately instead of fighting it. Each mechanism you characterize makes the rest of the control stack more reliable.
The Physics Model SysId Fits
SysId fits data to a specific motor model called the DC motor model. It describes the voltage required to produce a given velocity and acceleration:
V = kS·sgn(v) + kV·v + kA·a
Everything SysId does traces back to this equation. The tests it runs, the data it collects, and the regression it performs are all in service of finding the values of kS, kV, and kA that best fit your real mechanism's behavior.
kS is applied in the direction of motion as a fixed offset whenever the mechanism is commanded to move. It represents all the static resistive forces — bearing friction, gearbox stiction, belt tension — that must be overcome before the system moves at all.
In the quasistatic test, kS shows up as the initial voltage intercept before velocity develops. A high kS means your mechanism has significant static friction; a low kS means it moves easily. A kS that's much higher than kV × typical velocity suggests something is mechanically wrong — excessive preload, misaligned bearings, or a too-tight belt.
The DC motor model fitted by SysId does not include a gravity compensation term. For arms and elevators, kG must be determined separately: for an elevator, it's the voltage needed to hold a static position against gravity (measured by slowly increasing voltage until the elevator just barely doesn't drop). For an arm, it's the voltage needed at horizontal. WPILib's ArmFeedforward and ElevatorFeedforward can absorb kG in addition to the kS/kV/kA values SysId provides.
The SysId Process: Four Steps
Click each step to understand what you're doing and why.
SysId in Phoenix 6 / modern WPILib does not require a separate robot project. You add a SysIdRoutine object directly to your subsystem and expose its four test commands (quasistatic forward, quasistatic reverse, dynamic forward, dynamic reverse) for binding to controller buttons in RobotContainer.
The routine takes three things: a Config (ramp rate, step voltage, test timeout), a Mechanism (a lambda that applies voltage and a lambda that logs state), and a log-name string. The mechanism lambda is how you connect the routine to your actual motor — call setVoltage() on your motor object here, not a closed-loop command.
- Use
setVoltage()directly — notsetControl(new VoltageOut(...))— so the routine has raw voltage authority during testing - Log position (meters or rotations), velocity (m/s or rot/s), and applied voltage — all three are required for regression
- Use
Units.rotationsToRadians()if your feedforward class expects radians
Adding SysIdRoutine to a Subsystem
The following shows a drive subsystem with SysId wired in. The pattern applies equally to any mechanism — arm, elevator, shooter — with the mechanism lambda adapted to your motor and encoder types.
import edu.wpi.first.units.measure.*; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import static edu.wpi.first.units.Units.*; public class DriveSubsystem extends SubsystemBase { private final TalonFX m_leftLeader = new TalonFX(1); private final TalonFX m_rightLeader = new TalonFX(2); private final SysIdRoutine m_sysIdRoutine = new SysIdRoutine( new SysIdRoutine.Config( Volts.of(0.5).per(Second), // quasistatic ramp rate: 0.5 V/s Volts.of(3.0), // dynamic step voltage: 3 V Seconds.of(10), // test timeout: 10 s null // use default state consumer (logs to DS) ), new SysIdRoutine.Mechanism( // Drive mechanism: apply voltage to both sides symmetrically (voltage) -> { m_leftLeader.setVoltage(voltage.in(Volts)); m_rightLeader.setVoltage(voltage.in(Volts)); }, // Log mechanism state each loop for regression (log) -> { log.motor("drive-left") .voltage(Volts.of(m_leftLeader.getMotorVoltage().getValueAsDouble())) .linearPosition(Meters.of(getLeftPositionMeters())) .linearVelocity(MetersPerSecond.of(getLeftVelocityMetersPerSecond())); log.motor("drive-right") .voltage(Volts.of(m_rightLeader.getMotorVoltage().getValueAsDouble())) .linearPosition(Meters.of(getRightPositionMeters())) .linearVelocity(MetersPerSecond.of(getRightVelocityMetersPerSecond())); }, this ) ); // Expose the four test commands for binding in RobotContainer public Command sysIdQuasistaticForward() { return m_sysIdRoutine.quasistatic(SysIdRoutine.Direction.kForward); } public Command sysIdQuasistaticReverse() { return m_sysIdRoutine.quasistatic(SysIdRoutine.Direction.kReverse); } public Command sysIdDynamicForward() { return m_sysIdRoutine.dynamic(SysIdRoutine.Direction.kForward); } public Command sysIdDynamicReverse() { return m_sysIdRoutine.dynamic(SysIdRoutine.Direction.kReverse); } }
// In configureButtonBindings() — use a separate controller or chord // so these cannot be triggered accidentally during a match. // Quasistatic tests: hold button, mechanism ramps slowly. Release when done. m_operatorController.a().whileTrue(m_drive.sysIdQuasistaticForward()); m_operatorController.b().whileTrue(m_drive.sysIdQuasistaticReverse()); // Dynamic tests: press button, mechanism steps to full voltage. Release quickly. m_operatorController.x().whileTrue(m_drive.sysIdDynamicForward()); m_operatorController.y().whileTrue(m_drive.sysIdDynamicReverse()); // Safety note: wrap in a condition that only allows SysId bindings // when a "characterization mode" toggle is active, to prevent // accidental triggering during normal operation.
During a SysId test, the SysIdRoutine applies voltage directly to your motors, bypassing soft limits, current limits, and any command-level safety logic you've written. The mechanism will move as far as the test allows — potentially into its hard stops if you're not careful. Before running tests: verify the mechanism has room to move in both directions, have someone ready to disable on the Driver Station, and set the test timeout conservatively. Start with a step voltage of 3–4V, not 7V, until you've confirmed the mechanism behaves as expected.
The Two Test Types: What They Measure
SysId collects data via two fundamentally different test patterns. Each one is designed to isolate different physical properties of the mechanism. Running only one of them produces incomplete data and poor regression results.
Quasistatic Test
The quasistatic test ramps voltage very slowly — typically 0.5 V/s — from zero upward. "Quasistatic" means "approximately static" — the mechanism moves, but so slowly that it's essentially never accelerating. This nearly-zero acceleration means the kA term drops out of the model equation, leaving:
V ≈ kS·sgn(v) + kV·v
By plotting voltage against velocity during this test, the analyzer can fit a line: the intercept is kS (voltage at near-zero velocity) and the slope is kV (voltage per unit velocity). You run it in both forward and reverse directions to ensure the model is symmetric — if it isn't, you've discovered a mechanical asymmetry worth investigating.
Dynamic Test
The dynamic test applies a large voltage step — typically 3–7V — instantly from zero. This creates a rapid acceleration phase before the mechanism approaches free-speed velocity. During the acceleration phase, the kA term dominates: the model needs significant voltage above what kS + kV·v would predict to produce that acceleration. By comparing the measured acceleration against the "unexplained" voltage (voltage minus the quasistatic prediction), the analyzer isolates kA.
Running the dynamic test too long lets the mechanism overshoot its physical limits or reach free speed, where acceleration collapses to zero and kA becomes unobservable. Release the button well before the mechanism runs out of travel.
That's four total test runs: quasistatic forward, quasistatic reverse, dynamic forward, dynamic reverse. The analyzer uses all four together for the regression. Skipping the reverse runs produces gains that only model the forward direction accurately, and for mechanisms with gravity (arms, elevators) the two directions are physically very different. Teams sometimes skip the reverse runs to save time — this is a false economy that produces characterization data you can't fully trust.
Reading the Data: Clean vs. Contaminated
After collecting all four test runs, you export the .wpilog file from the roboRIO (via the Driver Station log folder or AdvantageScope) and open it in WPILib's SysId analysis tool. Before reading the gain outputs, always inspect the raw data plots. Good regression requires clean data.
Voltage vs. velocity forms a straight line with low scatter. The forward and reverse runs are near-symmetric. R² value is above 0.9 in the analyzer. The mechanism moved smoothly with no stops or jerks.
Velocity ramps up steadily from zero before leveling off at near free speed. Acceleration is relatively constant during the rising portion. The test was stopped before the mechanism ran out of travel.
Velocity data shows irregular jumps or drops that don't correspond to actual mechanism movement. The voltage-velocity plot has large scatter and looks like a cloud instead of a line. Check encoder mounting — the encoder is moving relative to the shaft it's measuring.
Voltage data suddenly drops mid-test, then recovers. Battery voltage sagged below the brownout threshold, causing the roboRIO to briefly stop commanding motors. Re-run with a fully charged battery. SysId tests are current-intensive — a weak battery produces unreliable kA values.
Velocity suddenly drops to zero mid-test while voltage remains high. The data after the impact is contaminated — velocity no longer represents free mechanism movement. You need to trim the data end point in the analyzer, or re-run the test with more conservative travel limits.
Timestamps in the log show gaps larger than 20 ms. The regression assumes uniform sample timing; large gaps produce incorrect kA values since acceleration is computed from finite differences of velocity. Fix roboRIO loop budget issues before characterizing, or note that kA results may be unreliable.
Reading the Analyzer Output and Placing Gains
When the data is clean and the regression runs, the SysId analyzer produces a set of gains and suggested feedback gains. Here's what a typical output looks like for a drivetrain, and where each number goes in your code:
public static final class DriveConstants { // ── SysId characterization output ───────────────────────────────────── // Run SysId on the actual competition robot with a fresh battery. // Re-run if drivetrain gearing, wheels, or carpet contact changes. public static final double kS = 0.14; // volts public static final double kV = 2.68; // V·s/m public static final double kA = 0.34; // V·s²/m // SysId suggests kP = 0.62 — start here, tune on carpet with fresh battery. public static final double kP = 0.62; public static final double WHEEL_DIAMETER_METERS = 0.1016; // 4 in public static final double GEAR_RATIO = 8.46; } // In DriveSubsystem.java: private final SimpleMotorFeedforward m_feedforward = new SimpleMotorFeedforward( DriveConstants.kS, DriveConstants.kV, DriveConstants.kA); private final PIDController m_drivePID = new PIDController(DriveConstants.kP, 0, 0);
Which Mechanisms to Characterize — and When
Not every motorized mechanism needs SysId. The return on investment depends on how precisely that mechanism must be controlled and how much that precision affects match outcomes.
| Mechanism | Characterize? | Notes |
|---|---|---|
| Drivetrain | ✅ Always | Required for accurate path following with PathPlanner or Choreo. kV accuracy directly determines how well trajectories track. Re-characterize if wheels or gearing changes. |
| Shooter wheel | ✅ Always | Velocity-controlled mechanism where feedforward accuracy determines RPM precision at shot time. Manual kV guessing produces inconsistent shot distances. |
| Arm / pivot | ✅ Recommended | Gravity loading makes manual gain tuning unreliable across the full range of motion. Characterize without game piece first, then note if gains need adjustment with mass loaded. |
| Elevator | ✅ Recommended | Same gravity argument as arm. Run with full payload (game pieces or end-effector attached) since kA and kV change significantly with load mass. |
| Intake rollers | ⚖️ Optional | If intake speed is just "fast enough" with duty-cycle control, characterization adds no value. Only needed if you're doing velocity-controlled intake for consistent game-piece speed matching. |
| Climber | ⚖️ Optional | Often just position-controlled with high kP. Useful if smooth profiled motion matters to reduce shock loads on the robot structure during climb. |
| Conveyor / indexer | ❌ Usually not needed | Typically sensor-triggered with duty-cycle commands. Adding velocity control and characterization adds complexity without meaningful benefit. |
I've watched teams at Week 1 regionals spend their entire practice match window trying to characterize their drivetrain because they "didn't have time during build season." You need a fully assembled, carpeted drivetrain with a charged battery and room to run 10+ feet in both directions. At competition, the pit is the size of a parking space, there are other robots everywhere, and you have 8 minutes between matches. Build characterization time into your schedule during the last two weeks of build season, when the robot is mechanically done but the software is still being written. The gains you collect in the shop will transfer directly to competition.
🔌 System Check
SysId applies raw open-loop voltage to your motors during testing. There is no position limit, no current limit bypass protection, and no closed-loop correction. Work through this list before enabling any test:
- Mechanism has clear travel in both directions. For a drivetrain, clear at least 10 feet forward and 10 feet back (more is better for quasistatic). For an arm, confirm full range of motion is unobstructed. Mark floor boundaries with tape so the operator knows when to release.
- Battery is fully charged. SysId, especially the dynamic test, draws heavy current. A battery below 12V produces unreliable kA values and may brownout mid-test, contaminating the data. Use a freshly charged battery for characterization runs.
- Encoder is correctly configured and verified. Position and velocity must read correctly before characterization. Run the quasistatic test with 0.5 V/s and watch the encoder reading — it should increase smoothly in one direction and decrease in the other. If it jumps, slips, or reads backwards, fix the encoder before collecting characterization data.
- AdvantageScope or Driver Station Log Viewer is open. You'll need to retrieve the
.wpilogfile after testing. Confirm you know where your log files are saved on the roboRIO before starting — the default path is/home/lvuser/logs/. Retrieve them via the Driver Station log folder tool or AdvantageScope's file fetch feature. - SysId commands are guarded against accidental trigger. These are the most dangerous commands on your robot during normal operation. They apply uncontrolled voltage until you release the button — or until the timeout fires. Guard them behind a mode toggle or use a controller that is physically disconnected during matches.
Knowledge Check
1. During the quasistatic forward test, a team notices that the velocity data shows a large, sudden jump about halfway through — the mechanism appeared to lurch. The rest of the data looks clean. What is the most likely physical cause, and how does it affect the regression?
2. A team characterizes their shooter wheel and gets kS = 0.11, kV = 0.082, kA = 0.003. They plug these into SimpleMotorFeedforward and find the shooter consistently reaches 3900 RPM when commanded to 4000 RPM. What is the correct interpretation?
3. A team wants to characterize their robot's arm. They run SysId without any game piece held by the arm. At competition, the arm consistently undershoots when holding a game piece. Why, and what is the correct fix?
Instrument a Subsystem and Simulate the Test Workflow
- Pick any motorized subsystem from your current or previous robot project — drivetrain, shooter, arm, or elevator. Add a
SysIdRoutinefield to the subsystem class. Configure theSysIdRoutine.Configwith a ramp rate of0.5 V/s, a step voltage of3.5 V, and a timeout of10 seconds. - Write the
SysIdRoutine.Mechanismlambda pair: one that applies voltage directly to the motor viasetVoltage(), and one that logs position, velocity, and applied voltage using the correct unit types (Meters/MetersPerSecondfor linear mechanisms,Rotations/RotationsPerSecondfor rotary). - Expose all four test commands as public methods and bind them to controller buttons in
RobotContainer. Add a comment on each binding explaining which physical test it runs and what property it measures. - If you have hardware access: run all four tests on the real mechanism. Export the
.wpilogfile and open it in WPILib's SysId analysis tool. Record the kS, kV, kA, and suggested kP values and place them in aCharacterizationConstantsinner class inside your subsystem's constants file. Note the R² value — if it's below 0.9, describe which data quality issue you suspect and what you would check first. - Bonus: Compare your characterized kV to the theoretical kV you can derive from the motor's free speed:
kV_theoretical = 12V ÷ free_speed_in_your_units. If your measured kV is significantly higher than theoretical (more than 20%), what does that tell you about the mechanism? What would you inspect on the mechanical side?