Lesson 2.1: Subsystems
🎯 What You’ll Learn
By the end of this lesson you will be able to:
- Explain what a subsystem is and why robots are organized this way
- Identify the subsystems in your team’s robot project
- Read a subsystem file and understand its structure
- Explain what the
generated/folder is and why you shouldn’t edit it - Apply the Code Reading Framework to a subsystem you haven’t studied yet
The Mental Model: Subsystem = Physical Capability
Here’s the single most important idea in this lesson:
A subsystem represents a physical capability of the robot — it’s the code that controls real hardware.
Think about your robot as a collection of things it can do:
- It can drive around the field
- It can pick up game pieces from the ground
- It can shoot game pieces at a target
- It can aim the shooter left and right
- It can climb during endgame
Each of those capabilities gets its own subsystem class. The subsystem “owns” the motors, sensors, and servos for that mechanism and provides methods that other code can call to make things happen.
Your Robot’s Subsystems
Your team’s robot has 5 subsystems, each in its own file inside the
| Subsystem | What It Controls |
|---|
Let’s walk through each one so you know what’s inside.
IntakeSubsystem — Picking Up Game Pieces
The
What hardware does it control?
The intake has two sets of motors:
- Roller motors (2× TalonFX) — spin to grab notes off the ground
- Deploy motors (2× TalonFXS) — swing the intake outward so the rollers can reach the floor
// Roller motors — spin to grab notes off the groundprivate final TalonFX intakeLeftMotor = new TalonFX(Constants.IntakeConstants.INTAKE_LEFT_MOTOR, Constants.kCANivoreBus);private final TalonFX intakeRightMotor = new TalonFX(Constants.IntakeConstants.INTAKE_RIGHT_MOTOR, Constants.kCANivoreBus);
// Deploy motors — swing the intake out/in (TalonFXS drives smaller Minion motors)private final TalonFXS intakeLeftActivator = new TalonFXS(Constants.IntakeConstants.DEPLOY_LEFT_MOTOR, Constants.kCANivoreBus);private final TalonFXS intakeRightActivator = new TalonFXS(Constants.IntakeConstants.DEPLOY_RIGHT_MOTOR, Constants.kCANivoreBus);Notice how every motor gets its CAN ID from Constants — the subsystem never hardcodes a number. This is the pattern you’ll see in every subsystem.
What methods does it expose?
The intake provides simple methods that commands can call:
| Method | What It Does |
|---|---|
runIntake(speed) | Spin the rollers at a given speed (-1.0 to 1.0) |
stopIntake() | Stop the rollers |
deployOut() | Swing the intake outward to reach the ground |
deployIn() | Swing the intake back into the robot frame |
stopDeploy() | Stop the deploy motors |
jostleCommand() | Returns a command that shakes the intake to unstick balls |
Here’s what deployOut() looks like — it’s just two lines:
public void deployOut() { intakeLeftActivator.set(DEPLOY_SPEED); intakeRightActivator.set(DEPLOY_SPEED);}That’s it. The subsystem provides the capability (“deploy the intake”), and commands decide when to use it.
Why does IntakeSubsystem get its CAN IDs from Constants instead of hardcoding numbers like `new TalonFX(31)`?
👉 See the full file on GitHub:
ShooterSubsystem — Launching Game Pieces
The
- Two flywheel motors — spin up to launch the note
- A feeder motor — pushes the note into the flywheels
- An indexer motor — queues notes before feeding
- A hood servo — adjusts the launch angle up/down
Closed-Loop Velocity Control
The flywheels use something called closed-loop velocity control. Instead of saying “run at 50% power,” we tell the motor “spin at 30 rotations per second” and the motor controller automatically adjusts voltage to maintain that speed — even as the battery drains during a match.
// VelocityVoltage tells the motor "maintain this speed (in rotations per second)"private final VelocityVoltage flywheelVelocity = new VelocityVoltage(0) .withEnableFOC(false);
public void runFlywheelsRPS(double rps) { double adjustedRPS = rps + rpsOffset; if (adjustedRPS < 0) adjustedRPS = 0; targetRPS = adjustedRPS; leftShooterMotor.setControl(flywheelVelocity.withVelocity(adjustedRPS)); rightShooterMotor.setControl(flywheelVelocity.withVelocity(adjustedRPS));}Auto-Aim with Interpolation Tables
The shooter can automatically set the right hood angle and flywheel speed based on distance to the target. It uses interpolation tables — lookup tables where you give a distance and get back a value. Between known data points, it estimates the right value.
// Hood table: distance → servo position (0.0 = flat, 1.0 = max angle up)hoodTable.put(1.3, 0.13);hoodTable.put(2.1, 0.21);hoodTable.put(3.0, 0.30);hoodTable.put(3.8, 0.38);hoodTable.put(4.7, 0.47);👉 See the full file on GitHub:
TurretSubsystem — Aiming Left and Right
The
The turret uses Motion Magic position control — you tell the motor “go to this angle” and it generates a smooth motion profile to get there without skipping gear teeth.
Key things to notice when reading this file:
- The turret has a gear ratio of 11.515:1 (11.515 motor rotations = 1 turret rotation)
- It has asymmetric limits — it can go +420° one way but only -245° the other way
- It has a reset mode for when it needs to wrap around 360° to reach a target
- The
aimAtPose()method handles velocity compensation so shots land accurately even while driving
👉 See the full file on GitHub:
ClimberSubsystem — Endgame Climbing
The
- An elevator motor — extends/retracts a lift mechanism
- Servos — lock/unlock the climbing hooks and gearbox
This subsystem is interesting because some of its hardware isn’t wired yet — you’ll see commented-out code for the climb motors. That’s normal in FRC! Teams often write code before the hardware is ready so they can test as soon as it’s built.
// Climb motors — pull the robot up// private final TalonFX leftClimbMotor = new TalonFX(Constants.ClimberConstants.CLIMB_LEFT_MOTOR);// private final TalonFX rightClimbMotor = new TalonFX(Constants.ClimberConstants.CLIMB_RIGHT_MOTOR);The elevator uses PID position control to move to specific heights, and the subsystem includes a “level-and-return” cycle for testing.
👉 See the full file on GitHub:
CommandSwerveDrivetrain — Driving
The
- It extends a CTRE base class instead of
SubsystemBase— but it still implements theSubsysteminterface, so the command scheduler treats it the same way - Most of its configuration comes from a generated file (more on that below)
- It integrates with PathPlanner for autonomous path following
public class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Subsystem {The drivetrain’s periodic() method handles setting the operator perspective based on alliance color — so “forward” on the joystick always means “toward the opposing alliance wall,” regardless of which side you’re on.
👉 See the full file on GitHub:
-
Every subsystem owns the hardware (motors, sensors, servos) for one physical mechanism. It declares them as private fields and configures them in the constructor.
-
Other code uses a subsystem by calling its public methods. For example, a command calls
intakeSubsystem.runIntake(0.5)to spin the rollers. The subsystem provides the capability, and commands decide when to use it. -
IntakeSubsystem — it controls the roller motors that grab notes and the deploy motors that swing the intake out to reach the ground.
The generated/ Folder and TunerConstants.java
You may have noticed a folder called
This file is auto-generated by CTRE’s Tuner X tool. It contains all the swerve drive configuration:
- Motor CAN IDs for all four swerve modules (drive + steer motors)
- Encoder IDs and offsets for each module
- PID gains for steering and driving
- Physical measurements (wheel radius, gear ratios, module positions)
- The
createDrivetrain()factory method that builds theCommandSwerveDrivetrain
// Front Leftprivate static final int kFrontLeftDriveMotorId = 1;private static final int kFrontLeftSteerMotorId = 2;private static final int kFrontLeftEncoderId = 3;private static final Angle kFrontLeftEncoderOffset = Rotations.of(-0.4228515625);How TunerConstants connects to CommandSwerveDrivetrain
The relationship is simple:
- TunerConstants defines all the hardware configuration and provides a
createDrivetrain()method - RobotContainer calls
TunerConstants.createDrivetrain()to create the drivetrain subsystem - CommandSwerveDrivetrain extends the generated
TunerSwerveDrivetrainclass and adds command-based features
Think of TunerConstants as the “blueprint” and CommandSwerveDrivetrain as the “finished product” that adds robot-specific behavior on top.
A teammate accidentally edited TunerConstants.java and changed a motor CAN ID. What happens next time someone runs the Tuner X calibration tool?
The Subsystem Pattern: What They All Have in Common
Now that you’ve seen all five subsystems, let’s identify the pattern. Every subsystem follows the same structure:
1. Hardware declarations (top of the file)
Motors, sensors, and servos are declared as private final fields. They’re created in the field declarations or constructor using CAN IDs from Constants.
2. Constructor (configures hardware)
The constructor sets up motor configurations — things like direction (inverted or not), neutral mode (brake vs coast), and PID gains.
3. Public methods (the capabilities)
These are the actions the subsystem can perform. Commands call these methods. Examples: runIntake(), deployOut(), runFlywheelsRPS(), aimAtPose().
4. periodic() method (runs every loop)
This method is called automatically every ~20ms by the command scheduler. Subsystems use it for telemetry (publishing data to the dashboard) and safety checks.
What is the periodic() method used for in most subsystems?
🔍 Code Reading Exercise: ShooterSubsystem.java
Time to practice the Code Reading Framework on a subsystem. Open
Take your time reading through the file. Focus on the structure — the hardware declarations at the top, the constructor in the middle, and the public methods. Don’t worry about understanding every line of the math.
ShooterSubsystem.javaHere’s an example using IntakeSubsystem:
- Hardware declarations —
private final TalonFX intakeLeftMotorandprivate final TalonFXS intakeLeftActivator(roller and deploy motors) - Constructor — Configures TalonFXS motors with
MotorArrangementValue.Minion_JSTand sets roller motors to opposite directions so they both pull inward - Public methods —
runIntake(speed),deployOut(),deployIn(),stopIntake(),jostleCommand() - periodic() — Publishes deploy motor positions to NetworkTables at ~10Hz for dashboard monitoring
Every subsystem in the project follows this same four-part structure.
Quick Reference: Subsystem Summary
| Subsystem | Hardware | Key Methods | Control Type |
|---|---|---|---|
| IntakeSubsystem | 2 rollers + 2 deploy motors | runIntake(), deployOut() | Open-loop (duty cycle) |
| ShooterSubsystem | 2 flywheels + feeder + indexer + hood servo | runFlywheelsRPS(), autoAim() | Closed-loop velocity |
| TurretSubsystem | 1 turret motor | aimAtPose(), rotate() | Motion Magic position |
| ClimberSubsystem | 1 elevator motor + servos | elevatorUp(), toggleGearboxLock() | PID position |
| CommandSwerveDrivetrain | 8 swerve motors (4 drive + 4 steer) | applyRequest() | CTRE Swerve API |
What’s Next?
Now that you understand what subsystems are and how they control hardware, it’s time to learn about commands — the code that tells subsystems when and how to act. In Lesson 2.2: Commands, you’ll see how AutoShootCommand coordinates the turret, flywheels, and feeder to score a note.
Remember: subsystems = capabilities, commands = instructions. Subsystems answer “what can the robot do?” and commands answer “when should it do it?”