Skip to content
FRC Article 13 min read

FRC Swerve Module Offsets: Calibration & Backwards Wheels

Zero your FRC swerve module offsets correctly, and fix wheels that spin backwards, modules that fight each other, and field-relative drive that feels rotated.

to read
13 min

to read

words
4,112

words

sections
9

sections

A swerve module offset is one number per module: the value your absolute encoder reports when that module's wheel is physically pointed at whatever direction you decided to call zero. That's the entire concept. The magnet on your steering shaft landed wherever it landed during assembly, so the encoder's raw zero is arbitrary. The offset is the constant that turns "raw encoder reading" into "which way this wheel is actually facing."

Four good offsets and swerve behaves. One wrong by 90° and that corner scrubs and fights the other three every time you move. One wrong by 180° and the module looks perfect sitting still, then drives backwards. The reason teams burn entire practice sessions on this is that three completely different bugs — a bad offset, a flipped inversion, and a module-order mismatch — produce overlapping symptoms, so guessing is expensive.

Work in this order every time and you'll find it fast: (1) prove each module reports the angle it's actually pointing, (2) prove a positive drive command moves that wheel forward, (3) prove your module order and module locations are right, (4) only then touch the gyro and field-relative code. Everything below follows that order, with the vendor-specific traps for CTRE, REV, and Thrifty hardware at the end. If you're still getting oriented on how the whole drivetrain works, start with swerve drive explained and come back.

