498
Control Lab FRC Programming Curriculum
Robot Code · L02 of 12

Motors and Motor Controllers

Prereqs: robot-code-01
Objectives 0 / 5

Hook

Your drivetrain code compiles, deploys, and the robot moves — then a wheel starts spinning the wrong way. You flip the sign in your set() call. Now one side goes backward when it should go forward. You flip another. Half an hour later your code is a tangled mess of minus signs.

This happens to every team that doesn’t understand motor controller configuration. In this lesson you’ll learn how to configure motors correctly from the start: how to pick the right class, how to invert cleanly, and how to add safety features so a stuck mechanism doesn’t fry a motor or trip a breaker mid-match.

Core concept

Key Concept

A motor controller sits between the robot’s power distribution and the motor itself. Your code tells the controller what to do (speed, voltage, position) and the controller handles the high-current switching. WPILib and vendor libraries give you Java classes that talk to these controllers over CAN bus or PWM.

Brushed vs brushless motors

Brushed motors (CIM, MiniCIM, 775pro, BAG) use physical brushes to switch current through the windings. They’re simple, cheap, and robust — and they wear out over time because the brushes erode. Any motor controller that can supply the right voltage/current can drive them.

Brushless motors (NEO, NEO 550, Falcon 500/Kraken X60) have no brushes. The controller itself manages the commutation electronically, which means the controller and the motor are a tightly coupled pair. A NEO requires a SPARK MAX. A Falcon requires a TalonFX. You cannot mix brands.

PropertyBrushedBrushless
ExamplesCIM, 775proNEO, Falcon 500
Controller pairingAny compatibleMust match vendor
EfficiencyLower (~75–80%)Higher (~90–95%)
Heat under stallBuilds quicklyBetter, but still stalls
Code differenceMotorType.kBrushedMotorType.kBrushless

CAN bus vs PWM

PWM (Pulse Width Modulation) sends a single analog signal over a 3-wire cable. The pulse width encodes speed from –1 to +1. It’s simple and works with any compatible controller, but you get no feedback — you can’t read current, temperature, or faults over the same wire.

CAN bus is a two-wire differential network shared by all devices on the robot. Each device has a unique numeric ID. CAN supports two-way communication: you can send commands and read back diagnostics (current draw, temperature, sticky faults, encoder position). Modern FRC teams use CAN for almost everything because the diagnostics save enormous debugging time.

Note

CAN IDs must be unique across all devices on the bus — motors, PDP, PCM, Pigeon IMU. Teams typically assign IDs by mechanism (1–4 for drivetrain, 10–12 for arm, 20 for elevator, etc.) and document them in a spreadsheet.

CTRE controllers: TalonSRX and VictorSPX

CTRE (Cross The Road Electronics) controllers use the com.ctre.phoenix.motorcontrol.can package. Import it from the CTRE Phoenix vendor library (added via vendordeps).

import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX;
import com.ctre.phoenix.motorcontrol.can.WPI_VictorSPX;
import com.ctre.phoenix.motorcontrol.NeutralMode;

// In your Subsystem:
private final WPI_TalonSRX leftLeader  = new WPI_TalonSRX(1);
private final WPI_VictorSPX leftFollower = new WPI_VictorSPX(2);

The WPI_ prefix means these classes implement WPILib’s SpeedController (now MotorController) interface, so they drop into standard WPILib patterns.

Basic speed control:

leftLeader.set(0.5);   // 50% output, forward
leftLeader.set(-0.3);  // 30% output, reverse
leftLeader.set(0.0);   // stop

Inverting direction:

leftLeader.setInverted(true);   // positive set() now spins the other way

Follower mode — one controller mirrors another so you don’t have to call set() on each:

leftFollower.follow(leftLeader);
// Now leftFollower always matches leftLeader's output.
// Call set() only on leftLeader.

Neutral mode — what happens when output is 0:

leftLeader.setNeutralMode(NeutralMode.Brake);  // motor fights rotation — stops fast
leftLeader.setNeutralMode(NeutralMode.Coast);  // motor freewheels — coasts to a stop

Brake mode is preferred for mechanisms that need to hold position (arms, elevators). Coast mode is often better for drivetrains where abrupt stops could tip the robot.

Current limits (TalonSRX):

// Stator current limit (current flowing through motor)
leftLeader.configStatorCurrentLimit(
    new StatorCurrentLimitConfiguration(
        true,   // enable
        40,     // limit (amps) — steady state
        60,     // trigger threshold (amps) — allowed briefly
        0.1     // time (seconds) above threshold before limiting
    )
);
⚠ Heads up

Without current limits, a stalled motor (e.g., an arm hitting a hard stop) can draw 200+ amps for several seconds, tripping the breaker and disabling your robot mid-match. Always configure current limits on mechanisms that can stall.

REV controllers: CANSparkMax

REV Robotics SPARK MAX controllers use the com.revrobotics package. Import the REVLib vendor library.

import com.revrobotics.CANSparkMax;
import com.revrobotics.CANSparkMax.IdleMode;
import com.revrobotics.CANSparkMaxLowLevel.MotorType;

private final CANSparkMax rightLeader  = new CANSparkMax(3, MotorType.kBrushless);
private final CANSparkMax rightFollower = new CANSparkMax(4, MotorType.kBrushless);

Use MotorType.kBrushed if driving a CIM or 775pro through a SPARK MAX (this is supported but less common).

Basic speed control — identical interface:

rightLeader.set(0.5);
rightLeader.set(0.0);

Inverting:

rightLeader.setInverted(true);

Follower mode:

rightFollower.follow(rightLeader);
// Optional: invert the follower relative to the leader
rightFollower.follow(rightLeader, true);

Idle mode (equivalent to neutral mode):

