Extend 2-motor arcade tank drive to a 4-motor setup by adding follower motors for each side of the drivetrain. Covers SparkMax follower configuration and TalonFX Follower control.
Use this file to discover all available pages before exploring further.
Most FRC drivetrains use four or more motors — two per side — to generate the torque needed to push heavy robots across the field. This lesson extends the 2-motor arcade drive from Lesson 3 by adding a follower motor to each side. Follower motors mirror their leader’s output automatically, so your drive loop stays exactly the same while doubling the power on each side of the drivetrain.
A follower motor is a motor controller that is configured to automatically mirror the output of a designated leader motor. Instead of calling .set() on every motor individually in your drive loop, you configure the follower once during initialization and then only command the leader. The follower watches the leader’s output and applies the same duty cycle in real time.
Only the leader motors need explicit set() or setControl() calls in your drive loop. Follower motors update their output automatically whenever the leader’s setpoint changes.
Benefits of using follower motors:
Simpler drive loop — half as many set() calls
Followers stay in sync with leaders even if your code has timing irregularities
Less risk of accidentally commanding one motor without the other
Four SPARK MAX controllers share two CAN IDs per side (IDs 1–4). The left follower is configured to follow m_leftLead, and the right follower follows m_rightLead. Right-side inversion is applied to the right leader only.
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj.XboxController;import edu.wpi.first.wpilibj.drive.DifferentialDrive;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 static final int kLeftLeadID = 1; private static final int kLeftFollowID = 2; private static final int kRightLeadID = 3; private static final int kRightFollowID = 4; private SparkMax m_leftLead; private SparkMax m_leftFollow; private SparkMax m_rightLead; private SparkMax m_rightFollow; private final XboxController m_controller = new XboxController(0); private DifferentialDrive m_robotDrive; @Override public void robotInit() { m_leftLead = new SparkMax(kLeftLeadID, MotorType.kBrushless); m_leftFollow = new SparkMax(kLeftFollowID, MotorType.kBrushless); m_rightLead = new SparkMax(kRightLeadID, MotorType.kBrushless); m_rightFollow = new SparkMax(kRightFollowID, MotorType.kBrushless); SparkMaxConfig leftFollowConfig = new SparkMaxConfig(); leftFollowConfig.follow(m_leftLead); m_leftFollow.configure(leftFollowConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); SparkMaxConfig rightLeadConfig = new SparkMaxConfig(); rightLeadConfig.inverted(true); m_rightLead.configure(rightLeadConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); SparkMaxConfig rightFollowConfig = new SparkMaxConfig(); rightFollowConfig.follow(m_rightLead); m_rightFollow.configure(rightFollowConfig, ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); m_robotDrive = new DifferentialDrive(m_leftLead, m_rightLead); } @Override public void teleopPeriodic() { m_robotDrive.arcadeDrive(-m_controller.getLeftY(), m_controller.getLeftX()); }}
Four TalonFX controllers use the Follower control request for follower assignment. Right-side inversion is applied to the right leader via MotorOutputConfigs. The right follower uses opposeLeader = false because the leader already handles inversion.
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj.XboxController;import edu.wpi.first.math.MathUtil;import com.ctre.phoenix6.hardware.TalonFX;import com.ctre.phoenix6.controls.Follower;import com.ctre.phoenix6.controls.DutyCycleOut;import com.ctre.phoenix6.signals.InvertedValue;import com.ctre.phoenix6.configs.MotorOutputConfigs;public class Robot extends TimedRobot { private static final int kLeftLeadID = 1; private static final int kLeftFollowID = 2; private static final int kRightLeadID = 3; private static final int kRightFollowID = 4; private final TalonFX m_leftLead = new TalonFX(kLeftLeadID); private final TalonFX m_leftFollow = new TalonFX(kLeftFollowID); private final TalonFX m_rightLead = new TalonFX(kRightLeadID); private final TalonFX m_rightFollow = new TalonFX(kRightFollowID); private final XboxController m_controller = new XboxController(0); @Override public void robotInit() { m_leftFollow.setControl(new Follower(m_leftLead.getDeviceID(), false)); m_rightFollow.setControl(new Follower(m_rightLead.getDeviceID(), false)); var rightConfig = new MotorOutputConfigs(); rightConfig.Inverted = InvertedValue.Clockwise_Positive; m_rightLead.getConfigurator().apply(rightConfig); } @Override public void teleopPeriodic() { double forward = -m_controller.getLeftY(); double turn = m_controller.getLeftX(); double leftSpeed = MathUtil.clamp(forward + turn, -1.0, 1.0); double rightSpeed = MathUtil.clamp(forward - turn, -1.0, 1.0); m_leftLead.setControl(new DutyCycleOut(leftSpeed)); m_rightLead.setControl(new DutyCycleOut(rightSpeed)); }}
PWM controllers use WPILib’s addFollower() method to pair trailing motors with their leaders. Right-side inversion is applied to m_rightLead via setInverted(true) — the follower inherits this direction through addFollower().
import edu.wpi.first.wpilibj.TimedRobot;import edu.wpi.first.wpilibj.XboxController;import edu.wpi.first.wpilibj.drive.DifferentialDrive;import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;public class Robot extends TimedRobot { private static final int kLeftLeadPort = 0; private static final int kLeftFollowPort = 1; private static final int kRightLeadPort = 2; private static final int kRightFollowPort = 3; private final PWMSparkMax m_leftLead = new PWMSparkMax(kLeftLeadPort); private final PWMSparkMax m_leftFollow = new PWMSparkMax(kLeftFollowPort); private final PWMSparkMax m_rightLead = new PWMSparkMax(kRightLeadPort); private final PWMSparkMax m_rightFollow = new PWMSparkMax(kRightFollowPort); private final XboxController m_controller = new XboxController(0); private DifferentialDrive m_robotDrive; @Override public void robotInit() { m_leftLead.addFollower(m_leftFollow); m_rightLead.addFollower(m_rightFollow); m_rightLead.setInverted(true); m_robotDrive = new DifferentialDrive(m_leftLead, m_rightLead); } @Override public void teleopPeriodic() { m_robotDrive.arcadeDrive(-m_controller.getLeftY(), m_controller.getLeftX()); }}
Every motor controller must have a unique ID or port. In a 4-motor drivetrain, you need four distinct values. FRC Academy uses the following default assignments:CAN-based motors (NEO / Kraken):
Motor
CAN ID
Left Leader
1
Left Follower
2
Right Leader
3
Right Follower
4
PWM-based motors (CIM):
Motor
PWM Port
Left Leader
0
Left Follower
1
Right Leader
2
Right Follower
3
Duplicate CAN IDs will cause one or both controllers to behave erratically or not respond at all. Always verify your CAN IDs are unique using REV Hardware Client (for SPARK MAX) or CTRE Phoenix Tuner X (for TalonFX) before running the robot.
opposeLeader = false means the follower runs in the same direction as the leader. Set true only if the follower is physically mounted in reverse relative to the leader.CIM (PWMSparkMax) — uses WPILib’s addFollower() directly on the leader:
Launch FRC Academy and click Motors from the main menu.
2
Select a motor type
Choose NEO, Kraken, or CIM.
3
Open Lesson 4
Select 4. 4 Motor Tank Drive (Arcade) from the lesson list.
4
Choose a mode
Pick Guided Mode or Unguided Mode and build on your Lesson 3 solution.
Only the leader motors need explicit set() or arcadeDrive() calls in the drive loop. Followers update automatically — adding redundant set() calls on follower motors may cause unexpected behavior.
← 2-Motor Tank Drive
Lesson 3 — The 2-motor arcade drive foundation this lesson builds upon.
Motor Types Overview
Compare NEO, Kraken, and CIM motor controllers — hardware specs, vendor libraries, and when to use each.