Gray Matter
WorkshopSwerve Calibration
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 18

Swerve Calibration

The project you generated came with somebody else's numbers in TunerConstants.java. This lesson replaces the ones that matter with measurements off your own robot, then closes the drive loop. Almost no Java.

15 minutes
You’ll need
  • A swerve robot you can drive, from Swerve Project Generator.
  • Logging on. Two of the measurements come out of a .wpilog.
  • Phoenix Tuner X, and six meters of clear carpet.
  • A tape measure, a long straight edge, and a wall you may push against.

Work in order: each step measures something the steps after it depend on.

Three kinds of zeroing

Three operations in the swerve code all get called resetting or seeding. Mix them up and you get a robot that drives beautifully and has no idea where it is.

seedFieldCentric(), on the left bumper, changes which way the sticks call forward: whatever the robot faces now becomes forward. Every loop, applyOperatorPerspective()sets that same forward from alliance color: 0° on blue and 180° on red. Neither supplies an x or a y.

resetPose(Pose2d) moves the pose itself: x, y and heading in meters from the blue corner. It exists on the Phoenix 6 drivetrain, but DriveMechanism does not expose it and nothing calls it. Until Workshop 5, read Drivetrain/Pose as distance traveled since boot.

Zero the modules

Each module has a CANcoder reading which way its wheel points, and an offset saying which reading counts as straight ahead. The four k*EncoderOffsetconstants in your file came off somebody else's robot.

  1. Disable the robot. Nothing should be commanding a module while you set it.
  2. Press a straight edge flat along one side so both wheels on that side sit against it, then do the other side. By eye is not close enough: half a degree of steering error walks the robot 5 cm sideways over six meters.
  3. Check which way each module faces. CTRE marks this one important: every module's bevel gear has to face the vertical center of the robot. A module that is straight but flipped zeroes 180° off, and the drive verification tests fail later without saying why.
  4. Holding the wheels straight, run the calibration step in the Tuner X swerve generator. It reads all four CANcoders where they sit and writes the offset constants for you.

Re-enable, push the left stick forward, and sight along a carpet seam. A consistent curve to one side means a module is zeroed wrong. A wander that comes and goes is the steer gains.

CTRE: Tuner X Swerve Project Generator

Tune the steer gains

A steering motor holds an angle: a position loop, and there is no gravity to fight here. Skip kG and start at kS.

The generator gave you real numbers: kP 100, kD 0.5, kS 0.1, kV 1.91. They are often close, so adjust rather than starting from zero.

The order, from the PID page. kS is the smallest output that breaks the module loose. Raise kP until it oscillates, then back off. Add kD, as much as you can get without jitter. One number per test.

Telemetry.java publishes Drivetrain/ModuleStates and Drivetrain/ModuleTargets. Put the angle from both on one AdvantageScope plot and flick the right stick. Tuned looks like two traces on top of each other. Untuned lags, overshoots, or buzzes.

Three measurements on carpet

Measure the radius and the top speed while the drive request is still open-loop voltage: kSpeedAt12Volts means the speed at 12 volts applied.

Wheel radius

Odometry counts wheel rotations and multiplies by a radius. You want the effective one: the wheel squashed under the robot's weight and sunk into carpet. It shrinks as the tread wears, so repeat this late in the season.

  1. Tape the floor at the robot's front edge. Restart the code so Drivetrain/Pose reads (0, 0).
  2. Drive straight forward, slowly, about five meters. A wheel that spins under hard acceleration counts distance the robot never travels.
  3. Tape the front edge again. The gap between marks is the actual distance, and Drivetrain/Pose at the end of the run is the reported one.
THE CORRECTION

Run it three times in each direction and average. Put the result in kWheelRadius, redeploy, and repeat until tape and log agree.

newRadius = (actualDistance / reportedDistance) × currentRadius
tape 5.00 m, log 4.80 m, file 2.167 in: (5.00 / 4.80) × 2.167 = 2.257 in

If it got worse, you inverted the ratio: a robot that under-reports needs a bigger radius.

Top speed

Do the radius first: this speed is wheel rotations times that radius. Find six meters of clear floor on the surface you compete on: carpet and a shop floor give different answers.

  1. Full stick forward. Hold until the speed stops climbing, then let go well before the wall.
  2. Plot Drivetrain/TranslationSpeedMps and read the plateau, not the spike.