rightLeader.setIdleMode(IdleMode.kBrake);
rightLeader.setIdleMode(IdleMode.kCoast);

Current limits:

// Smart current limit — REV's recommended approach for NEO motors
rightLeader.setSmartCurrentLimit(40);  // 40 amps for a NEO driving a swerve module

// Secondary (absolute) current limit — hardware enforced ceiling
rightLeader.setSecondaryCurrentLimit(60);

Burning to flash — SPARK MAX settings are lost on power cycle unless saved:

rightLeader.burnFlash();  // call once during configuration, not in a loop
Note

burnFlash() has a finite write-cycle limit on the controller’s flash memory. Configure once in a setup method, not on every boot. Most teams call it once in the constructor behind a if (RobotBase.isReal()) check, or manage it with a dedicated configuration command.

Complete teleop example

Here’s a full Subsystem skeleton wiring both sides of a differential drivetrain:

import com.ctre.phoenix.motorcontrol.NeutralMode;
import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX;
import com.ctre.phoenix.motorcontrol.can.WPI_VictorSPX;
import edu.wpi.first.wpilibj2.command.SubsystemBase;

public class DriveSubsystem extends SubsystemBase {

    private final WPI_TalonSRX  leftLeader   = new WPI_TalonSRX(1);
    private final WPI_VictorSPX leftFollower = new WPI_VictorSPX(2);
    private final WPI_TalonSRX  rightLeader  = new WPI_TalonSRX(3);
    private final WPI_VictorSPX rightFollower = new WPI_VictorSPX(4);

    public DriveSubsystem() {
        // Invert right side so positive = forward on both sides
        rightLeader.setInverted(true);
        rightFollower.setInverted(true);

        // Followers mirror their leader
        leftFollower.follow(leftLeader);
        rightFollower.follow(rightLeader);

        // Brake mode — stops quickly when driver releases sticks
        leftLeader.setNeutralMode(NeutralMode.Brake);
        leftFollower.setNeutralMode(NeutralMode.Brake);
        rightLeader.setNeutralMode(NeutralMode.Brake);
        rightFollower.setNeutralMode(NeutralMode.Brake);

        // Current limits to protect 40A breakers
        configureCurrentLimit(leftLeader);
        configureCurrentLimit(rightLeader);
    }

    private void configureCurrentLimit(WPI_TalonSRX talon) {
        talon.configStatorCurrentLimit(
            new com.ctre.phoenix.motorcontrol.StatorCurrentLimitConfiguration(
                true, 38, 55, 0.1
            )
        );
    }

    /** Drive the robot. left and right are in the range [-1, 1]. */
    public void tankDrive(double left, double right) {
        leftLeader.set(left);
        rightLeader.set(right);
    }

    @Override
    public void periodic() {
        // Telemetry — visible on SmartDashboard / Shuffleboard
        SmartDashboard.putNumber("Drive/LeftCurrent",
            leftLeader.getStatorCurrent());
        SmartDashboard.putNumber("Drive/RightCurrent",
            rightLeader.getStatorCurrent());
    }
}
Code Tracer
01tankDrive(0.8, 0.8)// driving forward
02tankDrive(0.8, -0.8)// turning in place
03tankDrive(0.0, 0.0)// stopped (brake mode)
04leftLeader stalls (arm hits hard stop)// current limit triggers at 55A
05current limit active// limited to 38A — motor protected
State
leftOut0.0
rightOut0.0
leftA0.0
rightA0.0

Step through to see values update.

Initial state

Key takeaways

  • Brushless motors (NEO, Falcon) must be paired with their matching controller. Brushed motors can use any compatible controller.
  • CAN bus gives you diagnostics (current, temperature, faults). PWM is simpler but one-way.
  • Use setInverted() to fix direction, never negate in your control logic.
  • Follower mode keeps secondary controllers in sync without additional set() calls.
  • Brake mode holds position; coast mode freewheels. Choose based on your mechanism.
  • Current limits protect motors from stall damage and protect breakers from tripping.

Common confusions

“The follower motor is going the wrong direction.” Followers mirror the output signal, not the physical motion. If the leader is already inverted, the follower may need to be inverted independently or follow with the invert flag: follower.follow(leader, true).

“I called set() but nothing happened.” Check: (1) Is the CAN ID correct and unique? (2) Is the vendor library in vendordeps? (3) Is the robot enabled in driver station? (4) Did you get a CAN error in the Rio’s console? Use Phoenix Tuner X or REV Hardware Client to confirm the controller appears on the bus.

“The motor stutters or behaves erratically at low speeds.” This is often a deadband issue. Both TalonSRX and SPARK MAX have a built-in deadband (default ~4%). Inputs below that threshold output zero. You can adjust it, but be careful — too small a deadband makes joystick drift visible as movement.

“Coast vs brake: my arm falls when I let go.” That’s coast mode. Switch to brake mode: setNeutralMode(NeutralMode.Brake) or setIdleMode(IdleMode.kBrake). For heavy mechanisms, even brake mode may not hold against gravity — you’ll need active control (covered in the PID lesson).

Challenge

⚡ Try it yourself

Write a static method clampAndLog(double requested) that clamps a motor output to the range [−0.75, 0.75] and prints both the requested and actual output. Then call it with several test values including out-of-range inputs.

Code EditorJavaCtrl+Enter to run
Stuck? Show hint

Use Math.max(-0.75, Math.min(0.75, requested)) to clamp. Use String.format('%.2f', value) to format to two decimal places.

What’s next

In Robot Code Lesson 03 we’ll add feedback: encoders and sensors. You’ll learn how to read position and velocity from a motor’s built-in encoder or an external quadrature encoder, how to use limit switches to detect mechanism boundaries, and how to convert raw encoder ticks into real-world units like inches or degrees.