SimpleMotorFeedforward and SysId
Lesson 2 introduced the feedforward formula and the meaning of kS, kV, and kA. This lesson does two things: it completes the SimpleMotorFeedforward API reference so you know every method and its use case, and it walks the complete SysId characterization workflow — from preparing the mechanism through deploying gains — so you can run it confidently on any FRC mechanism.
By the end of this lesson, you will:
- Use every method on
SimpleMotorFeedforwardand explain when each is appropriate - Walk through all four phases of the SysId characterization workflow without referring to documentation
- Distinguish what the quasistatic test measures (kS, kV) from what the dynamic test measures (kA)
- Read the SysId analyzer's regression plots and identify clean vs. noisy data
- Integrate SysId-derived constants into a subsystem with the full Constants → field → setVoltage() pattern
- State the three most common SysId mistakes and how to avoid each
The Complete SimpleMotorFeedforward API
You've seen calculate(velocity). There are three call signatures — each for a different use case. Knowing which to use prevents subtle errors, especially when moving toward motion profiling in Lesson 11.
| Method signature | When to use it | kA term? |
|---|---|---|
| calculate(velocity) | Constant-velocity targets: shooter flywheel at steady RPM, intake roller, drive motor holding speed. Assumes acceleration = 0. Returns kS·sgn(v) + kV·v. | No (kA × 0 = 0) |
| calculate(velocity, acceleration) | You know both the target velocity and the instantaneous acceleration. Use this when implementing a custom motion profile where you compute the velocity and acceleration setpoints yourself. | Yes |
| calculate(currentVel, nextVel, dt) | Profiled motion: pass the current velocity, the next velocity setpoint, and the timestep. WPILib computes acceleration as (nextVel − currentVel) / dt internally. Used with TrapezoidProfile in Lesson 11. |
Yes (derived) |
| maxAchievableVelocity(maxVoltage, accel) | Query: given a max voltage and an acceleration requirement, what is the fastest achievable velocity? Useful for sanity-checking whether your target velocity is physically reachable. | Used in calculation |
| minAchievableVelocity(maxVoltage, accel) | Same as above but returns the minimum (most negative) achievable velocity. Used for verifying bidirectional motion is possible. | Used in calculation |
| maxAchievableAcceleration(maxVoltage, vel) | Query: at a given velocity, what is the maximum achievable acceleration? Useful for configuring TrapezoidProfile constraints to not exceed the mechanism's physical capability. | Used in calculation |
The SysId Characterization Workflow
SysId doesn't require understanding the regression math — it requires understanding the four-step process and executing each step correctly. Click through each phase.
The most important step. SysId tests require the mechanism to move freely through its full range of motion without hitting limits, damaging itself, or being constrained. Before running anything:
- Drivetrain: Put the robot on blocks so the wheels spin freely. Remove any carpet traction — the characterization assumes unrestricted rotation.
- Arm/elevator: Ensure the full range of motion is clear. Remove any softlimits in code temporarily. Characterization tests ramp voltage continuously — if the mechanism hits a hard stop at full ramp voltage, you'll damage hardware.
- Shooter flywheel: No special prep needed — flywheels rotate freely. Just ensure no game pieces are loaded.
- Verify encoder wiring: SysId requires a functioning encoder on the mechanism being characterized. Run the test without SysId first: command a slow voltage and verify the encoder reads increasing position in the correct direction. A reversed or disconnected encoder produces garbage data.
- Fresh battery: A low battery affects the voltage-to-velocity relationship and corrupts the characterization. Always characterize with a battery above 12V.
Modern SysId (WPILib 2024+) is integrated directly into your robot project — no separate deployment needed. You add a SysIdRoutine to your subsystem and bind the test commands to buttons:
- Create a
SysIdRoutinefield in the subsystem with aConfig(ramp rate, step voltage, timeout) and aMechanism(drive consumer, log consumer, subsystem). - The
Mechanism's drive consumer receives aMeasure<Voltage>and should callm_motor.setVoltage(volts.in(Volts)). - The log consumer calls
log.motor("mechanism-name").voltage(...).linearPosition(...).linearVelocity(...)using the motor's StatusSignals. - In
RobotContainer, bind four commands: quasistatic forward, quasistatic reverse, dynamic forward, dynamic reverse — each to a different button or binding combination.
Key config values: Ramp rate for quasistatic (default 1V/s is fine for most mechanisms). Step voltage for dynamic (typically 4–7V). Timeout (5–10 seconds per test). Keep the timeout short enough that the mechanism doesn't travel too far.
With the robot on blocks (drivetrain) or clear of obstacles (mechanism), enable in Test Mode and run each test command in order. Watch the mechanism closely during each:
Quasistatic Forward: Voltage ramps slowly from 0V upward. The mechanism gradually accelerates. The data captures how much voltage is needed to sustain each speed — this maps directly to kS (the minimum voltage before motion starts) and kV (the slope of voltage vs. velocity). Run until the mechanism reaches near-max speed or the timeout expires.
Quasistatic Reverse: Same test in the reverse direction. Necessary to confirm kS and kV are symmetric (they usually are; large asymmetry indicates a mechanical problem).
Dynamic Forward: A step input — voltage jumps immediately to the configured step voltage. The mechanism accelerates hard from 0 to max speed. The sharp acceleration data is what kA is derived from. This test is shorter (2–3 seconds typically).
Dynamic Reverse: Same step test in reverse direction.
Data logging: WPILib's DataLogManager saves all test data automatically to a WPILOG file. After all four tests, retrieve the log file from /home/lvuser/logs/ via FTP or the DS log viewer.
Open the WPILOG file in SysId Analyzer (accessible from WPILib's tools menu or Ctrl+Shift+P → Open SysId Analyzer):
- Select the mechanism log: The analyzer shows all logged mechanisms from the file. Select the one you characterized.
- Review the plots: The quasistatic plot shows voltage vs. velocity — it should be a tight line. The dynamic plot shows velocity vs. time — it should be a smooth ramp. Noisy or scattered plots indicate bad data (covered in the next section).
- Check R²: The analyzer reports an R² (coefficient of determination) for the regression. Above 0.99 is excellent. 0.95–0.99 is acceptable. Below 0.90 indicates the data is too noisy or the model doesn't fit — re-run the tests.
- Read the gains: The analyzer shows kS, kV, kA, and a suggested feedback gain (kP) in the output panel. Copy these values.
- Enter in Constants.java: Paste kS, kV, kA into your constants file. Use them to construct
SimpleMotorFeedforwardin the subsystem. The characterization is complete.
Reading the Analyzer: Clean vs. Noisy Data
The quasistatic plot is voltage on the Y axis, velocity on the X axis. A clean characterization produces a straight line. Click each tab to see what the data looks like and why.
The Three Most Common SysId Mistakes
SysId requires consistent units. If your subsystem logs position in rotations but velocity in meters per second, the analyzer produces meaningless gains because the regression is fitting mixed-unit data. Decide your units before characterization — either use rotations throughout (and convert to meters per second later in code), or configure withSensorToMechanismRatio() on the TalonFX so everything reads in wheel rotations and use circumference in the log consumer. Whichever you choose, kV's unit will be V·s per [your velocity unit]. Make sure your SimpleMotorFeedforward constructor uses matching units.
Running the quasistatic test on a drive motor while the robot is on carpet gives you characterization data for a combination of motor + carpet + traction + frame inertia. That data doesn't apply when the robot is driving on a different surface or under a different load. For drivetrains, always characterize on blocks. For arms, always characterize with the same end-effector weight attached that will be present in competition — a lighter arm has a very different kA than a fully loaded one.
SysId characterization is valid for a given physical mechanism configuration. If you don't change the gearbox ratio, motors, or mechanism mass, last season's constants are often close enough to re-use (kV and kS drift slightly as bearings wear). Re-characterize when: you change gear ratios, replace motors, add significant mass to the mechanism, or notice that the mechanism's steady-state behavior has drifted far from the constants. Re-characterizing "just to be safe" without a reason wastes valuable pre-season testing time.
Complete Integration Pattern
This is the full pattern from SysId output to deployed subsystem. Click each tab, then click highlighted tokens for explanations.
public static final class Shooter {
// Feedforward — measured by SysId quasistatic + dynamic
public static final double kS = 0.23116; // V
public static final double kV = 0.11837; // V·s/rot
public static final double kA = 0.01024; // V·s²/rot
// PID — suggested by SysId Analyzer (Lesson 10)
public static final double kP = 0.001; // V per rot/s error
// Mechanism geometry
public static final double kGearRatio = 1.0;// direct drive
public static final double kMaxRPS = 100.0;
}
private final TalonFX m_motor = new TalonFX(Constants.Shooter.kMotorId);
private final SimpleMotorFeedforward m_ff =
new SimpleMotorFeedforward(
Constants.Shooter.kS,
Constants.Shooter.kV,
Constants.Shooter.kA);
private final StatusSignal<Double> m_velSignal =
m_motor.getVelocity();
public void setVelocityRPS(double targetRPS) {
double ffVolts = m_ff.calculate(targetRPS);
m_motor.setVoltage(ffVolts);
SmartDashboard.putNumber("Shooter/TargetRPS", targetRPS);
SmartDashboard.putNumber("Shooter/ActualRPS", m_velSignal.refresh().getValueAsDouble());
SmartDashboard.putNumber("Shooter/FFVolts", ffVolts);
}
public void stop() { m_motor.stopMotor(); }
public double getVelocityRPS() {
return m_velSignal.refresh().getValueAsDouble();
}
}
private final SysIdRoutine m_sysId = new SysIdRoutine(
new SysIdRoutine.Config(
null, // ramp rate: default 1 V/s
Volts.of(4), // dynamic step voltage: 4V
Seconds.of(5), // timeout per test: 5s
(state) -> sysIdState(state)), // state logger
new SysIdRoutine.Mechanism(
(volts) -> m_motor.setVoltage(volts.in(Volts)),
(log) -> {
log.motor("shooter")
.voltage(m_motor.getMotorVoltage().getValue().in(Volts))
.angularPosition(m_motor.getPosition().getValue().in(Rotations))
.angularVelocity(m_motor.getVelocity().getValue().in(RotationsPerSecond));
},
this));
// Expose as commands — bind in RobotContainer
public Command sysIdQuasistatic(SysIdRoutine.Direction dir) {
return m_sysId.quasistatic(dir);
}
public Command sysIdDynamic(SysIdRoutine.Direction dir) {
return m_sysId.dynamic(dir);
}
SysId gives you numbers derived from actual physics measurements of your robot. That's better than guessing. But "derived from measurements" doesn't mean "guaranteed correct" — bad test runs produce bad numbers. After running SysId and loading the constants, the first test is always: command a moderate constant velocity, wait for steady state, read the actual velocity from SmartDashboard. If the feedforward-only result is within 5% of the commanded value, the constants are good. If it's off by 20%, something is wrong — wrong units, wrong gear ratio in the log consumer, test run on carpet instead of blocks. Verify before relying on the numbers for competition tuning.
Four verification steps before using SysId-derived constants in competition code:
- Open the WPILOG in the SysId Analyzer. Check the R² value — it should be above 0.99 for a clean characterization. If below 0.95, re-run the tests and examine what caused the noise (encoder vibration, low battery, mechanism binding).
- Sanity-check the kV value against a back-of-envelope estimate: for a Falcon 500 with no gearbox (direct drive), free-speed is ~6380 RPM = 106 RPS. Free-speed voltage is ~12V. kV ≈ (12V − kS) / 106 RPS ≈ 0.111 V·s/rot. If your kV is wildly different from this estimate, check encoder units.
- Command your mechanism at a velocity you observed during testing. SmartDashboard should show actual velocity within 5% of commanded. If not, check that the log consumer and the setVelocityRPS() method use the same velocity units.
- Command velocity zero. Motor should stop. If it keeps spinning slowly, kS is too large — it provides enough voltage to overcome friction even when the feedforward "velocity" is zero. Reduce kS slightly.
Knowledge Check
1. The quasistatic test is used to measure kS and kV, while the dynamic test measures kA. Why can't the quasistatic test measure kA?
2. After running SysId on your drivetrain, the analyzer shows R² = 0.87 for the quasistatic test. The quasistatic plot looks scattered rather than linear. What should you do before using these gains?
3. You characterize a shooter flywheel with the SysId log consumer logging velocity in RotationsPerSecond. You then create new SimpleMotorFeedforward(kS, kV, kA) and call calculate(targetMPS) with a target in meters per second. What is wrong?
Run SysId on a Mechanism
- Choose a mechanism: drivetrain (on blocks), shooter flywheel, or elevator/arm (clear of limits). Add a
SysIdRoutinefield to its subsystem using the pattern in the SysIdRoutine tab above. Configure a 5-second timeout and a 4V step voltage. Expose the four test commands:sysIdQuasistatic(Direction.kForward),sysIdQuasistatic(Direction.kReverse),sysIdDynamic(Direction.kForward),sysIdDynamic(Direction.kReverse). - In
RobotContainer, bind each test command to a button combination (e.g.,m_driver.a().and(m_driver.leftBumper())for quasistatic forward). Deploy and verify each button triggers the expected test mode in Test Mode. Monitor SmartDashboard to confirm the mechanism responds during each test. - Run all four tests in order, recording what happens during each. Retrieve the WPILOG from the roboRIO. Open it in SysId Analyzer. Check the R² values — if below 0.98, identify the most likely cause from the data quality section and re-run if necessary.
- Copy the kS, kV, kA values into Constants.java. Update your subsystem to use them with
SimpleMotorFeedforward. Command a target velocity and compare the actual velocity on SmartDashboard. The steady-state actual velocity should be within 5% of the commanded target with feedforward only (no PID). - Stretch goal: Call
m_ff.maxAchievableVelocity(12.0, 0)with your measured kS and kV values. Compare this to the observed maximum velocity at full throttle during the dynamic test. Do they match? If not, what physical factor could explain the discrepancy (hint: back-EMF, temperature, friction changes at high RPM)?