Field-Oriented Controls
One method call separates robot-oriented from field-oriented. ChassisSpeeds.fromFieldRelativeSpeeds() is six words and nine parameters, but it unlocks the single most powerful competitive advantage in FRC driving. This lesson explains the math behind it, upgrades the DriveCommand from Lesson 8 with a one-line change, and covers everything that can go wrong — and how to handle it.
By the end of this lesson, you will:
- Explain why field-oriented control reduces driver cognitive load and produces faster cycle times
- Describe the 2D rotation matrix that
fromFieldRelativeSpeeds()applies internally - Upgrade the Lesson 8
DriveCommandto field-oriented with the single required change - Implement a gyro-failsafe toggle that switches between field-oriented and robot-oriented at runtime
- Handle alliance color flipping so field-oriented "forward" always means toward the opponent wall
- Explain the gyro zero button's role as the most critical driver interface element on a swerve robot
The Competitive Case for Field-Oriented
In a real match, the robot gets hit. It spins during a defensive engagement, ends up sideways after a collision, gets pushed off-target during an intake cycle. With robot-oriented control, every time this happens, the driver's mental model breaks: "forward" now points somewhere wrong. Under competition adrenaline and time pressure, the driver hesitates, miscorrects, wastes a half-second. Against an alliance running field-oriented, that hesitation is the difference between a completed cycle and a missed shot.
Field-oriented control eliminates this entirely. The joystick always means the same thing: toward the far wall, away from the far wall, left across the field, right across the field. Robot heading becomes irrelevant to the driver's translation decisions. They think in field coordinates — the natural coordinate system of a player looking down at the field — and the code handles the rotation.
Watch a driver at competition for five seconds. If the robot hesitates after a collision or drives at an angle when they push straight, that's robot-oriented. If the robot snaps back to the driver's intended direction immediately after a hit, that's field-oriented. The difference isn't visible in the code — it's visible in the driver's confidence. 2910 drivers don't think about the robot's heading during TeleOp. They think about the field. That's the mental space field-oriented control buys them, and it compounds into real cycle time advantages over a match.
The Rotation Matrix: One Paragraph of Geometry
Field-oriented control is 2D vector rotation. The driver's joystick produces a velocity vector in field coordinates — (vx_field, vy_field). The robot's current heading θ is known from the gyro. To produce the same physical field-relative motion, the robot must be commanded with that vector rotated by −θ into the robot's reference frame. This is a standard 2D rotation:
2D rotation transformation — field → robot frame
You don't need to implement this math yourself. ChassisSpeeds.fromFieldRelativeSpeeds() applies it internally. But understanding it is what lets you debug when field-oriented goes wrong — if the robot curves instead of going straight, θ is wrong. If axes are mirrored, θ has the wrong sign. The math is the map to the bug.
Field-Oriented vs. Robot-Oriented: The Difference Made Visible
Select a robot heading below. The joystick is always pushed forward (+vx in field coordinates). Watch how the module states and the resulting robot motion differ between the two control modes.
The One-Line Change: Robot-Oriented → Field-Oriented
This is the complete transformation of the Lesson 8 DriveCommand into a field-oriented drive command. Every line is identical except the ChassisSpeeds construction.
// ── Old: robot-oriented ──────────────────────────────
−m_drive.drive(new ChassisSpeeds(vx, vy, omega));
// ── New: field-oriented ───────────────────────────────
+m_drive.drive(ChassisSpeeds.fromFieldRelativeSpeeds(vx, vy, omega, m_drive.getHeading()));
That's the complete change. One constructor call replaced with one static factory call that takes an additional argument: the robot's current heading from the gyro. The rest of the pipeline — discretize(), toSwerveModuleStates(), desaturateWheelSpeeds(), four setDesiredState() calls — is identical. The architecture held exactly as designed.
The Complete FieldOrientedDriveCommand
private final SwerveDrivetrain m_drive;
private final CommandXboxController m_controller;
// Failsafe: toggle to robot-oriented if gyro unreliable
private boolean m_fieldOriented = true;
public FieldOrientedDriveCommand(SwerveDrivetrain drive,
CommandXboxController controller) {
m_drive = drive;
m_controller = controller;
addRequirements(drive);
}
@Override
public void execute() {
double vx = -MathUtil.applyDeadband(m_controller.getLeftY(), 0.05)
* Constants.Swerve.kMaxSpeedMetersPerSecond;
double vy = -MathUtil.applyDeadband(m_controller.getLeftX(), 0.05)
* Constants.Swerve.kMaxSpeedMetersPerSecond;
double omega = -MathUtil.applyDeadband(m_controller.getRightX(), 0.05)
* Constants.Swerve.kMaxAngularRadPerSecond;
ChassisSpeeds speeds;
if (m_fieldOriented) {
speeds = ChassisSpeeds.fromFieldRelativeSpeeds(
vx, vy, omega, m_drive.getHeading());
} else {
// Robot-oriented fallback — no gyro needed
speeds = new ChassisSpeeds(vx, vy, omega);
}
m_drive.drive(speeds);
// Publish active mode for driver awareness
SmartDashboard.putBoolean("Drive/FieldOriented", m_fieldOriented);
}
/** Called by a button binding to toggle the control mode */
public void toggleFieldOriented() {
m_fieldOriented = !m_fieldOriented;
}
@Override
public void end(boolean interrupted) { m_drive.stop(); }
@Override
public boolean isFinished() { return false; }
}
Wiring in RobotContainer
new FieldOrientedDriveCommand(m_drive, m_driver);
private void configureDefaultCommands() {
m_drive.setDefaultCommand(m_driveCmd);
}
private void configureButtonBindings() {
// Zero gyro heading — most important button on the robot
m_driver.start().onTrue(
Commands.runOnce(m_drive::zeroHeading, m_drive)
.withName("ZeroHeading"));
// Gyro failsafe: Back button toggles field/robot oriented
m_driver.back().onTrue(
Commands.runOnce(m_driveCmd::toggleFieldOriented)
.withName("ToggleDriveMode"));
// Lock wheels: X button hold (from Lesson 8)
m_driver.x().whileTrue(
Commands.run(m_drive::lockWheels, m_drive)
.withName("LockWheels"));
}
Alliance Color Flipping
WPILib's field coordinate system places the origin at the blue alliance wall. "Forward" in field coordinates (+X) always means toward the red alliance wall. For blue alliance drivers, this is natural — the driver is standing at the blue wall, and joystick-forward means toward the opponent.
For red alliance drivers, the driver is standing at the red wall. Joystick-forward should still mean toward their opponent — which is now the blue wall, the −X direction. Without a flip, red alliance drivers experience the field coordinates backward: pushing forward drives the robot away from the opponents instead of toward them.
The fix is a sign flip on the field-relative vx and vy components when on the red alliance:
return DriverStation.getAlliance()
.map(a -> a == Alliance.Red)
.orElse(false);
}
// In execute(), apply flip before fromFieldRelativeSpeeds:
double allianceFlip = isRedAlliance() ? -1.0 : 1.0;
double vxField = vx * allianceFlip;
double vyField = vy * allianceFlip;
ChassisSpeeds speeds = ChassisSpeeds.fromFieldRelativeSpeeds(
vxField, vyField, omega, m_drive.getHeading());
m_drive.drive(speeds);
// Heading zero must also account for alliance at match start.
// Red alliance: robot faces the blue wall → zeroHeading sets
// this as 0°. FO control then treats blue wall as "forward".
// The flip corrects this so joystick-fwd still means opponent.
PathPlanner's AutoBuilder.configure() accepts an alliance color supplier that it uses to mirror paths for the red alliance. For path-following autonomous, you don't need to apply the flip manually — the library does it. The flip described above is only needed for the driver-controlled TeleOp period. Double-check you're not applying the flip twice: once in the drive command and once in PathPlanner.
The Gyro Zero Button: Most Important Driver Control
The gyro zero button is not optional. It is not a nice-to-have. It is the recovery mechanism for every field-oriented failure mode.
Field-oriented control depends entirely on the gyro knowing which direction the field is. At match start, the robot is placed at a known orientation and the gyro is zeroed to that orientation. This works perfectly — until a defense collision spins the robot and the driver's spatial reference drifts slightly, or a gyro initialization issue means the starting heading is subtly wrong, or the driver simply loses track after a fast rotation.
The gyro zero button tells the robot: "My front is now facing the correct forward direction. Reset your reference." After pressing it, field-oriented control is re-anchored. Every match, the 2910 driver protocol includes: at the start of TeleOp, before touching the joystick, press Start to zero the gyro with the robot facing its current orientation. This takes 0.2 seconds and prevents the most common source of field-oriented failure.
Mistake 1: Zeroing the gyro during robot motion. zeroHeading() calls m_pigeon.setYaw(0.0). If the robot is spinning when this fires, the zeroed heading is whatever random angle the robot happened to be at that moment. The gyro zero should always fire when the robot is stationary — use an .onTrue() binding so it fires once on button press, not .whileTrue() which would repeatedly zero while the button is held.
Mistake 2: Using zeroHeading() for odometry reset. Zeroing the gyro changes the heading reference but does NOT reset the Pose2d — after calling zeroHeading, the odometry's heading becomes inconsistent with its position. For odometry-accurate reset, call drivetrain.resetPose(new Pose2d(x, y, new Rotation2d())) which resets both heading and position together.
After upgrading from robot-oriented (Lesson 8):
- Basic forward test: Hold the robot at 0° heading. Push joystick forward — robot drives toward the red wall. Rotate robot 90° by hand (don't zero gyro). Push joystick forward again — robot should still drive toward the red wall, not toward the right wall. If it drives in the direction the robot is facing, field-oriented is not active.
- Heading zero test: Rotate robot 90°. Press Start. Drive forward — robot should still drive toward the red wall (not where it's now facing). The heading zero only redefines which direction is "zero degrees" — it doesn't change the physical robot position.
- Alliance flip test: Set
DriverStation.getAlliance()to return Red (or use the DS alliance selector). Push joystick forward — robot should drive toward the blue wall. If it drives toward the red wall, the flip logic is not active for red alliance. - Failsafe toggle test: Press Back button to toggle to robot-oriented. Push joystick forward — robot drives in the direction its front is pointing. Press Back again — robot switches back to field-oriented. Confirm SmartDashboard's Drive/FieldOriented boolean matches the active mode.
Knowledge Check
1. The robot is facing 90° (pointing toward the right wall). The driver pushes the left stick forward (vx_joy = 1.0). In field-oriented mode, what ChassisSpeeds values are passed to drive()?
2. The gyro is working but the heading was never zeroed at match start — it powers on reporting 47°. The driver pushes forward. What does the robot do?
3. Mid-match, the Pigeon 2 encounters a hardware fault and begins reporting random yaw values that change rapidly. Without the gyro failsafe toggle, what is the robot's behavior in field-oriented mode?
Field-Oriented Drive — Full Competition Build
- Upgrade your
DriveCommandby replacing thenew ChassisSpeeds(vx, vy, omega)call withChassisSpeeds.fromFieldRelativeSpeeds(vx, vy, omega, m_drive.getHeading()). Deploy and run the field-oriented verification protocol from the System Check above. Confirm all three directions (forward, strafe, rotation) behave correctly regardless of the robot's heading. - Add the
m_fieldOrientedtoggle field and thetoggleFieldOriented()method. InRobotContainer, bind the Back button to aCommands.runOnce(m_driveCmd::toggleFieldOriented). Test: with the robot rotated 45°, push forward in field-oriented mode → drives toward opponent wall. Toggle to robot-oriented → drives in the direction the robot is facing. Toggle back → field-oriented resumes. - Add the alliance color flip. Test both alliance colors by forcing
DriverStation.getAlliance()to return Red (you can do this with the DS Alliance selector during testing). Confirm pushing forward on red alliance drives toward the blue wall, not the red wall. - Bind the gyro zero to Start (if not already done). Practice the full competition startup sequence: robot placed in starting position facing forward → enable → press Start → confirm
Drive/HeadingDegreads 0 → drive forward → confirm the robot goes the right direction. Do this five times until it's muscle memory for your driver. - Stretch goal: Add a
RateLimiterto the omega input to smooth the rotation response: declareprivate final SlewRateLimiter m_rotLimiter = new SlewRateLimiter(Constants.Swerve.kMaxAngularRadPerSecond)and replaceomegawithm_rotLimiter.calculate(omega)in the execute method. Compare driving with and without the rate limiter — the limiter prevents sudden heading jerks and makes field-oriented feel more predictable at competition. This is a production quality-of-life feature on 2910 robots.