For the complete documentation index, see llms.txt. This page is also available as Markdown.

Cascade Control

package com.seattlesolvers.solverslib.controller

A CascadeController chains two controllers together into a single closed-loop system. The primary (outer) controller runs on position: it compares the measured position against the position setpoint and outputs a velocity command. The secondary (inner) controller runs on velocity: it compares the mechanism's measured velocity against that velocity command and outputs the final control value (typically motor power).

This nested structure is called cascade control. The inner velocity loop reacts quickly to disturbances (battery sag, friction, gravity, a game element landing in your intake) before they ever show up as position error, while the outer position loop only has to steer a well-behaved velocity loop. The result is generally smoother, more consistent motion than a single position PID - especially on heavy arms and high-speed slides.

You can find the source code for the CascadeController class here, and the theory behind closed-loop control on CtrlAltFtc here.,

When to use this over a standard PIDF?

  • Heavy arms and lifts: the inner velocity loop fights gravity and load changes immediately, instead of waiting for position error to build up

  • Fast(er) slides: commanding velocity rather than raw power gives controlled acceleration and less slamming at the ends of travel

  • Consistency across battery voltage: a velocity loop compensates for voltage drop automatically, so the same setpoint behaves the same at 14V and at 12V

If a single well-tuned PIDF already does the job for your mechanism, you probably don't need a cascade. Consider using a dual PIDF controller (one for large error, one for small error) before this.

Constructing a CascadeController

CascadeController takes any two SolversLib Controller objects - most commonly a PIDController or PIDFController for each loop:

// Outer loop: position error (ticks) -> velocity command (ticks per second)
PIDController positionController = new PIDController(kP_pos, kI_pos, kD_pos);

// Inner loop: velocity error (ticks per second) -> motor power
PIDFController velocityController = new PIDFController(kP_vel, kI_vel, kD_vel, kF_vel);

CascadeController cascade = new CascadeController(positionController, velocityController);

Because the two gain sets multiply through the cascade, the outer controller's output is in velocity units, and the inner controller's gains convert velocity error into power. Expect the outer kP to be much larger than a standalone position PID's, and the inner gains to be small.

Setting Setpoints

The velocity setpoint from setSetPoints() is added to the primary controller's output before it reaches the inner loop, so it acts as a velocity feedforward - useful for following motion profiles, where each loop you feed in the profile's instantaneous target position and target velocity.

Usage Example

A lift driven to a scoring position with a cascade of two controllers:

Measuring Velocity

getMeasuredVel() returns the velocity the controller measured on its most recent calculate() call, in position units per second. This is handy for telemetry, logging, and tuning the inner loop.

Resetting the Controller

reset() clears the measured velocity and timestamps, and also resets both the primary and secondary controllers, so any wound-up integral term in either loop is discarded. This is useful for when you need to re-enable the mechanism or switching between control tasks.

Last updated