Unit 5 · Lesson 5

Driving a Simple Robot

Lessons 2, 3, and 4 gave you the pieces: controller input, motor controllers, and sensors. This lesson puts them together into a working differential drive robot — a two-side drivetrain that can move forward, turn, and stop reliably under driver control. It is the first complete system you will build in this course.

By the end of this lesson, you will:

  • Implement arcade drive and tank drive by computing left/right motor outputs from controller input
  • Use WPILib's DifferentialDrive class for safe, clamp-protected drive output
  • Add a speed scaling (slow mode) feature using a held button modifier
  • Implement a ramp rate to prevent sudden full-speed starts that stress the drivetrain
  • Build a complete, deployment-ready drive subsystem with all protections in place

Differential Drive: How Two Motors Produce Motion

A differential drive (also called a tank drive or skid-steer) controls direction by running the left and right sides of the robot at different speeds. Both sides forward at the same speed = drive straight. Left faster than right = turn right. Left backward, right forward = spin in place. All of motion planning in a differential drive reduces to calculating two numbers: the left motor output and the right motor output.

// The fundamental differential drive equation — everything else is just how you compute these leftMotor.set(leftOutput); // -1.0 to 1.0 rightMotor.set(rightOutput); // -1.0 to 1.0 — often inverted from left

Drive Style Visualizer

There are three common input patterns for differential drive. Select each to see how it maps controller inputs to motor outputs — and which situations each is best for.

Drive Style Comparison — select to compare
🕹️ Arcade
🦾 Tank
↩️ Curvature
How it works

