Your First Robot Program
Hook
The robot is wired, the code is deployed, Driver Station is connected. You click Enable. Nothing happens.
You open Robot.java. The template has six methods in it. You pasted your motor control code into robotInit() — the method that runs once at startup. The motor ran for a split second, then stopped.
Every experienced FRC programmer has made this mistake. The fix is understanding the lifecycle: which method runs when, how often, and what each one is designed for.
The TimedRobot lifecycle
TimedRobot is the base class for almost every FRC robot program. It provides a set of methods that WPILib calls automatically based on what the robot is doing. You override these methods to add your behavior.
Here’s the full lifecycle:
public class Robot extends TimedRobot {
@Override
public void robotInit() {
// Called ONCE when the robot program starts.
// Create motor controllers, sensors, joysticks here.
}
@Override
public void robotPeriodic() {
// Called EVERY 20ms regardless of mode.
// Good for logging, updating SmartDashboard.
}
@Override
public void teleopInit() {
// Called ONCE when entering teleop mode.
// Reset any state that teleop depends on.
}
@Override
public void teleopPeriodic() {
// Called EVERY 20ms while in teleop mode.
// Read joysticks, set motors — this is your main driver-control loop.
}
@Override
public void autonomousInit() {
// Called ONCE when autonomous begins.
}
@Override
public void autonomousPeriodic() {
// Called EVERY 20ms during autonomous.
// Execute your auto routine here.
}
@Override
public void disabledPeriodic() {
// Called EVERY 20ms while disabled.
// Useful for reading sensors even when motors can't run.
}
}
robotInit() runs once at startup. teleopPeriodic() runs 50 times per second during driver control. Put initialization in Init methods. Put ongoing behavior in Periodic methods.
Lifecycle timing
Robot powers on
↓
robotInit() ← once
↓
[disabled — waiting for Driver Station enable]
disabledPeriodic() ← every 20ms
↓
[Driver Station: Enable Teleop]
teleopInit() ← once
teleopPeriodic() ← every 20ms, 50x/second
↓
[Driver Station: Disable]
disabledPeriodic() ← every 20ms again
This is why putting motor code in robotInit() made the motor blip: it ran once and immediately returned. The motor ran for one 20ms tick, then the scheduler moved on to the disabled state — and WPILib zeros all motors on disable.
Declaring hardware objects
In Java, you declare your hardware as fields of the Robot class so every method can use them. You instantiate them in robotInit().
import com.revrobotics.CANSparkMax;
import com.revrobotics.CANSparkLowLevel.MotorType;
import edu.wpi.first.wpilibj.Joystick;
public class Robot extends TimedRobot {
// Declare fields at the class level
private CANSparkMax leftMotor;
private CANSparkMax rightMotor;
private Joystick driverController;
@Override
public void robotInit() {
// Instantiate them here
leftMotor = new CANSparkMax(1, MotorType.kBrushless);
rightMotor = new CANSparkMax(2, MotorType.kBrushless);
driverController = new Joystick(0); // USB port 0 in Driver Station
}
}
Why declare at class level, not inside robotInit? If you declared leftMotor as a local variable inside robotInit(), it would go out of scope when that method returned. teleopPeriodic() would have no way to reach it. Fields belong to the whole class — every method can access them.
The number you pass to new CANSparkMax(1, ...) is the CAN ID of the motor controller. It must match the ID configured on the physical hardware using the REV Hardware Client. CAN ID mismatches are one of the most common reasons a motor does nothing.
Reading a joystick
The Joystick class (or the newer XboxController class) reads input from a gamepad or flight stick connected to the Driver Station laptop.
// Joystick axis values range from -1.0 to +1.0
double speed = driverController.getRawAxis(1); // left stick Y-axis
Common axis numbers for a standard Logitech joystick:
- Axis 0: X (left/right)
- Axis 1: Y (forward/back) — note: pushing forward gives a negative value
For an Xbox controller, use XboxController for named methods:
import edu.wpi.first.wpilibj.XboxController;
XboxController xbox = new XboxController(0);
double speed = xbox.getLeftY(); // left stick Y axis
double turn = xbox.getRightX(); // right stick X axis
On most joysticks, pushing the stick forward gives a negative value (axis returns -1.0 at full forward). This is the industry standard but always surprises beginners. Negate the axis value if your robot drives backwards when you push forward.
Setting motor speed
CANSparkMax.set(double speed) takes a value from -1.0 to 1.0:
1.0= full power forward-1.0= full power reverse0.0= stop
Putting it together — a minimal single-motor teleop:
@Override
public void teleopPeriodic() {
double speed = driverController.getRawAxis(1);
leftMotor.set(speed);
}
Every 20ms: read the joystick, send the value to the motor. That’s it. The motor controller receives the command over CAN bus and outputs the appropriate voltage.
Tank drive
A tank drive robot has two independent sides — left motors and right motors. Each side is controlled by a separate joystick axis.
@Override
public void teleopPeriodic() {
// Left stick Y controls left side, right stick Y controls right side
double leftSpeed = -driverController.getRawAxis(1); // negate: forward = negative
double rightSpeed = -driverController.getRawAxis(5); // axis 5 = right stick Y
leftMotor.set(leftSpeed);
rightMotor.set(rightSpeed);
}
If your robot has two motors per side (four motors total), you can use rightMotor.follow(leftMotor) to make the second motor mirror the first, then only set the lead motor. Or set all motors explicitly — either approach works.
Adding a deadband
Joystick axes are rarely exactly 0.0 when you’re not touching them — electrical noise and mechanical slop mean you might see values like 0.03 or -0.02. Without a deadband, the motors will twitch and hum constantly.
private double applyDeadband(double value, double threshold) {
if (Math.abs(value) < threshold) return 0.0;
return value;
}
@Override
public void teleopPeriodic() {
double leftSpeed = applyDeadband(-driverController.getRawAxis(1), 0.1);
double rightSpeed = applyDeadband(-driverController.getRawAxis(5), 0.1);
leftMotor.set(leftSpeed);
rightMotor.set(rightSpeed);
}
WPILib also provides MathUtil.applyDeadband(value, threshold) — but understanding how it works matters more than using a utility.
Tracing a single periodic call
Let’s trace what happens in one 20ms call to teleopPeriodic() when the driver pushes the left stick halfway forward:
getRawAxis(1)// stick at 50% forward → -0.5negate: -(-0.5)// forward becomes positiveapplyDeadband(0.5, 0.1)// 0.5 > 0.1 → pass throughleftSpeed = 0.5// storedleftMotor.set(0.5)// CAN message sentStep through to see values update.
Putting it all together
Here’s a complete, minimal robot program for a two-motor tank drive:
package frc.robot;
import com.revrobotics.CANSparkMax;
import com.revrobotics.CANSparkLowLevel.MotorType;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.XboxController;
public class Robot extends TimedRobot {
private CANSparkMax leftMotor;
private CANSparkMax rightMotor;
private XboxController controller;
@Override
public void robotInit() {
leftMotor = new CANSparkMax(1, MotorType.kBrushless);
rightMotor = new CANSparkMax(2, MotorType.kBrushless);
controller = new XboxController(0);
}
@Override
public void teleopPeriodic() {
double leftSpeed = MathUtil.applyDeadband(-controller.getLeftY(), 0.1);
double rightSpeed = MathUtil.applyDeadband(-controller.getRightY(), 0.1);
leftMotor.set(leftSpeed);
rightMotor.set(rightSpeed);
}
@Override
public void disabledInit() {
leftMotor.set(0.0);
rightMotor.set(0.0);
}
}
This is a complete, deployable robot program. Deploy it, enable in Teleop, and your robot drives.
Knowledge check
You put motor.set(0.5) in robotInit(). The motor runs briefly then stops. Why?
A joystick axis returns -0.03 when untouched. Without a deadband, what happens?
Key takeaways
robotInit()runs once;teleopPeriodic()runs 50 times per second during driver control. Initialize hardware inInit, control it inPeriodic.- Declare hardware objects as class fields so all lifecycle methods can access them.
- Joystick axes return -1.0 to 1.0; pushing forward is typically negative — negate it for intuitive control.
- Apply a deadband (typically 0.05–0.1) to eliminate joystick noise.
- A complete tank drive is only ~10 meaningful lines of code.
Common confusions
“My motor runs in the wrong direction.” Either negate the set() value, or call motor.setInverted(true) once in robotInit(). Inversion bakes the reversal into the motor controller so you don’t have to track it in every set() call.
“The robot drives forward when I push back and back when I push forward.” You forgot to negate the Y-axis. getLeftY() returns -1.0 at full forward. Multiply by -1 or negate: -controller.getLeftY().
“I’m reading axis 1 but getting 0.0 every time.” Check Driver Station’s USB Devices tab. Your joystick might be on a different USB port than 0, so new Joystick(0) is reading nothing. Re-order the devices in Driver Station to put your controller at port 0.
Challenge
Write a tankDrive method that takes two speed values, applies a deadband of 0.1 to each, and returns them as a double[]. Then call it from teleopPeriodic.
For this exercise, simulate it: given left and right axis inputs, compute and print the final speeds that would go to the motors.
Stuck? Show hint
Negate the axis before applying deadband. Math.abs() helps you check if a value is within the deadband threshold.
What’s next
In Lesson 5, we’ll explore the Command-Based architecture — WPILib’s recommended way to organize robot code so that mechanisms, autonomous routines, and driver controls don’t tangle together as your robot grows more complex.