Build a 2-motor arcade-style tank drive in FRC Java. Configure left and right motors, invert the right side, and bind joystick axes to forward speed and turning.
Use this file to discover all available pages before exploring further.
Arcade tank drive controls a differential drivetrain using two joystick axes — the left stick’s Y axis for forward/reverse speed and the left stick’s X axis for turning. This lesson implements a complete 2-motor arcade drive from scratch: you’ll declare a left and right motor, invert the right side so both motors push the robot in the same direction, and implement the drive loop inside teleopPeriodic().
Straight forward: turn = 0 → both sides get the same speed.
Turn right: turn > 0 → left side speeds up, right side slows down → robot arcs right.
Turn left: turn < 0 → right side speeds up, left side slows down → robot arcs left.
Spin in place: speed = 0, turn ≠ 0 → one side drives forward, the other reverse.
The left Y axis is negated (-driverController.getLeftY()) because WPILib follows joystick convention: pushing the stick forward produces a negative value. Negating it converts “up = positive,” which is the expected behavior for forward drive.
The right motor is inverted via SparkMaxConfig so that both motors push the robot forward when given a positive setpoint. The drive loop runs every cycle inside teleopPeriodic().
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj2.command.CommandXboxController;import edu.wpi.first.math.MathUtil;import com.revrobotics.spark.SparkMax;import com.revrobotics.spark.SparkLowLevel.MotorType;import com.revrobotics.spark.config.SparkMaxConfig;import com.revrobotics.spark.SparkBase.ResetMode;import com.revrobotics.spark.SparkBase.PersistMode;public class Robot extends TimedRobot { private final SparkMax m_leftMotor = new SparkMax(1, MotorType.kBrushless); private final SparkMax m_rightMotor = new SparkMax(2, MotorType.kBrushless); private final CommandXboxController driverController = new CommandXboxController(0); private final CommandXboxController operatorController = new CommandXboxController(1); @Override public void robotInit() { SparkMaxConfig rightConfig = new SparkMaxConfig(); rightConfig.inverted(true); m_rightMotor.configure(rightConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); } @Override public void teleopPeriodic() { double forward = -driverController.getLeftY(); double turn = driverController.getLeftX(); double leftSpeed = forward + turn; double rightSpeed = forward - turn; leftSpeed = MathUtil.clamp(leftSpeed, -1.0, 1.0); rightSpeed = MathUtil.clamp(rightSpeed, -1.0, 1.0); m_leftMotor.set(leftSpeed); m_rightMotor.set(rightSpeed); }}
The Kraken uses MotorOutputConfigs for inversion and DutyCycleOut control requests for motor output.
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj2.command.CommandXboxController;import edu.wpi.first.math.MathUtil;import com.ctre.phoenix6.hardware.TalonFX;import com.ctre.phoenix6.controls.DutyCycleOut;import com.ctre.phoenix6.configs.MotorOutputConfigs;import com.ctre.phoenix6.signals.InvertedValue;public class Robot extends TimedRobot { private final TalonFX m_leftMotor = new TalonFX(1, "DINO"); private final TalonFX m_rightMotor = new TalonFX(2, "DINO"); private final CommandXboxController driverController = new CommandXboxController(0); private final CommandXboxController operatorController = new CommandXboxController(1); @Override public void robotInit() { var rightConfig = new MotorOutputConfigs(); rightConfig.Inverted = InvertedValue.Clockwise_Positive; m_rightMotor.getConfigurator().apply(rightConfig); } @Override public void teleopPeriodic() { double forward = -driverController.getLeftY(); double turn = driverController.getLeftX(); double leftSpeed = forward + turn; double rightSpeed = forward - turn; leftSpeed = MathUtil.clamp(leftSpeed, -1.0, 1.0); rightSpeed = MathUtil.clamp(rightSpeed, -1.0, 1.0); m_leftMotor.setControl(new DutyCycleOut(leftSpeed)); m_rightMotor.setControl(new DutyCycleOut(rightSpeed)); }}
PWM controllers use WPILib’s setInverted() method directly on the motor object. The drive math and loop are identical to the NEO version.
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj2.command.CommandXboxController;import edu.wpi.first.math.MathUtil;import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;public class Robot extends TimedRobot { private final PWMSparkMax m_leftMotor = new PWMSparkMax(0); private final PWMSparkMax m_rightMotor = new PWMSparkMax(1); private final CommandXboxController driverController = new CommandXboxController(0); private final CommandXboxController operatorController = new CommandXboxController(1); @Override public void robotInit() { m_rightMotor.setInverted(true); } @Override public void teleopPeriodic() { double forward = -driverController.getLeftY(); double turn = driverController.getLeftX(); double leftSpeed = forward + turn; double rightSpeed = forward - turn; leftSpeed = MathUtil.clamp(leftSpeed, -1.0, 1.0); rightSpeed = MathUtil.clamp(rightSpeed, -1.0, 1.0); m_leftMotor.set(leftSpeed); m_rightMotor.set(rightSpeed); }}
On a differential drivetrain, the left and right motors face opposite directions — if you send both the same positive setpoint, the robot will spin in place instead of driving straight. Inverting one side corrects this so a positive value always means “push the robot forward.”The method for inverting differs by motor type:NEO (SparkMax) — Configuration APIREV SPARK MAX uses the SparkMaxConfig object. You create a config, call .inverted(true), then apply it with .configure():
SparkMaxConfig rightConfig = new SparkMaxConfig();rightConfig.inverted(true);m_rightMotor.configure(rightConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters);
ResetMode.kResetSafeParameters clears previous settings before applying. PersistMode.kPersistParameters saves the configuration to the controller’s flash memory so it survives a power cycle.Kraken (TalonFX) — MotorOutputConfigsCTRE Phoenix 6 uses a MotorOutputConfigs object applied via the device configurator:
var rightConfig = new MotorOutputConfigs();rightConfig.Inverted = InvertedValue.Clockwise_Positive;m_rightMotor.getConfigurator().apply(rightConfig);
CIM (PWMSparkMax) — setInverted()PWM controllers use WPILib’s simpler setInverted() method directly on the motor object:
Launch FRC Academy and click Motors from the main menu.
2
Select a motor type
Choose NEO, Kraken, or CIM.
3
Open Lesson 3
Select 3. 2 Motor Tank Drive (Arcade) from the lesson list.
4
Choose a mode
Pick Guided Mode or Unguided Mode and start coding.
Always test your drivetrain direction on the ground (or with the robot elevated) before a match. If the robot spins in place when you push the stick forward, your inversion is backwards. If it drives in the wrong direction entirely, check which side you inverted.
4-Motor Tank Drive →
Lesson 4 — Add follower motors to each drivetrain side for a full 4-motor arcade drive setup.
← Activate a Motor
Lesson 2 — Bind a controller button to run a single motor at 50% speed.