Skip to main content
This example demonstrates elevator simulation with advanced control using ProfiledPIDController, feedforward, and Mechanism2d visualization.

Main Robot Class

package edu.wpi.first.wpilibj.examples.elevatorsimulation;

import edu.wpi.first.wpilibj.Joystick;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.examples.elevatorsimulation.subsystems.Elevator;

public class Robot extends TimedRobot {
  private final Joystick m_joystick = new Joystick(Constants.kJoystickPort);
  private final Elevator m_elevator = new Elevator();

  @Override
  public void robotPeriodic() {
    // Update the telemetry, including mechanism visualization, regardless of mode.
    m_elevator.updateTelemetry();
  }

  @Override
  public void simulationPeriodic() {
    // Update the simulation model.
    m_elevator.simulationPeriodic();
  }

  @Override
  public void teleopPeriodic() {
    if (m_joystick.getTrigger()) {
      // Here, we set the constant setpoint of 0.75 meters.
      m_elevator.reachGoal(Constants.kSetpointMeters);
    } else {
      // Otherwise, we update the setpoint to 0.
      m_elevator.reachGoal(0.0);
    }
  }

  @Override
  public void disabledInit() {
    // This just makes sure that our simulation code knows that the motor's off.
    m_elevator.stop();
  }
}

Elevator Subsystem (Partial)

package edu.wpi.first.wpilibj.examples.elevatorsimulation.subsystems;

import edu.wpi.first.math.controller.ElevatorFeedforward;
import edu.wpi.first.math.controller.ProfiledPIDController;
import edu.wpi.first.math.system.plant.DCMotor;
import edu.wpi.first.math.trajectory.TrapezoidProfile;
import edu.wpi.first.wpilibj.Encoder;
import edu.wpi.first.wpilibj.RobotController;
import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
import edu.wpi.first.wpilibj.simulation.BatterySim;
import edu.wpi.first.wpilibj.simulation.ElevatorSim;
import edu.wpi.first.wpilibj.simulation.EncoderSim;
import edu.wpi.first.wpilibj.simulation.RoboRioSim;
import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d;
import edu.wpi.first.wpilibj.smartdashboard.MechanismLigament2d;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;

public class Elevator implements AutoCloseable {
  // This gearbox represents a gearbox containing 4 Vex 775pro motors.
  private final DCMotor m_elevatorGearbox = DCMotor.getVex775Pro(4);

  // Standard classes for controlling our elevator
  private final ProfiledPIDController m_controller =
      new ProfiledPIDController(
          kElevatorKp,
          kElevatorKi,
          kElevatorKd,
          new TrapezoidProfile.Constraints(2.45, 2.45));
  
  ElevatorFeedforward m_feedforward =
      new ElevatorFeedforward(
          kElevatorkS,  // Static gain
          kElevatorkG,  // Gravity gain
          kElevatorkV,  // Velocity gain
          kElevatorkA); // Acceleration gain
  
  private final Encoder m_encoder = new Encoder(kEncoderAChannel, kEncoderBChannel);
  private final PWMSparkMax m_motor = new PWMSparkMax(kMotorPort);

  // Simulation classes help us simulate what's going on, including gravity.
  private final ElevatorSim m_elevatorSim =
      new ElevatorSim(
          m_elevatorGearbox,
          kElevatorGearing,
          kCarriageMass,
          kElevatorDrumRadius,
          kMinElevatorHeightMeters,
          kMaxElevatorHeightMeters,
          true,
          0,
          0.01,
          0.0);
  private final EncoderSim m_encoderSim = new EncoderSim(m_encoder);

  // Create a Mechanism2d visualization of the elevator
  private final Mechanism2d m_mech2d = new Mechanism2d(20, 50);
  private final MechanismRoot2d m_mech2dRoot = m_mech2d.getRoot("Elevator Root", 10, 0);
  private final MechanismLigament2d m_elevatorMech2d =
      m_mech2dRoot.append(
          new MechanismLigament2d("Elevator", m_elevatorSim.getPositionMeters(), 90));

  public Elevator() {
    m_encoder.setDistancePerPulse(kElevatorEncoderDistPerPulse);
    SmartDashboard.putData("Elevator Sim", m_mech2d);
  }