One stick controls forward/backward speed (throttle); a second stick (or the same stick's X-axis) controls left/right rotation (turn).

left = throttle + turn
right = throttle − turn

Both outputs are clamped to [−1, 1].

Best for / trade-offs

Most intuitive for new drivers — feels like a video game. Left stick forward = go forward, right stick sideways = turn. Recommended for any team whose driver didn't grow up playing tank games. Trade-off: diagonal stick movements can produce unexpected outputs at the output boundaries.

How it works

Left stick Y-axis directly controls the left motors; right stick Y-axis directly controls the right motors.

left = leftStickY
right = rightStickY

The driver has independent control over each side.

Best for / trade-offs

Gives experienced drivers maximum mechanical intuition — they can feel exactly what each side is doing. Good for defense-heavy robots where precise lateral positioning matters. Trade-off: requires both thumbs simultaneously; novice drivers often make the robot shake trying to hold both sticks at the same position.

How it works

Forward speed on one axis, curvature (turning radius) on another. At higher speeds, turning input produces a wider arc. At zero speed (with quick-turn enabled), it rotates in place.

WPILib's DifferentialDrive.curvatureDrive() handles the math.

Best for / trade-offs

Feels like driving a car — large turning radius at speed, tight turns when slow. Good for robots that need to hold straight lines at high speed while still being maneuverable. Used by many championship-level teams. Requires more practice to feel natural than arcade drive.

Drive Output Calculator

Adjust the forward and rotation inputs to see how arcade drive and tank drive compute left/right motor outputs. Watch the outputs clamp when the sum exceeds 1.0.

Drive Output Calculator
0.60
0.00
Left motor output
0.60
Right motor output
0.60
Arcade formula left = clamp(forward + rotation, −1, 1)  |  right = clamp(forward − rotation, −1, 1)

WPILib's DifferentialDrive Class

You could compute arcade/tank drive manually — the math is simple. But WPILib's DifferentialDrive class adds output clamping, motor safety (stops motors if no command is sent for 100ms), and WPILib telemetry. Using it is a good default for any two-sided robot.

// Construction — in robotInit() or as a field private final TalonFX leftMotor = new TalonFX(Constants.DRIVE_LEFT_ID); private final TalonFX rightMotor = new TalonFX(Constants.DRIVE_RIGHT_ID); private final MotorControllerGroup leftGroup = new MotorControllerGroup(leftMotor); private final MotorControllerGroup rightGroup = new MotorControllerGroup(rightMotor); private final DifferentialDrive drive = new DifferentialDrive(leftGroup, rightGroup); // In robotInit() — set right side inverted (most drivetrains are mirrored) @Override public void robotInit() { rightMotor.setInverted(true); // physical motors spin opposite directions for same robot direction } // teleopPeriodic() — three drive modes, pick one @Override public void teleopPeriodic() { // Arcade: left stick Y = forward, right stick X = rotation drive.arcadeDrive(-controller.getLeftY(), controller.getRightX()); // Tank: left stick Y = left side, right stick Y = right side drive.tankDrive(-controller.getLeftY(), -controller.getRightY()); // Curvature: left stick Y = speed, right stick X = curvature drive.curvatureDrive(-controller.getLeftY(), controller.getRightX(), controller.getRightBumper()); }
💡 DifferentialDrive applies its own deadband

DifferentialDrive applies a default deadband of 0.02 to inputs. That's usually not enough for joystick noise — apply MathUtil.applyDeadband() with 0.05 before passing values to the drive methods. The internal deadband does not replace your manual deadband application.

Speed Scaling and Turbo Mode

Running at full speed in the pit during calibration, or bumping into field elements at 5 m/s because a driver accidentally full-sticked, are both common and both preventable. Speed scaling lets you reduce maximum output by default and unlock full speed with a held button — or the reverse (default full speed, slow mode with a bumper).

// Default: limited speed. Hold right bumper to unlock full speed. @Override public void teleopPeriodic() { double speedScale = controller.getRightBumper() ? 1.0 : 0.5; double forward = MathUtil.applyDeadband(-controller.getLeftY(), 0.05) * speedScale; double rotation = MathUtil.applyDeadband( controller.getRightX(), 0.05) * speedScale; drive.arcadeDrive(forward, rotation); SmartDashboard.putNumber("Speed Scale", speedScale); }

A ramp rate limits how fast the motor output can change per second. Without a ramp rate, a driver going from 0 to full speed in one stick movement slams the full output onto the motors in one 20ms cycle — 50 output units per second. A 0.2-second ramp limits acceleration to manageable levels for the drivetrain hardware.

// Ramp rate in TalonFX configuration — applied in robotInit() config.OpenLoopRamps.DutyCycleOpenLoopRampPeriod = 0.2; // seconds to reach full output motor.getConfigurator().apply(config); // Manual software ramp — apply in teleopPeriodic() private double lastOutput = 0.0; private static final double RAMP_RATE = 0.05; // max change per cycle (0.05 * 50Hz = 2.5/sec) private double ramp(double target, double current) { if (Math.abs(target - current) <= RAMP_RATE) return target; return current + Math.signum(target - current) * RAMP_RATE; } // Usage in teleopPeriodic: lastOutput = ramp(desiredOutput, lastOutput); leftMotor.set(lastOutput);
💡 Hardware ramp vs. software ramp

The TalonFX's hardware ramp applies to all output modes including feedforward and closed-loop. A software ramp only applies in code that explicitly uses it. For simple open-loop teleop driving, the hardware ramp is cleaner. For mechanisms where you need full control of the ramp behavior across modes, use the software version.

A Complete Drive Subsystem

// Robot.java — complete driving implementation with all protections public class Robot extends TimedRobot { // Hardware — constructed once, reused every cycle private final TalonFX leftMotor = new TalonFX(Constants.DRIVE_LEFT_ID); private final TalonFX rightMotor = new TalonFX(Constants.DRIVE_RIGHT_ID); private final DifferentialDrive drive; private final XboxController controller = new XboxController(0); public Robot() { // DifferentialDrive wraps our motors drive = new DifferentialDrive(leftMotor, rightMotor); } @Override public void robotInit() { TalonFXConfiguration cfg = new TalonFXConfiguration(); cfg.CurrentLimits.StatorCurrentLimit = 50; cfg.CurrentLimits.StatorCurrentLimitEnable = true; cfg.OpenLoopRamps.DutyCycleOpenLoopRampPeriod = 0.15; leftMotor.getConfigurator().apply(cfg); rightMotor.getConfigurator().apply(cfg); rightMotor.setInverted(true); // right side physically reversed } @Override public void robotPeriodic() { SmartDashboard.putNumber("Left Amps", leftMotor.getStatorCurrent().getValueAsDouble()); SmartDashboard.putNumber("Right Amps", rightMotor.getStatorCurrent().getValueAsDouble()); SmartDashboard.putNumber("Left Vel", leftMotor.getVelocity().getValueAsDouble()); } @Override public void teleopPeriodic() { double scale = controller.getRightBumper() ? 1.0 : 0.6; double fwd = MathUtil.applyDeadband(-controller.getLeftY(), 0.05) * scale; double rot = MathUtil.applyDeadband( controller.getRightX(), 0.05) * scale; drive.arcadeDrive(fwd, rot); } @Override public void disabledInit() { leftMotor.setNeutralMode(NeutralModeValue.Coast); rightMotor.setNeutralMode(NeutralModeValue.Coast); } @Override public void teleopInit() { leftMotor.setNeutralMode(NeutralModeValue.Brake); rightMotor.setNeutralMode(NeutralModeValue.Brake); } }
🔍 LRI Observation

The single most reliable indicator of a team's drive software quality during inspection is whether I see a speed scale. A robot with no speed limiting that can drive at full speed in any mode is a robot that will hit field elements, alliance partners, or volunteers the first time a driver is startled. It is also a robot whose drivers have no way to practice fine positioning. Default-limited with a bumper for turbo is the right model: safe by default, full capability when intentionally engaged.

⚙️ 🔌 System Check
  • Verify inversion physically before competition. Deploy, enable, push forward — robot should move forward. If it spins, check which side is inverted. If it moves backward, both sides are inverted or the Y-axis isn't negated.
  • Apply deadband before any drive calculation, not after. Computing arcade drive output then applying a deadband to the result is wrong — you'd be deadbanding the combined output, not the source axes individually.
  • Use a speed limiter as default. Default to 60–70% speed in teleop. Document that the right bumper unlocks full speed. Tell drivers. Write it in your drive team notes.
  • Configure ramp rate in motor controller, not only in software. The hardware ramp applies even if your software sends a step change due to a code bug. Software ramp alone can be bypassed; hardware ramp cannot.
  • Monitor stator current during pushing matches. Current spikes on drive motors during defense indicate when your gearbox and chain are being stressed. A drive motor consistently pulling above its current limit is a mechanism maintenance warning.

Knowledge Check

In arcade drive, the driver pushes the left stick fully forward (+1.0) and the right stick fully right (+1.0) simultaneously. What are the left and right motor outputs before clamping?
  • 1Left: 1.0, Right: 1.0
  • 2Left: 0.5, Right: 0.5
  • 3Left: 2.0, Right: 0.0 — the arcade formula is left = forward + rotation, right = forward − rotation; both must be clamped to [−1, 1] before commanding motors
  • 4Left: 1.0, Right: −1.0
A robot uses arcade drive with no speed scaling. During a driver practice session, the robot hits a field element hard enough to bend a bumper bracket. What is the minimal code change that would most directly prevent this class of accident?
  • 1Increase the deadband threshold so small stick movements do nothing
  • 2Multiply the final forward and rotation values by a speed scale constant (e.g., 0.6) — this limits maximum output without changing the control feel; optionally restore full speed only when a bumper is held
  • 3Switch from arcade to tank drive
  • 4Add a ramp rate to the motor controller
Both drive motors are spinning forward on the bench but the robot turns in a circle when placed on the ground. What is the most likely cause?
  • 1The CAN bus is too slow to synchronize both motors
  • 2The speed scale is asymmetric
  • 3The right motor was not inverted — both motors spin the same direction (forward on the bench), but on the robot they are physically mirrored; without inversion, one side pushes forward and one side pushes backward, causing rotation in place
  • 4The deadband is applied before the drive calculation instead of after
💪 Practice Prompt

Build and Drive Your First Robot

  1. In your Robot.java, add two TalonFX motors for left and right drive. Configure both with 50A current limit and a 0.15s ramp rate. Invert the right motor.
  2. Implement arcade drive using DifferentialDrive. Apply a 0.05 deadband and negate the left Y. Deploy to a robot (or simulate) and confirm forward stick drives the robot forward.
  3. Add a speed scale: default 0.6, hold right bumper for 1.0. Print the scale factor to SmartDashboard. Drive both with and without the bumper held and feel the difference.
  4. Switch from arcade to curvature drive using drive.curvatureDrive(). Try driving at high speed and low speed. Note how the turning radius changes with speed.
  5. In disabledInit(), switch to Coast. In teleopInit(), switch to Brake. Verify: push the robot forward when disabled (should coast), then push when enabled in brake (should resist).
  6. Bonus: Implement tank drive and have your drive team test all three modes (arcade, tank, curvature) on the same robot. Write a paragraph explaining which mode your team would choose for competition and why based on your observation session.