Unit 5 · Lesson 3

Motor Controllers

A motor controller sits between your code and the motor's electrical power. It translates a -1.0 to 1.0 software command into a voltage and current that spins a motor. This lesson covers the two motor controller families you will use in FRC — CTRE's TalonFX and REV's SparkMax — and the configuration patterns that protect your hardware.

By the end of this lesson, you will:

  • Construct a TalonFX or SparkMax with the correct CAN ID and configure it once in robotInit()
  • Set motor output, read velocity, position, and current draw from both controller families
  • Apply a stator current limit that protects the motor from stall damage
  • Choose brake vs. coast neutral mode and explain the mechanical consequence of each
  • Invert motor direction correctly without changing the physical wiring

The Two Families

Most FRC teams use motor controllers from one of two vendors. CTRE (Cross The Road Electronics) makes the TalonFX — the all-in-one motor controller integrated into the Falcon 500 and Kraken X60. REV Robotics makes the SPARK MAX and SPARK Flex, used with NEO and NEO Vortex brushless motors. Both families are competitive and capable; many teams use both on the same robot.

The vendor library you installed in Unit 0 provides the Java classes for each. CTRE uses the Phoenix 6 API; REV uses REVLib. The APIs are different — same concept, different method names. The comparison tabs later in this lesson show equivalent operations side by side.

TalonFX: The Configuration Pattern

Phoenix 6 uses a configuration object pattern: you build a TalonFXConfiguration, set all your parameters on it, then call motor.getConfigurator().apply(config) once in robotInit(). The configuration is stored in the motor controller's flash memory and persists even through a power cycle — though re-applying it each match guarantees you start with the correct values.

Click each highlighted section of the configuration block below to understand what it does and what happens if you skip it.