  /** Advance the simulation. */
  public void simulationPeriodic() {
    // In this method, we update our simulation of what our elevator is doing
    // First, we set our "inputs" (voltages)
    m_elevatorSim.setInput(m_motorSim.getSpeed() * RobotController.getBatteryVoltage());

    // Next, we update it. The standard loop time is 20ms.
    m_elevatorSim.update(0.020);

    // Finally, we set our simulated encoder's readings and simulated battery voltage
    m_encoderSim.setDistance(m_elevatorSim.getPositionMeters());
    // SimBattery estimates loaded battery voltages
    RoboRioSim.setVInVoltage(
        BatterySim.calculateDefaultBatteryLoadedVoltage(m_elevatorSim.getCurrentDrawAmps()));
  }

  /**
   * Run control loop to reach and maintain goal.
   *
   * @param goal the position to maintain
   */
  public void reachGoal(double goal) {
    m_controller.setGoal(goal);

    // With the setpoint value we run PID control like normal
    double pidOutput = m_controller.calculate(m_encoder.getDistance());
    double feedforwardOutput = m_feedforward.calculate(m_controller.getSetpoint().velocity);
    m_motor.setVoltage(pidOutput + feedforwardOutput);
  }

  /** Update telemetry, including the mechanism visualization. */
  public void updateTelemetry() {
    // Update elevator visualization with position
    m_elevatorMech2d.setLength(m_encoder.getDistance());
  }

  public void stop() {
    m_controller.setGoal(0.0);
    m_motor.set(0.0);
  }
}

What This Example Demonstrates

ElevatorSim

Simulates a vertical elevator mechanism with realistic physics:
ElevatorSim m_elevatorSim = new ElevatorSim(
    m_elevatorGearbox,           // DCMotor gearbox (4x Vex 775 Pro)
    kElevatorGearing,            // Gear reduction
    kCarriageMass,               // Mass of carriage + payload (kg)
    kElevatorDrumRadius,         // Radius of drum/pulley (meters)
    kMinElevatorHeightMeters,    // Minimum height (0.0)
    kMaxElevatorHeightMeters,    // Maximum height (1.25m)
    true,                        // Simulate gravity
    0,                           // Starting height
    0.01,                        // Measurement std dev
    0.0);                        // Measurement noise
Key Parameters:
  • Carriage Mass: Total mass being lifted (elevator carriage + payload)
  • Drum Radius: Determines linear distance per rotation
  • Gravity: Elevator will fall if not powered (unlike arms which rotate)

ProfiledPIDController

Adds motion profiling to PID control for smooth, constrained motion:
ProfiledPIDController m_controller = new ProfiledPIDController(
    kP, kI, kD,
    new TrapezoidProfile.Constraints(
        2.45,  // Max velocity (m/s)
        2.45   // Max acceleration (m/s²)
    ));

// Set goal and calculate control output
m_controller.setGoal(goal);
double pidOutput = m_controller.calculate(currentPosition);
Advantages over regular PID:
  • Smooth acceleration/deceleration
  • Respects velocity and acceleration limits
  • No jerky motion
  • Predictable timing

ElevatorFeedforward

Compensates for known system dynamics:
ElevatorFeedforward m_feedforward = new ElevatorFeedforward(
    kS,  // Static friction voltage (volts)
    kG,  // Gravity voltage (volts) - voltage to hold position
    kV,  // Velocity gain (volts per meter/second)
    kA   // Acceleration gain (volts per meter/second²)
);

// Calculate feedforward voltage based on desired velocity
double feedforwardOutput = m_feedforward.calculate(desiredVelocity);
Feedforward components:
  • kS: Overcomes static friction to start moving
  • kG: Counteracts gravity (specific to elevators!)
  • kV: Maintains constant velocity
  • kA: Provides power during acceleration

Combined Control

Feedback (PID) + Feedforward for optimal control:
public void reachGoal(double goal) {
  m_controller.setGoal(goal);
  
  // PID: Corrects for errors
  double pidOutput = m_controller.calculate(m_encoder.getDistance());
  
  // Feedforward: Compensates for known system behavior
  double feedforwardOutput = m_feedforward.calculate(m_controller.getSetpoint().velocity);
  
  // Combine both
  m_motor.setVoltage(pidOutput + feedforwardOutput);
}
Why combine them?
  • Feedforward: Does the “heavy lifting” based on physics model
  • PID: Corrects for modeling errors and disturbances
  • Result: Faster response, less overshoot, better tracking