That number goes in kSpeedAt12Volts. It measures the robot; the driver cap is maxSpeed in TeleopOpMode, which reads it straight back. Expect a plateau near the 4.54 the file shipped with. Wildly off means the file: check the radius, then check kDriveGearRatio against your modules.

Slip current

Stator current is proportional to torque, so a stator limit caps how hard a wheel twists. Set it where the tire loses grip. Torque above that point polishes carpet.

  1. Drive the robot against a wall on carpet and square it up so all four wheels point into the wall.
  2. In Tuner X, open one drive motor on Voltage Out and plot its velocity and its stator current.
  3. Ramp the voltage up slowly from zero, watching both traces.

Current climbs while velocity sits at zero. Then velocity jumps and current drops in the same instant. That is the tire letting go. Read the current at the top of the climb, just before the drop.

Don't

Stalled motors get hot fast

A drive motor pushing a wall it cannot move is a stalled motor. Keep each ramp to a couple of seconds, back off to zero between attempts, and give the motor a minute. One person on the disable, somebody else on the laptop.

That number goes in kSlipCurrent. You may be measuring the limit, not the tire. The shipped 120 A is itself a stator limit, and driveInitialConfigs sets 70 A on the supply. If the trace flattens at 120 A and the wheel never breaks loose, raise the constant temporarily and ramp again. Then redeploy and floor it from a dead stop. The robot should launch without the squeal and sideways hop of wheel spin.

CTRE: Preventing Wheel Slip with Current Limits

Close the drive loop

A drive motor holds a speed. On a velocity loop the feedforward does nearly all of the work. The order is kV, then kS, then kP. The file starts you at kP 0.2 and kV 0.124.

Tune it on the ground, not on blocks. A wheel in the air carries no load, and gains found there will not hold a speed under a robot.

Plot the same two signals as the steer gains, speed this time. A constant gap is kV. A gap only at low speed is kS. A slow recovery after a change of direction is kP.

Switch the drive request

Up to here the stick position went straight to volts. In TeleopOpMode.java, change DriveRequestType.OpenLoopVoltage to DriveRequestType.Velocity. It comes from the same class, so the import already covers it.

No branch makes this edit for you: 1-Swerve, 2-Logging and the robot template all ship open-loop. Make it last. If the robot drives worse than it did on volts, go back to the gains.

The deadband

The same request line sets withDeadband(maxSpeed * 0.1) and withRotationalDeadband(maxAngularRate * 0.1), throwing away the bottom 10% of both sticks. At 4.54 m/s that is everything under about 0.45 m/s, which open-loop driving could not hold anyway. A tuned velocity loop can, so the deadband now discards control you paid for.

Shrink it rather than deleting it. The deadband is what keeps a worn stick's drift from creeping the robot across the field.

  1. Put the robot on blocks. Enable, and take your hands off the controller.
  2. Watch the speed component of Drivetrain/ModuleTargets. It should be flat zero.
  3. Halve the deadband, redeploy, repeat. When the targets start twitching, go back one value.

Set it per controller, for the worst one you will compete with.

Check your work

One run tells you whether the page landed, on the surface you compete on. Tape a start mark, restart the code so Drivetrain/Pose reads (0, 0), and drive a square: three meters forward, three left, three back, three right.

Check

You should see

The robot back on the tape, and Drivetrain/Pose near (0, 0) after twelve meters. In the log, measured module traces sitting on the commanded ones through all four corners. Hands off the sticks, the speed component of Drivetrain/ModuleTargetsflat at zero. Turn 90° in place and press the left bumper: forward moves, x and y do not.

Square comes out rotated
Module zeros
A module a degree off steers the robot sideways the whole way, and no radius correction fixes a heading error. Straight edge back on, re-save the zeros.
Worse right after the switch
Drive gains
Velocity with untuned gains chases a speed the motor cannot hold. Sluggish or surging is kV. A hum at constant speed is kP too high.
Pose numbers look odd
Nothing is broken
Odometry measures from where the code started, not from a point on the field. Nothing here sets one. Vision fixes it in Workshop 5.

Check yourself

TeleopOpMode.java on 1-Swerve: what does the drive request read before you edit it?

You press the left bumper, which runs seedFieldCentric(). What changed?

Tape says 5.00 m, the log says 4.80 m, and kWheelRadius is 2.167 in. What goes in the file?

Why does the wheel radius get measured before top speed?

Current climbs while velocity sits at zero, then velocity jumps and current drops. What goes in kSlipCurrent?

With the drive loop closed, why shrink the 10% deadband instead of deleting it?

Pick an answer for each.