TalonFX Configuration Builder — click any highlighted section
private final TalonFX driveMotor = new TalonFX(Constants.DRIVE_FL_ID);   private void configureMotor(TalonFX motor) {   TalonFXConfiguration config = new TalonFXConfiguration();     // Current limiting   config.CurrentLimits.StatorCurrentLimit = 50;   config.CurrentLimits.StatorCurrentLimitEnable = true;     // Neutral mode   config.MotorOutput.NeutralMode = NeutralModeValue.Brake;     // Inversion   config.MotorOutput.Inverted = InvertedValue.CounterClockwise_Positive;     motor.getConfigurator().apply(config); }
← click any highlighted section to learn what it does

Current Limiting: The Most Important Protection

A brushless motor's stall current — what it draws when it's commanded to move but physically can't — can exceed 100 amps. Most PDH breakers are rated at 40A for drive motors. Without a current limit, a stalled motor will trip the breaker, cutting power to that motor for the rest of the match. With a software current limit, the controller reduces output before the breaker trips.

// TalonFX (Phoenix 6) — stator current limit config.CurrentLimits.StatorCurrentLimit = 50; // amps — below 40A breaker is safer config.CurrentLimits.StatorCurrentLimitEnable = true; // REV SparkMax — supply current limit motor.setSmartCurrentLimit(40); // amps // Common limits by motor type (starting points — tune for your gearing): // Drive motors: 40–60A stator (TalonFX), 40A supply (SparkMax) // Intake motors: 30–40A stator // Arm/wrist: 20–30A stator // Climber: 40–60A stator (expect high stall loads)
🔍 LRI Observation

During robot inspections I look for motor controllers without current limits. A TalonFX integrated Falcon 500 can draw over 105A at stall. If you put a 40A breaker on a motor with no software current limit and stall it — intake jam, arm hitting a hard stop, climber at full extension — the breaker trips. Not after a delay, not with a warning: instantly. The motor goes silent mid-match. Every motor on your robot that can stall should have a current limit. This takes two lines of configuration and costs you nothing except a few minutes to choose a reasonable starting value.

Neutral Mode: Brake vs. Coast

Neutral mode controls what happens when the motor output is set to zero. Brake mode actively shorts the motor leads — the motor resists motion and the robot decelerates quickly. Coast mode disconnects the motor leads — the robot coasts freely. The choice is mechanical and strategic, not just software preference.

// TalonFX (Phoenix 6) config.MotorOutput.NeutralMode = NeutralModeValue.Brake; // resists motion config.MotorOutput.NeutralMode = NeutralModeValue.Coast; // free-wheels // REV SparkMax motor.setIdleMode(CANSparkMax.IdleMode.kBrake); motor.setIdleMode(CANSparkMax.IdleMode.kCoast); // Mechanical implications: // Brake: drive motors → robot stops quickly, holds position on slopes // arm motors → holds arm position without active PID // risk: abrupt stops can stress chain/belts if not ramped // Coast: drive motors → robot coasts after releasing stick (fine for defense) // enabled mode → usually Brake | disabled mode → often Coast // (wheels shouldn't drag when pushing robot across pit floor) // Best practice: switch to Coast in disabledInit() so the robot rolls freely @Override public void disabledInit() { driveMotor.setNeutralMode(NeutralModeValue.Coast); } @Override public void teleopInit() { driveMotor.setNeutralMode(NeutralModeValue.Brake); }

Reading from Motor Controllers

Motor controllers report sensor data back to the roboRIO over the CAN bus. The TalonFX has an integrated encoder built into the motor — no separate encoder needed. SparkMax reads the encoder attached to the motor's hall-effect sensor (NEO) or an external encoder.

// TalonFX (Phoenix 6) — all reads return StatusSignal objects, call .getValueAsDouble() double velocityRPS = driveMotor.getVelocity().getValueAsDouble(); // rotations/sec double positionRot = driveMotor.getPosition().getValueAsDouble(); // rotations double statorAmps = driveMotor.getStatorCurrent().getValueAsDouble(); // amps double supplyAmps = driveMotor.getSupplyCurrent().getValueAsDouble(); // amps from PDH double tempCelsius = driveMotor.getDeviceTemp().getValueAsDouble(); // °C // REV SparkMax — encoder is a separate object retrieved once RelativeEncoder encoder = motor.getEncoder(); // called once in robotInit() double velocityRPM = encoder.getVelocity(); // RPM double positionRev = encoder.getPosition(); // rotations double outputAmps = motor.getOutputCurrent(); // amps // Convert velocity to useful units (example: drive wheel surface speed) double wheelRPS = velocityRPS / GEAR_RATIO; double speedMPS = wheelRPS * Math.PI * WHEEL_DIAMETER_M;

TalonFX vs. SparkMax: API Comparison

The tabs below show equivalent operations in both APIs. The conceptual model is identical — the syntax differs.

Both APIs accept a duty-cycle output from -1.0 (full reverse) to +1.0 (full forward). Phoenix 6 also supports velocity and position closed-loop control natively; REV's closed-loop is configured through the SparkMax PID controller object.

CTRE — TalonFX / Phoenix 6
private final TalonFX motor = new TalonFX(MOTOR_ID); // Duty-cycle output (-1.0 to 1.0) motor.set(0.5); // Phoenix 6 typed request motor.setControl( new DutyCycleOut(0.5)); // Stop motor.set(0.0); motor.stopMotor();
REV — SparkMax / REVLib
private final CANSparkMax motor = new CANSparkMax(MOTOR_ID, MotorType.kBrushless); // Duty-cycle output (-1.0 to 1.0) motor.set(0.5); // Stop motor.set(0.0); motor.stopMotor();

TalonFX uses Phoenix 6's StatusSignal pattern — every reading returns a signal object with .getValueAsDouble(). SparkMax returns primitive doubles directly from the encoder object. Both report velocity in their native units: TalonFX in rotations/second, SparkMax in RPM.

CTRE — TalonFX / Phoenix 6
// Velocity — rotations/sec double rps = motor .getVelocity() .getValueAsDouble(); // Position — rotations double rot = motor .getPosition() .getValueAsDouble(); // Convert RPS → m/s double mps = (rps / GEAR_RATIO) * Math.PI * WHEEL_DIAM_M;
REV — SparkMax / REVLib
// Encoder — get once in init() RelativeEncoder enc = motor.getEncoder(); // Velocity — RPM double rpm = enc.getVelocity(); // Position — rotations double rot = enc.getPosition(); // Convert RPM → m/s double mps = (rpm / 60.0 / GEAR_RATIO) * Math.PI * WHEEL_DIAM_M;

Both APIs protect the motor from stall damage via current limits, but the terminology differs. TalonFX uses stator current (current flowing through the motor windings). SparkMax uses supply current (current drawn from the PDH). Stator limits are generally more protective for the motor itself.

CTRE — TalonFX / Phoenix 6
TalonFXConfiguration cfg = new TalonFXConfiguration(); cfg.CurrentLimits .StatorCurrentLimit = 50; cfg.CurrentLimits .StatorCurrentLimitEnable = true; motor.getConfigurator() .apply(cfg);
REV — SparkMax / REVLib
// Smart current limit // (supply current, amps) motor.setSmartCurrentLimit(40); // Secondary hard limit motor.setSecondaryCurrentLimit( 60); // Persist to flash so it // survives power cycle motor.burnFlash();
💡 Call burnFlash() on SparkMax — carefully

SparkMax configuration is volatile by default — it resets to factory defaults on power loss. Call motor.burnFlash() to save settings to the controller's flash memory. However: burnFlash() takes ~200ms and can only be called a limited number of times (rated for ~10,000 writes). Call it once during the initial setup session, not every match.

Many mechanisms use two or more motors mechanically linked together (gearbox with two motors, elevator with two motors). Rather than commanding each motor separately, set one as the "leader" and the others as "followers" — followers mirror the leader's output automatically.

CTRE — TalonFX / Phoenix 6
// Leader private final TalonFX leader = new TalonFX(LEAD_ID); // Follower — mirrors leader // true = oppose leader direction private final TalonFX follower = new TalonFX(FOLLOW_ID); follower.setControl( new Follower(LEAD_ID, false)); // Only command the leader leader.set(0.5);
REV — SparkMax / REVLib
// Leader private final CANSparkMax leader = new CANSparkMax(LEAD_ID, MotorType.kBrushless); // Follower private final CANSparkMax follower = new CANSparkMax(FOLLOW_ID, MotorType.kBrushless); follower.follow(leader); // Only command the leader leader.set(0.5);

Motor Inversion

On most mechanisms, two motors on opposite sides of a gearbox spin in opposite directions to produce the same mechanical output (both wheels push forward). Rather than rewiring hardware, invert one motor in software. Inversion should be applied in configuration, not by negating the output value — negating the output in code bypasses inversion tracking and causes confusing behavior in closed-loop control.

// TalonFX — set in configuration, not by negating set() value config.MotorOutput.Inverted = InvertedValue.Clockwise_Positive; // inverted config.MotorOutput.Inverted = InvertedValue.CounterClockwise_Positive; // normal // SparkMax — setInverted() or pass to follow() motor.setInverted(true); follower.follow(leader, true); // true = oppose leader direction // ❌ Do NOT negate output to invert motor.set(-speed); // bypasses closed-loop inversion tracking — use config instead
⚙️ 🔌 System Check
  • Every motor that can stall must have a current limit. Calculate your limit based on the PDH breaker rating (typically 40A for drive). Set the software limit ~10A below the hardware breaker rating to prevent trips under normal operation.
  • Set neutral mode per mode: Brake for teleop and auto, Coast for disabled. This prevents the robot from dragging during pit movement while maintaining stopping authority during competition.
  • Apply all configuration in robotInit(), once per power cycle. Never call getConfigurator().apply() inside a periodic method — it sends CAN traffic and can block the loop.
  • Use InvertedValue enum for TalonFX inversion, not output negation. Software inversion is part of the motor's configuration model and interacts correctly with closed-loop control.
  • Read getStatorCurrent() and log it to SmartDashboard. Current draw is the most reliable early indicator of a mechanical problem — a motor drawing 2x its normal current is a mechanism about to jam or a chain about to snap.

Knowledge Check

A team uses a TalonFX on a drive motor with no current limit. During a pushing match, the motor stalls. What is the most likely immediate hardware consequence?
  • 1The TalonFX automatically reduces output to protect the motor
  • 2The motor draws its full stall current (potentially 100+ amps), trips the 40A PDH breaker, and the drive motor loses power for the rest of the match — with no way to reset during play
  • 3The roboRIO detects the stall and commands the motor to stop
  • 4The motor browns out and the voltage drops across the whole robot
A robot's arm drops when disabled even though the motor is set to brake mode. What is the most likely cause?
  • 1Brake mode doesn't work when the motor is under load
  • 2The neutral mode is being switched to Coast in disabledInit() — the arm coasts freely when disabled; if the arm needs to hold position when disabled, it needs brake mode in all modes or a mechanical brake
  • 3The TalonFX resets neutral mode to Coast on every power cycle by default
  • 4Brake mode only works in teleop and autonomous, not in disabled
Two SparkMax motors are mechanically linked. A programmer calls motor1.set(0.5) and motor2.set(-0.5) every cycle. What is wrong with this approach?
  • 1Nothing — commanding two motors independently is the correct pattern
  • 3If the two set() calls have any timing difference or value mismatch — due to a code change or bug — the motors fight each other mechanically; using the follower API guarantees synchronization at the hardware level with zero code-side drift
  • 3SparkMax motors cannot be set to negative values
  • 4Two CAN commands per cycle exceeds the bus bandwidth limit
💪 Practice Prompt

Configure, Command, and Monitor a Motor

  1. Add a TalonFX driveMotor field to your Robot.java. In robotInit(), construct it with the correct CAN ID from Constants.java and call a private void configureMotor(TalonFX motor) helper that applies a TalonFXConfiguration with a 50A stator current limit, Brake neutral mode, and no inversion.
  2. In teleopPeriodic(), read the left stick Y-axis (with negation and deadband from Lesson 2) and pass it to driveMotor.set(). Test with a physical robot or simulation: confirm forward stick drives the motor forward.
  3. In robotPeriodic(), publish driveMotor.getStatorCurrent().getValueAsDouble(), getVelocity().getValueAsDouble(), and getPosition().getValueAsDouble() to SmartDashboard. Watch the values change as you command the motor.
  4. In disabledInit(), switch the motor to Coast mode. In teleopInit(), switch back to Brake. Verify the transition by enabling and disabling while monitoring dashboard output.
  5. Add a second TalonFX followerMotor. Configure it as a follower of driveMotor. Command only the leader — confirm the follower mirrors it.
  6. Bonus: Repeat steps 1–3 using a SparkMax with REVLib instead of TalonFX with Phoenix 6. What API differences did you notice? Which pattern did you find more readable?