Skip to content

Mini-Project: A Velocity-Controlled Shooter on REVLib

learnfrc.com
learnfrc.comAuthor
Veer Bajaj
Veer BajajMaintainer

A shooter has to reach a consistent surface speed before it launches, or the shots scatter. We’ll run a NEO Vortex (free speed 6784 RPM, Kv 565) on a SPARK Flex in closed-loop velocity mode, using REVLib 2025’s declarative configuration API.

In REVLib 2025 you no longer call setters one at a time on the controller. Instead you build a SparkFlexConfig and configure() it once, with explicit reset and persist modes:

private final SparkFlex m_motor = new SparkFlex(31, MotorType.kBrushless);
private final SparkClosedLoopController m_pid = m_motor.getClosedLoopController();
private final RelativeEncoder m_enc = m_motor.getEncoder();
public Shooter() {
SparkFlexConfig config = new SparkFlexConfig();
config.closedLoop
.feedbackSensor(FeedbackSensor.kPrimaryEncoder)
.pid(0.0001, 0.0, 0.0) // velocity P,I,D
.velocityFF(1.0 / 6784.0); // ~1/free-speed-RPM for a NEO Vortex (6784 RPM)
config.smartCurrentLimit(60);
m_motor.configure(config,
ResetMode.kResetSafeParameters,
PersistMode.kPersistParameters);
}

PersistMode.kPersistParameters writes to flash, so the config survives a brown-out reboot. That is worth doing for a one-time setup, but never call it every loop: flash writes block CAN comms.

Command the velocity through the SparkClosedLoopController with setReference(). REVLib 2026 deprecates that method in favor of an identical setSetpoint(), so use that one on the 2026 library:

private static final double kTargetRpm = 4800;
public Command spinUp() {
return run(() ->
m_pid.setReference(kTargetRpm, ControlType.kVelocity));
}
public boolean atSpeed() {
return Math.abs(m_enc.getVelocity() - kTargetRpm) < 100; // RPM tolerance
}

Expose atSpeed() as a Trigger and run the feeder only when the flywheel is ready. This is the BoVLB best practice of asking yes/no questions in problem-domain language:

Trigger ready = new Trigger(m_shooter::atSpeed);
// hold to spin up
m_driver.rightTrigger().whileTrue(m_shooter.spinUp());
// feed only once the wheel has recovered to speed
m_driver.rightTrigger().and(ready).whileTrue(m_feeder.feed());

A flywheel’s steady-state voltage is almost entirely feedforward: V = kV * rpm. If velocityFF is set correctly, the controller jumps to near the right voltage immediately, and P only has to trim the last few percent. Teams that leave FF at zero and crank P instead get a sluggish, oscillating flywheel that dips badly when a game piece loads it. The SPARK’s velocityFF multiplies the RPM setpoint to produce a duty-cycle output, so its value is roughly 1 / free-speed-RPM. Find it from a SysId run, or empirically: command a fixed duty cycle, read the steady RPM, and velocityFF = appliedOutput / rpm. (REVLib is moving toward a feedForward config with explicit kS/kV terms, but velocityFF() still works in 2025.)

Plot m_enc.getVelocity() against the setpoint in AdvantageScope. A good shooter recovers to within tolerance in well under half a second after each shot.

  • REVLib 2025 uses declarative SparkFlexConfig/SparkMaxConfig objects applied with configure(), not per-parameter setters.
  • In REVLib 2025 use SparkClosedLoopController.setSetpoint() – setReference() is deprecated.
  • Use PersistMode.kPersistParameters once at setup; never persist every loop – flash writes block CAN.
  • Flywheels are feedforward-dominated: set velocityFF (~1/Kv) correctly and keep P small; gate the feeder on an atSpeed() Trigger.

This lesson was adapted from learnfrc.com.