What an offset actually is (and what it isn't)

Your absolute encoder measures azimuth — the rotation of the steering axis — and it does so without needing to be homed. It reports the same value at the same physical angle every boot. That's the whole point of using one.

What it reports at "wheel straight ahead" is arbitrary, and that's what you're calibrating. Every vendor expresses it slightly differently, and mixing up the units or the sign is the single most common way this goes wrong:

  • CTRE CANcoder reports position in rotations. MagnetSensorConfigs.MagnetOffset is documented as: "This offset is added to the reported position, allowing the application to trim the zero position." Range −1 to 1 rotations, default 0.
  • REV SPARK with a Through Bore Encoder reports rotations natively. AbsoluteEncoderConfig.zeroOffset(double) takes a value in [0, 1) and is "specified as the reported position of the encoder in the desired zero position as if the zero offset was set to 0."
  • Thrifty Absolute Magnetic Encoder is read by WPILib's AnalogEncoder class, which returns a value scaled by whatever fullRange you passed to the constructor (default 1).

Note the sign trap right there in CTRE's own wording: MagnetOffset is added to the reported position. So if a CANcoder reads 0.152 rotations with the wheel aligned, the offset you write to the device is −0.152, not +0.152. Meanwhile a code-side template that does angle = rawReading - offset wants you to store +0.152. Same physical situation, opposite sign. Half the "my offsets did nothing" posts on Chief Delphi are this.

One more thing an offset is not: it is not a fix for a wrong steering gear ratio. If your steer motor's internal encoder conversion is wrong — MK4i steering is 150/7:1, MK4 is 12.8:1, Thrifty Swerve azimuth is 25:1 — the module will consistently overshoot or undershoot its target and look exactly like a drifting offset. Confirm the ratio before you start chasing numbers.

The zeroing procedure

This works on any module from any vendor. The details of how you read the encoder change; the sequence doesn't.

1. Pick a convention and write it on the whiteboard. The two you'll encounter in real templates are genuinely different:

  • All bevels the same way. Team 364's widely-copied TalonFX template says: "Point the bevel gears of all the wheels in the same direction (either facing left or right), where a positive input to the drive motor drives the robot forward." YAGSL agrees: "Wheels should be aligned with the bevels facing the same way to get the absolute encoder offset."
  • Bevels toward the robot's centerline. CTRE's Tuner X Swerve Project Generator says: "It's extremely important for the modules to be aligned such that the bevel gear faces the vertical center of the robot." This is why CTRE's generated TunerConstants.java carries two separate drive inverts — in the published example, kInvertLeftSide = false and kInvertRightSide = true.

Neither is more correct. What is not optional is that your physical alignment matches the convention your template assumes. Follow the diagram in the wizard you're actually using.

2. Zero out the offsets already in your code and deploy. If you skip this you'll measure an offset relative to an offset and get a number that's wrong by exactly your old value. Team 364's README is explicit that this must be redone every time you determine offsets.

3. Align the modules physically, and use something rigid. Eyeballing gets you within maybe 5°, which is enough to scrub. Team 364's instruction is to "use a piece of 1x1 metal that is straight against the forks of the front and back modules." A long piece of aluminum extrusion or a machinist's straightedge across the fork faces of both left modules, then both right modules, then diagonally, is worth the two minutes. If your module has a purpose-built fixture (MAXSwerve's calibration tool, EasySwerve's hex-key slot), use it — it's more repeatable than any straightedge.

4. Read the encoder with the robot disabled. Push the reading to your dashboard, or use the vendor client (Phoenix Tuner X, REV Hardware Client). Write down all four to a few decimal places. Team 364 suggests two decimal places in degrees, which is about 0.01° — far finer than you can align by hand, so don't agonize over the last digit.

5. Store them with the correct sign for your setup (see the trap above), and deploy or write to the device.

6. Verify by undoing it. Rotate all four modules to random angles, then re-align them to your fixture by hand and confirm each one now reports approximately zero, not the raw number. If it doesn't, you have a sign error or your offsets aren't actually being applied.

For the code side of this — how offsets slot into a module class, PID setup, and continuous input — see the FRC swerve code tutorial.

Storing offsets on the device vs. in code

Both are fine. Mixing them is not.

On the device (CANcoder MagnetOffset set through Tuner X, SPARK zeroOffset set through REV Hardware Client) survives a code redeploy and is what the vendor wizards do by default. The cost: it isn't in version control, it doesn't survive swapping a controller or encoder, and nobody reviewing your repo can see it.

In code (a constants file, applied by your module class) is in git, reviewable, and diffable. The cost: it isn't live until someone deploys, and a robot that boots the wrong branch drives wrong.

The trap when you combine them: Phoenix 6's swerve API overwrites the device config. CTRE documents that for the azimuth encoder, MagnetSensorConfigs.MagnetOffset is overwritten by SwerveModuleConstants.EncoderOffset, and MagnetSensorConfigs.SensorDirection is overwritten by EncoderInverted. So if you carefully zero a CANcoder in Tuner X and then deploy a project whose TunerConstants still has last year's offsets, the code silently wins and your calibration evaporates. CTRE also warns that "if an encoder has been reassigned to a new module, users should perform encoder calibration."

REV splits it differently, and it's worth understanding because it confuses people: the MAXSwerve template stores the absolute encoder zero offset on the SPARK (the README says to calibrate it "in Hardware Client 2 using the Absolute Encoder utility"), and keeps a separate chassisAngularOffset in code. Those are two different things. More on that below.

Pick one home for the offset, write it in your team's build notes, and make sure the other one is left at its default.

"The wheels spin backwards": drive inversion or a 180° offset?

Here's the part almost nobody explains properly.

If you're calling optimize() on your module states — and you should be; WPILib 2026's SwerveModuleState has an instance method optimize(Rotation2d currentAngle) that "minimizes the change in heading this swerve module state would require by potentially reversing the direction the wheel spins" — then a 180° offset error and an inverted drive motor produce the identical visible symptom.

Walk it through. Say your reported angle is 180° off from physical. You command "point forward." Optimize compares your desired 0° against a reported 180°, sees a half-turn of error, flips the target to 180° and negates the wheel speed. The steering controller is already there, so the module doesn't move — and the wheel spins the wrong way. Wheel visually straight, robot drives backwards. Exactly what a flipped drive motor looks like.

How to tell them apart in ten seconds: hold or push that wheel until it's physically pointed straight forward with the bevel gear on the side your convention demands. Now read the module's reported angle on your dashboard. If it says ~0°, your offset is fine and you have a drive inversion. If it says ~180°, your offset is 180° out. That's it — you're reading the sensor, not inferring from behavior.

Why bother fixing the offset instead of just flipping the drive invert? Because the two are only mathematically equivalent in isolation. Three ways it bites:

  • On a CTRE-generated project the drive invert is a per-side constant. Flipping kInvertRightSide to fix one module breaks the other module on that side.
  • Your dashboard and AdvantageScope module visualizations will show wheels pointing the opposite direction from reality, which makes every future debugging session harder.
  • The next person who runs the vendor calibration wizard — including you, in three weeks, after replacing a bearing — will re-zero the encoder correctly and the compensating invert will suddenly make that corner wrong.

Related but distinct: a module that spins continuously and never settles is not an offset problem at all. YAGSL states it plainly: "When the inversion state of your motor controller is incorrect for your steering/angle/azimuth the Swerve Module WILL spin out of control when any input is given and sometimes even at rest." That's a steering polarity problem — the motor is driving the error larger. CTRE's Tuner X validation checks exactly this with its Verify Steer test, where "the modules should rotate counter-clockwise" when viewed from above; Phoenix's SwerveModuleConstants.SteerMotorInverted is documented as "the azimuth should rotate counter-clockwise (as seen from the top of the robot) for a positive motor output."

One module fights the others: module order and Translation2d signs

If all four modules point where the dashboard says they point, and a positive drive command moves each wheel forward, but the robot still misbehaves, stop looking at the modules. Look at your kinematics.

WPILib's robot coordinate frame is NWU: "the positive X axis points ahead, the positive Y axis points left, and the positive Z axis points up referenced from the floor," with CCW rotation positive. So for a square drivebase:

// front-left, front-right, back-left, back-right
new Translation2d( wheelBase / 2,  trackWidth / 2),
new Translation2d( wheelBase / 2, -trackWidth / 2),
new Translation2d(-wheelBase / 2,  trackWidth / 2),
new Translation2d(-wheelBase / 2, -trackWidth / 2)

SwerveDriveKinematics is blunt about ordering: "The order in which you pass in the module locations is the same order that you will receive the module states when performing inverse kinematics," and "it is also expected that you pass in the module states in the same order when calling the forward kinematics methods." Your kinematics array, the array you apply to hardware, and the SwerveModulePosition[] you feed odometry must all be in the same order. If your kinematics is FL-FR-BL-BR and your subsystem applies states in FL-BL-FR-BR order, two corners are permanently receiving each other's commands.

The diagnostic that saves you an hour: pure translation asks all four modules for the same angle, regardless of where they sit on the robot. Module locations only matter when there's rotation in the command. So:

  • Drives cleanly in every direction, but spinning in place scrubs, shudders, or drifts sideways → module order or Translation2d signs. Not offsets.
  • Spinning in place is smooth, but driving straight scrubs or veers → offsets, drive inverts, or one module's steering not tracking.

A subtler one: if you enter track width and wheelbase in inches where meters are expected, the directions stay correct because the geometry scales uniformly — the robot just rotates at the wrong rate and your odometry is garbage. It won't scrub. So "correct-looking wheels, wildly wrong rotation speed" points at units, not signs. Also measure track width and wheelbase between wheel contact-patch centers, not frame rail edges. If you're laying out a new chassis, the swerve drivebase layout worked example walks through where those dimensions come from.

Field-relative feels rotated

Field-relative failures never involve the modules. Four causes, in order of how often they're the answer:

The gyro was zeroed while the robot wasn't facing the right way. ChassisSpeeds.fromFieldRelativeSpeeds documents its robotAngle parameter as "the angle of the robot as measured by a gyroscope. The robot's angle is considered to be zero when it is facing directly away from your alliance station wall." If you zero the gyro on the cart with the robot sideways, everything is rotated by exactly that amount. Zero it deliberately, pointed downfield, or set the yaw from a known starting pose.

The gyro sign is backwards. WPILib warns directly: "Some gyroscope and IMU models use CW positive rotation, such as the NavX IMU. Care must be taken to handle rotation properly, sensor values may need to be inverted." WPILib expects CCW-positive. Symptom: field-relative works perfectly until you rotate, then the drive direction rotates away from where you wanted at double rate.

The gyro is mounted rotated or on its side. Fix it in configuration rather than in math. Pigeon 2 has MountPoseConfigs with MountPoseYaw (−360 to 360 degrees, default 0) alongside pitch and roll, so a Pigeon bolted 90° off or standing on edge can be corrected once at the device.

Alliance flip. WPILib recommends the always-blue-origin field convention, where the origin sits at the blue alliance wall regardless of your alliance, and notes that for red alliance operation "driver inputs require inversion to maintain intuitive control." A red-alliance driver pushing the stick away wants the robot to move toward the blue wall, which is −X in field coordinates. Read DriverStation.getAlliance() — it returns Optional<DriverStation.Alliance> and is empty when the alliance is invalid, which includes the moments before the driver station has reported one. Read it at autonomous init or on every enable, not once in a subsystem constructor, or you'll cache an empty value and run the whole match unflipped.

Three drills that actually verify the calibration

Do these on blocks first, then on the floor, before anyone tries a full-speed lap.

Drive straight. Tape a line on the floor, line the robot up, drive forward at maybe 30% for three meters, then backward over the same path. Watch the wheels, not the robot: any module that visibly points a few degrees off from the others has a stale offset. YAGSL is right that this matters competitively — "sometimes if the offset is off just a little bit a module will be dragged which could result in penalties." A dragged wheel also sounds different. Listen.

Spin in place. Rotation stick only. The robot should rotate about its geometric center with no translation and no shudder. Wheels should form a clean tangential pinwheel. If the robot walks across the floor while spinning, or one corner sounds like it's being dragged, that's module locations or module ordering.

Strafe. Pure left, pure right, at low speed, holding heading. All four wheels should snap to 90° and stay there. Then do it while slowly rotating — this is the drill that exposes a steer motor that can't keep up, which looks like an offset problem but is really PID or a current limit.

Two more worth adding once those pass: push the disabled robot two meters by hand and confirm odometry reports roughly two meters in the right direction, and take a slow-motion phone video of all four modules during a hard direction change so you can see which one lags. If you want the mechanical background on why a scrubbing module chews wheels and loses you matches, the swerve drivetrain build guide covers the hardware side.

Vendor-specific gotchas

CTRE — CANcoder and the Tuner X wizard

The CANcoder's absolute reading wraps according to AbsoluteSensorDiscontinuityPoint, documented as "the positive discontinuity point of the absolute sensor in rotations. This determines the point at which the absolute sensor wraps around, keeping the absolute position (after offset) in the range [x−1, x)." It defaults to 0.5, so out of the box your absolute position lives in [−0.5, 0.5) rotations, which is why CTRE's published example offsets look like Rotations.of(-0.4873046875). If you're expecting 0-to-1 and seeing negative numbers, that's why — nothing is broken.

Also know the difference between the two signals. getPosition() is "initialized to the absolute position on boot" and then accumulates across full turns. getAbsolutePosition() is "affected by the MagnetSensor configs and the user-set position via setPosition()" and stays within one rotation. Calibrate against the absolute signal. As a practical note, the documented AbsolutePosition range tops out at 0.999755859375 rotations, i.e. steps of 1/4096 rotation, roughly 0.088° — so there's no point recording an offset to five decimal places of a degree.

One more: CTRE warns that during the Tuner X module setup "the devices are factory defaulted" to make the drive and steer tests accurate, and advises backing up configs first. Don't run the wizard the night before a competition with tuned gains you haven't exported.

REV — MAXSwerve has two different offsets

This is the one that trips up teams switching from SDS. MAXSwerve uses two independent corrections and they do different jobs.

The absolute encoder zero offset lives on the SPARK. You set it with the physical calibration tool and REV Hardware Client: "The Calibration Tool needs to be placed on the MAXSwerve module with the lip facing the module," "the MAXSwerve Wheel will only fit in one orientation because of the placement of the wheel's bevel gear," "align the bevel gear with the side of the cutout indicated with the orange dot," then "click the Set Zero Offset button to calibrate the zero position of the absolute encoder to this position." (EasySwerve uses a 1/8" or 3 mm hex key into a calibration point and a "Zero Encoder" button instead.) Note this is done through the steering motor controller over USB-C, since the Through Bore Encoder plugs into it.

The chassisAngularOffset lives in code and exists because the four module bodies are mounted at different rotations on the chassis. REV's Java template uses -Math.PI / 2 for front-left, 0 for front-right, Math.PI for back-left, and Math.PI / 2 for back-right. The module class adds it to the desired angle and subtracts it from the reported angle. If you copy someone else's MAXSwerve constants without checking how their modules are physically clocked, you'll inherit a 90° error per corner that no amount of re-zeroing will fix.

Two smaller ones: the template configures the turning encoder with .inverted(true) because the encoder turns opposite the azimuth, and enables positionWrappingEnabled(true) with an input range of 0 to 2π so the closed loop takes the short way around. Both are easy to lose when you refactor.

Thrifty

The Thrifty Absolute Magnetic Encoder ships with a 3-wire PWM-style cable, but the connector being PWM-style doesn't make the signal PWM. WPILib's encoder documentation lists it under analog encoders, alongside the Team 221 Lamprey2 and US Digital MA3 — so read it with AnalogEncoder, not DutyCycleEncoder. Use AnalogEncoder(int channel, double fullRange, double expectedZero) to bake in your units and zero, get() to read it, setInverted(boolean) to fix direction, and setVoltagePercentageRange(double min, double max) if readings are noisy near the ends of travel.

There's no vendor wizard writing an offset onto the device the way CTRE and REV have, so on a Thrifty module the offset is a code-side constant by default. That's arguably the cleanest arrangement, since it lives in git — just be disciplined about redeploying after you change it. The encoder also connects to a SPARK MAX or Talon SRX rather than the roboRIO if you'd rather not eat analog channels.

A note on MK4i

Swerve Drive Specialties advertises the MK4i's encoder as an "on-axis steering encoder (zero backlash, module can be disassembled/reassembled without resetting encoder offset)." That's real and it's a genuine maintenance advantage — but it only holds if the magnet and encoder board stay in their fixed relationship to the azimuth shaft. If you pull the encoder board, replace the magnet, or swap in a different CANcoder, re-zero. For how the different module families compare on this and everything else, see FRC swerve modules compared.

Frequently asked questions

Do I have to redo my swerve offsets after taking a module apart?**

It depends entirely on whether the magnet's relationship to the azimuth shaft changed. On an MK4i, Swerve Drive Specialties says the module "can be disassembled/reassembled without resetting encoder offset" because the encoder is on-axis — pulling the wheel or drive gears doesn't disturb it. But any time you replace the magnet, unbolt the encoder board, swap the CANcoder or SPARK, or reassemble the steering shaft in a different clock position, you re-zero that module. CTRE says this explicitly for reassignments: "failure to perform encoder calibration will lead to unexpected module behavior." When in doubt, re-run the two-minute alignment check — align by hand and confirm the module reports ~0°.

Why does one swerve module spin continuously and never stop?

That's a steering polarity problem, not an offset. YAGSL puts it bluntly: an incorrect steering motor inversion means "the Swerve Module WILL spin out of control when any input is given and sometimes even at rest." The controller is driving the error bigger instead of smaller, so it never converges. Fix the steer motor's inverted flag — Phoenix documents the correct state as "the azimuth should rotate counter-clockwise (as seen from the top of the robot) for a positive motor output" — and verify the absolute encoder counts up in the same direction, since an inverted encoder produces the identical runaway.

Should I store swerve offsets in degrees, radians, or rotations?

Store them in whatever unit the API you're writing them to expects, and label the constant with the unit. CANcoder MagnetOffset is rotations, range −1 to 1. REV's AbsoluteEncoderConfig.zeroOffset is [0, 1) in native rotations, though the MAXSwerve template then applies a conversion factor so downstream code sees radians. Team 364-style templates use Rotation2d.fromDegrees(...). Mixing units is a top-three cause of "my offsets did nothing" — a value of 0.25 meaning a quarter turn versus 0.25 degrees is a 89.75° error.

How precise do swerve offsets need to be?

About a degree is plenty, and you physically cannot align by hand much better than that anyway. The CANcoder's absolute position signal quantizes to roughly 0.088° (1/4096 rotation), so recording more precision than that is theater. What matters is that all four are consistent — a single module a few degrees off will scrub the whole match, and YAGSL notes that a dragged module "could result in penalties." If you're seeing errors larger than about 5°, that's not a precision problem, that's a sign error or a template mismatch.

My robot drives straight fine but odometry drifts badly — is that my offsets?

Usually not. If straight-line driving looks clean and spin-in-place looks clean, your offsets and inverts are fine. Odometry drift on a well-calibrated swerve is normally wheel diameter (measure the actual worn tread, not the nominal 4 inches), drive gear ratio, or unit errors in track width and wheelbase — remember that scaling module locations uniformly leaves the wheel directions correct while wrecking the rotation-to-distance mapping. Confirm by pushing the disabled robot a measured distance and comparing what odometry reports.

**Can I calibrate offsets without the vendor's calibration tool?

Yes. The tool just makes alignment repeatable. A rigid straightedge — Team 364 recommends "a piece of 1x1 metal that is straight against the forks of the front and back modules" — held across the fork faces of both left modules, then both right modules, then diagonally corner to corner, gets you well inside a degree. Do it with the robot on its side or on blocks so the modules turn freely, and always zero out your existing offsets and deploy before measuring, or you'll measure an offset relative to an offset.

Spot an error or something out of date?Create a free account to suggest an edit

Go deeper

Keep building — the full course is free

This article pairs with a full structured design course — swerve layout, assemblies, worked mini-projects. A free account saves your progress lesson by lesson.

Save my progress

Keep reading

More from the pit

Start learning FRC — free

Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.

394lessons
11departments
100%free