Introduction to State-Space Control
PID answers the question "how far off am I right now?" State-space answers a harder question: "given everything I know about my system's physics, what is the optimal control action at every moment?" This lesson introduces the vocabulary and architecture — so you can read elite team code and decide when the added complexity earns its keep.
By the end of this lesson, you will:
- Explain what a "state" is and why representing a system in state-space captures more information than a single error signal
- Describe what the system matrices A, B, C, and D represent in plain physical terms for a simple mechanism
- Understand the role of the Linear Quadratic Regulator (LQR) as an optimal state-feedback controller and what "optimal" means in this context
- Describe what a Kalman filter does — fusing noisy measurements with a model prediction — and why this matters for mechanism control
- Read and interpret a WPILib state-space flywheel example using
LinearSystemId,LinearQuadraticRegulator, andKalmanFilter - Articulate when state-space control is worth the additional complexity compared to feedforward + PID
State-space control draws on linear algebra and control theory that most FRC students encounter for the first time here. The goal of this lesson is to build mental models, read real WPILib code, and make informed decisions about when state-space is the right tool. You do not need to derive matrices from scratch — WPILib's LinearSystemId class and SysId do that work. What you need is enough understanding to configure the tools correctly and trust the output.
What PID Doesn't Know
Across Unit 8 you've built powerful control with PID and feedforward. For most FRC mechanisms, that combination works extremely well. But PID has a structural limitation worth understanding: it operates on a single number — the current error — and has no direct knowledge of how the mechanism got there or where it's going.
Consider a shooter flywheel. PID measures the difference between target RPM and current RPM. When the error is large, it applies more voltage. When the error shrinks, it backs off. This works, but PID doesn't inherently know the flywheel's rotational inertia, how quickly it can accelerate, or what voltage is physically needed to hold a given RPM against back-EMF. You provide that knowledge indirectly through kP, kI, kD — but the gains don't encode the physics. They encode your tuning intuition.
State-space control takes a fundamentally different approach: it starts with a mathematical model of the mechanism's physics and designs a controller directly from that model. Instead of asking "what gain makes the response look right," it asks "what control law is mathematically optimal given what we know about how this system behaves."
- Observes: current error only
- Model: implicit in gain values
- Design: tune kP, kI, kD by observation
- Noise: no explicit handling
- Multi-state systems: requires cascading loops
- Works with partial sensor info
- Low setup complexity
- Requires hardware for validation
- Observes: full system state vector
- Model: explicit physics matrices (A, B, C, D)
- Design: computed mathematically from model
- Noise: explicitly modeled and filtered
- Multi-state systems: handled natively
- Estimates unmeasured states (velocity from position)
- Higher setup complexity
- Can be validated in simulation before hardware
The State Vector: Everything the System Knows About Itself
The central concept in state-space control is the state vector — a compact mathematical description of everything relevant about a mechanism's current condition. For a flywheel, the state might be just one thing: angular velocity. For an arm, it might be two things: angle and angular velocity. For a robot's full position on the field, it might be three: x position, y position, and heading.
Formally, the state vector is written as x (bold x, or x with an arrow). The control input — the voltage or command sent to the actuator — is written as u. The measurement from sensors is written as y.
The state-space model then describes two things:
- How the state evolves over time given a control input:
ẋ = Ax + Bu - What measurements the sensors produce from the current state:
y = Cx + Du
A, B, C, and D are matrices — grids of numbers that encode the system's physics. They look intimidating but each has a clear physical meaning. Click each one to understand what it represents for a real mechanism.
The A matrix describes how each state variable naturally affects the others over time, in the absence of any control input. For a flywheel, A captures back-EMF: a spinning flywheel naturally decelerates due to motor and friction losses, so A encodes the rate at which velocity decays without voltage applied.
For a double-jointed arm where angle and angular velocity are both states, A would encode how angular velocity translates into changing angle (they're coupled: velocity is the derivative of position). A also encodes how back-EMF and gravity affect angular velocity over time.
In WPILib, you rarely compute A by hand. LinearSystemId.createFlywheelSystem(), LinearSystemId.createElevatorSystem(), and related factory methods compute A automatically from motor parameters and mechanism geometry that you provide.
LinearSystemId is a factory class that builds the A, B, C, D matrices for common FRC mechanism types from a small set of physical parameters: the DCMotor model, gear ratio, mass/inertia, and (for arms) the arm length. Feed it the same SysId gains you collected in Lesson 13 and it produces a complete LinearSystem object ready for LQR and Kalman filter construction. The matrix math is handled internally — you reason about the parameters, not the algebra.
The Linear Quadratic Regulator: Optimal State Feedback
Once you have a model of the system, you need a controller. The Linear Quadratic Regulator (LQR) is a method for designing that controller automatically, given two user-specified cost matrices that express your priorities:
- Q — how much you penalize state error. A high Q entry for velocity means you want velocity to track the setpoint aggressively. A low Q entry means you're tolerant of velocity error.
- R — how much you penalize control effort (voltage). A high R means you want the controller to be conservative with voltage. A low R means you're willing to use aggressive voltage to achieve the goal.
Given the system model (A, B matrices) and your Q and R preferences, LQR solves a mathematical optimization problem and produces a gain matrix K. The controller output is then simply:
This looks like PID — multiply a gain by an error — but K is a matrix that accounts for all state variables simultaneously. For a flywheel with only velocity as a state, K is a 1×1 matrix and the result is nearly identical to a well-tuned proportional controller with good feedforward. For an arm with position and velocity as states, K is 1×2 and the controller simultaneously considers both "how far off am I?" and "how fast am I moving?" This is the D term's insight — baked in automatically from the model rather than tuned manually.
Team 254 has used state-space control for shooter RPM and arm position since 2019. Team 6328 (Mechanical Advantage) published detailed write-ups on their flywheel and turret state-space controllers. Team 971 (Spartan Robotics) is particularly well-known for state-space architecture — their publicly available code is among the most sophisticated in FRC. What you'll notice reading their code is that the LQR gain matrix K is computed once during construction and the per-loop calculation is a single matrix multiplication. The complexity is front-loaded into model design, and the loop itself is simple.
The Kalman Filter: Trusting Your Model Alongside Your Sensors
Controllers need to know the current state. For a flywheel, you can measure velocity directly from an encoder. But encoders are noisy — the measured value jumps around the true value by some amount on every loop. LQR driving off noisy measurements produces noisy control outputs, which causes the mechanism to jitter.
The Kalman filter solves this by combining two sources of information on every timestep:
- The model prediction — "given the voltage we applied last loop and our physics model, where should the state be now?"
- The sensor measurement — "what does the encoder actually read right now?"
Neither source is perfectly trusted. The model has uncertainty (the physics model isn't exact). The sensor has noise (the encoder reading bounces). The Kalman filter blends the two in proportion to how much each one is trusted, producing a smoothed state estimate that is more accurate than either source alone.
You configure the Kalman filter with two noise matrices:
- stateStdDevs — how much you trust your model. High values mean the model is uncertain; the filter leans more on measurements.
- measurementStdDevs — how noisy your sensors are. High values mean measurements are unreliable; the filter leans more on the model.
SwerveDrivePoseEstimator — the class you used in Unit 7 to fuse vision measurements with odometry — is a Kalman filter. It combines the model prediction from wheel odometry with noisy vision pose measurements, weighting each by how much it's trusted. State-space control uses the same mathematical idea, but applied to mechanism velocity or position rather than field-level localization. If odometry fusion made intuitive sense, the Kalman filter for mechanism control is the same concept applied to a simpler, single-mechanism context.
Comparing Controllers: A Live Flywheel Simulation
The following simulation runs three controller strategies simultaneously against the same flywheel model. Adjust the target RPM and watch how each controller responds. The differences in settling time, overshoot, and noise rejection illustrate why state-space adds value in high-precision velocity control.
State-Space in WPILib: A Flywheel Example
WPILib's state-space classes are in the edu.wpi.first.math.controller and edu.wpi.first.math.estimator packages. The following is a complete, commented flywheel subsystem using LinearSystemId, LinearQuadraticRegulator, and KalmanFilter. Read through it with the preceding theory in mind — each class corresponds directly to one of the three concepts: model, controller, estimator.
import edu.wpi.first.math.Matrix; import edu.wpi.first.math.Nat; import edu.wpi.first.math.VecBuilder; import edu.wpi.first.math.controller.LinearQuadraticRegulator; import edu.wpi.first.math.estimator.KalmanFilter; import edu.wpi.first.math.numbers.*; import edu.wpi.first.math.system.LinearSystem; import edu.wpi.first.math.system.LinearSystemLoop; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.math.system.plant.LinearSystemId; public class FlywheelSubsystem extends SubsystemBase { private final TalonFX m_motor = new TalonFX(FlywheelConstants.MOTOR_ID); // ── 1. SYSTEM MODEL ─────────────────────────────────────────────────────── // LinearSystemId builds the A, B, C, D matrices for a flywheel. // State: [angular velocity (rad/s)] Input: [voltage (V)] Output: [angular velocity] // DCMotor.getKrakenX60(1) = one Kraken X60, with no gearing reduction. private final LinearSystem<N1, N1, N1> m_plant = LinearSystemId.createFlywheelSystem( DCMotor.getKrakenX60(1), FlywheelConstants.MOMENT_OF_INERTIA_KG_M2, // measure or estimate from CAD FlywheelConstants.GEAR_RATIO); // ── 2. KALMAN FILTER (Estimator) ─────────────────────────────────────────── // Blends model prediction with encoder measurement. // stateStdDevs: how much we trust the model (rad/s uncertainty) // measurementStdDevs: how noisy the encoder is (rad/s noise floor) // Smaller value = more trust. Larger value = less trust. private final KalmanFilter<N1, N1, N1> m_observer = new KalmanFilter<>( Nat.N1(), Nat.N1(), m_plant, VecBuilder.fill(3.0), // model uncertainty: 3 rad/s VecBuilder.fill(0.01), // measurement noise: 0.01 rad/s (encoder is accurate) 0.020); // dt: 20 ms standard WPILib loop // ── 3. LQR CONTROLLER ───────────────────────────────────────────────────── // Q: how much we penalize velocity error (rad/s). Lower = more aggressive. // R: how much we penalize voltage usage (V). Higher = more conservative. // LQR solves for the optimal gain matrix K automatically. private final LinearQuadraticRegulator<N1, N1, N1> m_controller = new LinearQuadraticRegulator<>( m_plant, VecBuilder.fill(8.0), // Q: tolerate up to 8 rad/s error VecBuilder.fill(12.0), // R: limit to 12V max effort 0.020); // ── 4. LOOP (combines all three) ─────────────────────────────────────────── // LinearSystemLoop wires together the plant, controller, and observer. // maxVoltage clamps output to protect against voltage saturation. private final LinearSystemLoop<N1, N1, N1> m_loop = new LinearSystemLoop<>(m_plant, m_controller, m_observer, 12.0, 0.020); @Override public void periodic() { // Correct the Kalman filter with the latest encoder measurement (converted to rad/s) m_loop.correct(VecBuilder.fill(getVelocityRadPerSec())); // Predict: advance the model one timestep and compute the control output voltage m_loop.predict(0.020); // Apply the computed voltage to the motor m_motor.setVoltage(m_loop.getU(0)); } public void setGoalVelocity(double velocityRadPerSec) { m_loop.setNextR(VecBuilder.fill(velocityRadPerSec)); } public boolean atGoal() { double errorRadPerSec = m_loop.getError(0); return Math.abs(errorRadPerSec) < FlywheelConstants.VELOCITY_TOLERANCE_RAD_PER_S; } private double getVelocityRadPerSec() { // TalonFX returns velocity in rot/s — convert to rad/s for state-space units return m_motor.getVelocity().getValueAsDouble() * 2.0 * Math.PI / FlywheelConstants.GEAR_RATIO; } }
The two lines in periodic() — correct() then predict() — are the two-step Kalman filter cycle. Correct updates the state estimate using the latest sensor measurement: "here's what the encoder read, update my best guess of the true velocity." Predict advances the model forward one timestep and computes the control output: "given the updated state estimate and the setpoint, what voltage should I apply?" The controller runs this update 50 times per second in sync with the rest of the robot loop.
The One Parameter You Have to Get Right: Moment of Inertia
LinearSystemId.createFlywheelSystem() takes a moment of inertia in kg·m². This is the rotational equivalent of mass — it describes how hard it is to spin the flywheel up or slow it down. Getting it wrong produces a model that doesn't match the real mechanism, which means the LQR gains are computed for the wrong system and performance degrades.
There are three ways to get a good value:
- From CAD. Most modern FRC teams use Onshape or SolidWorks. Both can compute the moment of inertia of a part or assembly about any axis in a single click. This is the most accurate method and should be your first choice.
- From SysId. The kA value from SysId is the acceleration feedforward — and kA is physically related to the moment of inertia:
J ≈ kA / (kV / 12V)for a rough estimate. This isn't exact, but it's a usable starting point when CAD isn't available. - From the WPILib simulation classes.
DCMotorSimandFlywheelSimaccept a moment of inertia and simulate the mechanism response. If your simulation matches the real hardware behavior with a particular J value, that J is likely correct.
When State-Space Earns Its Complexity
State-space control has real overhead: you need to understand the model, set appropriate noise parameters, and ensure your moment of inertia estimate is accurate. For many FRC mechanisms, feedforward + PID is the right choice. Here's an honest comparison:
| Mechanism & situation | FF + PID | State-Space (LQR + Kalman) | Notes |
|---|---|---|---|
| Shooter flywheel (high-precision RPM) | Good | Better | State-space handles noisy velocity measurements more gracefully; Kalman smoothing produces more consistent shot-to-shot RPM |
| Arm / pivot (single joint) | Good | Marginal benefit | ProfiledPIDController + ArmFeedforward works well for most single-joint arms; state-space adds value when noise is severe or gravity model must be exact |
| Elevator | Good | Marginal benefit | ElevatorFeedforward handles gravity; state-space beneficial if elevator has heavy variable load that changes inertia |
| Drivetrain velocity tracking | Good | Situational | PathPlanner / Choreo + feedforward is industry standard; state-space mainly used by teams with significant simulation pipelines |
| Field-level pose estimation | Not applicable | Standard approach | SwerveDrivePoseEstimator is a Kalman filter — you're already using state-space here whether you realize it or not |
| First-year programmer, build season crunch | Right choice | Too much setup risk | Get mechanisms working with proven PID + feedforward; add state-space in the off-season on a mechanism you understand deeply |
The flywheel is the ideal first state-space mechanism for an FRC team. It has a single state (velocity), a single actuator (one or two motors), well-understood physics, and a measurable benefit (more consistent RPM → more consistent shot distance). The WPILib flywheel example is well-documented, and the Kalman filter's noise parameters are easy to reason about with a real encoder attached. Successfully deploying state-space control on a flywheel in the off-season gives you the confidence and intuition to apply it to more complex mechanisms during build season.
🔌 System Check
State-space controllers can behave unexpectedly when the model doesn't match the hardware. Work through this before enabling:
- Moment of inertia is verified. If you used a CAD estimate, confirm the robot's flywheel mass and geometry match what was modeled — added bolts, a heavier wheel, or a different shaft can change J meaningfully. If J is off by more than 30%, the LQR gains will be computed for the wrong system.
- Velocity units are consistent. WPILib state-space classes use SI units (radians per second for angular velocity, meters per second for linear). If your encoder returns rotations per second or RPM, convert explicitly before calling
correct(). A unit error produces a controller that thinks the system is always dramatically off-setpoint. - Voltage is clamped.
LinearSystemLoopclamps output tomaxVoltage. Confirm that value is 12V (or your actual bus voltage), not a smaller number that limits mechanism performance, and not a larger one that lets the loop command unsafe voltages. - Test at reduced setpoints first. Set the initial goal velocity to 25% of maximum. Confirm the mechanism accelerates smoothly to that speed and holds it before commanding full speed. A misconfigured Kalman filter or wrong J will be visible at low speeds before it becomes dangerous at full speed.
- Current limits are still configured on the motor controller. The state-space loop outputs a voltage command, not a current limit. Stator current limits on the TalonFX or SparkMax still apply and still matter — don't skip them because you switched to state-space control.
Knowledge Check
1. A programmer sets up a LinearQuadraticRegulator for a flywheel and sets the Q vector to a very large value (e.g., 100 rad/s tolerance) and the R vector to a very small value (e.g., 0.01 V). What behavior should they expect, and why?
2. A team's state-space flywheel controller performs well in simulation but oscillates on the real robot. All other parameters look correct. What is the most likely cause?
3. Which statement best explains why SwerveDrivePoseEstimator — which you used in Unit 7 — is a Kalman filter?
Unit 8 covered the full control theory stack used by championship FRC teams: why open-loop control fails, feedforward from physical models (kS, kV, kA, kG), PID and its three terms, tuning methodology, combining feedforward and PID, motion profiling with ProfiledPIDController, on-controller profiling with Motion Magic, system identification with SysId, and — in this lesson — the state-space architecture that ties models, controllers, and estimators together systematically. Every lesson in this unit connects to something on the physical robot: a motor being driven, a gear being accelerated, a shot being taken. That's the systems-first thread. Take what you've built here into Unit 9, where autonomous routines bring it all together on the field.
Read and Map a State-Space Flywheel to the Three-Layer Architecture
- Open the WPILib state-space flywheel example at
docs.wpilib.org(search "state-space flywheel"). Read through the complete example. For each of the three main objects (LinearSystem,KalmanFilter,LinearQuadraticRegulator), write one sentence in plain English describing what that object does and what physical quantities it works with. - In the example code, identify: (a) where the model uncertainty (stateStdDevs) is set, (b) where the measurement noise (measurementStdDevs) is set, and (c) where the Q and R matrices are set. For each, describe in a sentence what you would change if the encoder were very noisy, and in what direction.
- Find the
DCMotorclass in WPILib's Javadoc. List the static factory methods available —getNEO(),getKrakenX60(), etc. What parameters does each motor's factory method take, and what do they represent? Why does the number of motors matter for the flywheel model? - Design exercise (no hardware needed): You have a shooter flywheel with two Kraken X60 motors, a gear ratio of 1:1, and a flywheel disk you estimate has a moment of inertia of 0.004 kg·m² (a rough estimate for a 6-inch, 0.5 kg rubber flywheel). Write the complete Java subsystem constructor (just the state-space setup — not the full periodic loop) that creates the
LinearSystem,KalmanFilter,LinearQuadraticRegulator, andLinearSystemLoop. Label every numeric parameter with a comment explaining its physical meaning and units. - Bonus: Read Team 6328's or Team 254's most recent publicly available code (GitHub links in Unit 0, Lesson 5). Find a mechanism they control with state-space methods. Identify which mechanism it is, what the state vector contains, and how they chose their Q and R matrix values. Write three sentences summarizing your findings and what you'd do differently or the same on your own team's robot.