Simulation Update Loop

public void simulationPeriodic() {
  // 1. Get motor output voltage
  m_elevatorSim.setInput(m_motorSim.getSpeed() * RobotController.getBatteryVoltage());
  
  // 2. Update physics (20ms timestep)
  m_elevatorSim.update(0.020);
  
  // 3. Update simulated encoder
  m_encoderSim.setDistance(m_elevatorSim.getPositionMeters());
  
  // 4. Simulate battery voltage sag
  RoboRioSim.setVInVoltage(
      BatterySim.calculateDefaultBatteryLoadedVoltage(m_elevatorSim.getCurrentDrawAmps()));
}

Mechanism2d Visualization

Vertical elevator display:
Mechanism2d m_mech2d = new Mechanism2d(20, 50);  // 20 wide, 50 tall
MechanismRoot2d m_mech2dRoot = m_mech2d.getRoot("Elevator Root", 10, 0);  // Bottom center

MechanismLigament2d m_elevatorMech2d = 
    m_mech2dRoot.append(
        new MechanismLigament2d(
            "Elevator", 
            m_elevatorSim.getPositionMeters(),  // Initial height
            90));                                // Angle (90° = straight up)

// Update height in telemetry
public void updateTelemetry() {
  m_elevatorMech2d.setLength(m_encoder.getDistance());
}

Identifying Feedforward Gains

Use SysId tool to characterize your elevator:
  1. Run SysId on your robot
  2. Collect data (quasistatic and dynamic tests)
  3. Analyze to get kS, kG, kV, kA values
  4. Use these values in your code
Example values:
public static final double kElevatorkS = 0.5;   // Volts
public static final double kElevatorkG = 0.7;   // Volts (gravity compensation)
public static final double kElevatorkV = 5.0;   // V/(m/s)
public static final double kElevatorkA = 0.2;   // V/(m/s²)

Tuning ProfiledPID Gains

Start with these steps:
  1. Set velocity/acceleration constraints based on mechanism capabilities
  2. Tune kP: Increase until oscillation, then reduce by half
  3. Add kD if needed to reduce overshoot
  4. Add kI only if steady-state error persists (usually not needed with feedforward)
ProfiledPIDController m_controller = new ProfiledPIDController(
    10.0,  // kP - start here and adjust
    0.0,   // kI - usually not needed
    0.5,   // kD - add if oscillating
    new TrapezoidProfile.Constraints(2.45, 2.45));

Safety Considerations

Elevators can be dangerous! Include safety features:
// Software limits
if (height > kMaxElevatorHeightMeters) {
  m_motor.set(0);
}

// Limit switches
if (m_topLimitSwitch.get()) {
  // At top limit
  if (motorOutput > 0) {
    motorOutput = 0;  // Don't go higher
  }
}

Running in Simulation

  1. Run robot simulation
  2. Open Glass or Shuffleboard
  3. Add “Elevator Sim” Mechanism2d widget
  4. Enable teleoperated mode
  5. Press joystick trigger:
    • Elevator rises smoothly to 0.75m
    • Observe trapezoidal velocity profile
  6. Release trigger:
    • Elevator lowers back to 0m
    • Watch feedforward compensate for gravity

What to Observe

  • Smooth motion: No jerky starts/stops due to motion profiling
  • Gravity compensation: Elevator doesn’t fall when stopped (feedforward kG)
  • Velocity limiting: Elevator respects max speed constraint
  • Battery voltage: Drops when elevator is accelerating upward
  • Current draw: Higher when moving up vs. moving down

Advantages Over Simple PID

Without feedforward:
  • Large steady-state error due to gravity
  • Slow response (needs high kI to fight gravity)
  • Overshoot and oscillation
With feedforward:
  • Minimal steady-state error
  • Fast, smooth response
  • Little to no overshoot

Source Location

  • Java: wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/elevatorsimulation/
  • C++: wpilibcExamples/src/main/cpp/examples/ElevatorSimulation/

Build docs developers (and LLMs) love