- FRC Game Manual
- FRC Game Q&A

# Combining Motion Profiling and PID Control with ProfiledPIDController

Note

For a guide on implementing the `ProfiledPIDController` class in the command-based framework framework, see Combining Motion Profiling and PID in Command-Based.

In the previous article, we saw how to use the `TrapezoidProfile` class to create and use a trapezoidal motion profile. The example code from that article demonstrates manually composing the `TrapezoidProfile` class with the external PID control feature of a “smart” motor controller.

This combination of functionality (a motion profile for generating setpoints combined with a PID controller for following them) is extremely common. To facilitate this, WPILib comes with a `ProfiledPIDController` class (Java, C++, `Python`) that does most of the work of combining these two functionalities. The API of the `ProfiledPIDController` is very similar to that of the `PIDController`, allowing users to add motion profiling to a PID-controlled mechanism with very few changes to their code.

## Using the ProfiledPIDController class

Note

In C++, the `ProfiledPIDController` class is templated on the unit type used for distance measurements, which may be angular or linear. The passed-in values must have units consistent with the distance units, or a compile-time error will be thrown. For more information on C++ units, see The C++ Units Library.

Note

Much of the functionality of `ProfiledPIDController` is effectively identical to that of `PIDController`. Accordingly, this article will only cover features that are substantially-changed to accommodate the motion profiling functionality. For information on standard `PIDController` features, see PID Control in WPILib.

### Constructing a ProfiledPIDController

Note

C++ is often able to infer the type of the inner classes, and thus a simple initializer list (without the class name) can be sent as a parameter. The full class name is included in the example below for clarity.

Creating a `ProfiledPIDController` is nearly identical to creating a PIDController. The only difference is the need to supply a set of trapezoidal profile constraints, which will be automatically forwarded to the internally-generated `TrapezoidProfile` instances:

JAVA

    // Creates a ProfiledPIDController
    // Max velocity is 5 meters per second
    // Max acceleration is 10 meters per second
    ProfiledPIDController controller = new ProfiledPIDController(
      kP, kI, kD,
      new TrapezoidProfile.Constraints(5, 10));

C++

    // Creates a ProfiledPIDController
    // Max velocity is 5 meters per second
    // Max acceleration is 10 meters per second
    frc::ProfiledPIDController<units::meters> controller(
      kP, kI, kD,
      frc::TrapezoidProfile<units::meters>::Constraints{5_mps, 10_mps_sq});

PYTHON

    from wpimath.controller import ProfiledPIDController
    from wpimath.trajectory import TrapezoidProfile
      # Creates a ProfiledPIDController
    # Max velocity is 5 meters per second
    # Max acceleration is 10 meters per second
    controller = ProfiledPIDController(
      kP, kI, kD,
      TrapezoidProfile.Constraints(5, 10))

### Goal vs Setpoint

A major difference between a standard `PIDController` and a `ProfiledPIDController` is that the actual setpoint of the control loop is not directly specified by the user. Rather, the user specifies a goal position or state, and the setpoint for the controller is computed automatically from the generated motion profile between the current state and the goal. So, while the user-side call looks mostly identical:

JAVA

    // Calculates the output of the PID algorithm based on the sensor reading
    // and sends it to a motor
    motor.set(controller.calculate(encoder.getDistance(), goal));

C++

    // Calculates the output of the PID algorithm based on the sensor reading
    // and sends it to a motor
    motor.Set(controller.Calculate(encoder.GetDistance(), goal));

PYTHON

    # Calculates the output of the PID algorithm based on the sensor reading
    # and sends it to a motor
    motor.set(controller.calculate(encoder.getDistance(), goal))

The specified `goal` value (which can be either a position value or a `TrapezoidProfile.State`, if nonzero velocity is desired) is not necessarily the current setpoint of the loop - rather, it is the eventual setpoint once the generated profile terminates.

#### Getting/Using the Setpoint

Since the `ProfiledPIDController` goal differs from the setpoint, is if often desirable to poll the current setpoint of the controller (for instance, to get values to use with feedforward). This can be done with the `getSetpoint()` method.

The returned setpoint might then be used as in the following example:

JAVA

    37  public void goToPosition(double goalPosition) {
    38    double pidVal = m_controller.calculate(m_encoder.getDistance(), goalPosition);
    39    m_motor.setVoltage(
    40        pidVal
    41            + m_feedforward.calculateWithVelocities(
    42                m_lastSpeed, m_controller.getSetpoint().velocity));
    43    m_lastSpeed = m_controller.getSetpoint().velocity;
    44  }

C++

    25  void GoToPosition(units::meter_t goalPosition) {
    26    auto pidVal = m_controller.Calculate(
    27        units::meter_t{m_encoder.GetDistance()}, goalPosition);
    28    m_motor.SetVoltage(units::volt_t{pidVal} +
    29                       m_feedforward.Calculate(
    30                           m_lastSpeed, m_controller.GetSetpoint().velocity));
    31    m_lastSpeed = m_controller.GetSetpoint().velocity;
    32  }

PYTHON

    from wpilib.controller import ProfiledPIDController
    from wpilib.controller import SimpleMotorFeedforward
    def __init__(self):
        # Assuming encoder, motor, controller are already defined
        self.lastSpeed = 0
        # Assuming feedforward is a SimpleMotorFeedforward object
        self.feedforward = SimpleMotorFeedforward(ks=0.0, kv=0.0, ka=0.0)
    def goToPosition(self, goalPosition: float):
        pidVal = self.controller.calculate(self.encoder.getDistance(), goalPosition)
        self.motor.setVoltage(
            pidVal
            + self.feedforward.calculate(self.lastSpeed, self.controller.getSetpoint().velocity))
        self.lastSpeed = self.controller.getSetpoint().velocity

## Complete Usage Example

A more complete example of `ProfiledPIDController` usage is provided in the ElevatorProfilePID example project (Java, C++, Python):

JAVA

     5package edu.wpi.first.wpilibj.examples.elevatorprofiledpid;
     6
     7import edu.wpi.first.math.controller.ElevatorFeedforward;
     8import edu.wpi.first.math.controller.ProfiledPIDController;
     9import edu.wpi.first.math.trajectory.TrapezoidProfile;
    10import edu.wpi.first.wpilibj.Encoder;
    11import edu.wpi.first.wpilibj.Joystick;
    12import edu.wpi.first.wpilibj.TimedRobot;
    13import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
    14
    15@SuppressWarnings("PMD.RedundantFieldInitializer")
    16public class Robot extends TimedRobot {
    17  private static double kDt = 0.02;
    18  private static double kMaxVelocity = 1.75;
    19  private static double kMaxAcceleration = 0.75;
    20  private static double kP = 1.3;
    21  private static double kI = 0.0;
    22  private static double kD = 0.7;
    23  private static double kS = 1.1;
    24  private static double kG = 1.2;
    25  private static double kV = 1.3;
    26
    27  private final Joystick m_joystick = new Joystick(1);
    28  private final Encoder m_encoder = new Encoder(1, 2);
    29  private final PWMSparkMax m_motor = new PWMSparkMax(1);
    30
    31  // Create a PID controller whose setpoint's change is subject to maximum
    32  // velocity and acceleration constraints.
    33  private final TrapezoidProfile.Constraints m_constraints =
    34      new TrapezoidProfile.Constraints(kMaxVelocity, kMaxAcceleration);
    35  private final ProfiledPIDController m_controller =
    36      new ProfiledPIDController(kP, kI, kD, m_constraints, kDt);
    37  private final ElevatorFeedforward m_feedforward = new ElevatorFeedforward(kS, kG, kV);
    38
    39  public Robot() {
    40    m_encoder.setDistancePerPulse(1.0 / 360.0 * 2.0 * Math.PI * 1.5);
    41  }
    42
    43  @Override
    44  public void teleopPeriodic() {
    45    if (m_joystick.getRawButtonPressed(2)) {
    46      m_controller.setGoal(5);
    47    } else if (m_joystick.getRawButtonPressed(3)) {
    48      m_controller.setGoal(0);
    49    }
    50
    51    // Run controller and update motor output
    52    m_motor.setVoltage(
    53        m_controller.calculate(m_encoder.getDistance())
    54            + m_feedforward.calculate(m_controller.getSetpoint().velocity));
    55  }
    56}

C++

     5#include <numbers>
     6
     7#include <frc/Encoder.h>
     8#include <frc/Joystick.h>
     9#include <frc/TimedRobot.h>
    10#include <frc/controller/ElevatorFeedforward.h>
    11#include <frc/controller/ProfiledPIDController.h>
    12#include <frc/motorcontrol/PWMSparkMax.h>
    13#include <frc/trajectory/TrapezoidProfile.h>
    14#include <units/acceleration.h>
    15#include <units/length.h>
    16#include <units/time.h>
    17#include <units/velocity.h>
    18#include <units/voltage.h>
    19
    20class Robot : public frc::TimedRobot {
    21 public:
    22  static constexpr units::second_t kDt = 20_ms;
    23
    24  Robot() {
    25    m_encoder.SetDistancePerPulse(1.0 / 360.0 * 2.0 * std::numbers::pi * 1.5);
    26  }
    27
    28  void TeleopPeriodic() override {
    29    if (m_joystick.GetRawButtonPressed(2)) {
    30      m_controller.SetGoal(5_m);
    31    } else if (m_joystick.GetRawButtonPressed(3)) {
    32      m_controller.SetGoal(0_m);
    33    }
    34
    35    // Run controller and update motor output
    36    m_motor.SetVoltage(
    37        units::volt_t{
    38            m_controller.Calculate(units::meter_t{m_encoder.GetDistance()})} +
    39        m_feedforward.Calculate(m_controller.GetSetpoint().velocity));
    40  }
    41
    42 private:
    43  static constexpr units::meters_per_second_t kMaxVelocity = 1.75_mps;
    44  static constexpr units::meters_per_second_squared_t kMaxAcceleration =
    45      0.75_mps_sq;
    46  static constexpr double kP = 1.3;
    47  static constexpr double kI = 0.0;
    48  static constexpr double kD = 0.7;
    49  static constexpr units::volt_t kS = 1.1_V;
    50  static constexpr units::volt_t kG = 1.2_V;
    51  static constexpr auto kV = 1.3_V / 1_mps;
    52
    53  frc::Joystick m_joystick{1};
    54  frc::Encoder m_encoder{1, 2};
    55  frc::PWMSparkMax m_motor{1};
    56
    57  // Create a PID controller whose setpoint's change is subject to maximum
    58  // velocity and acceleration constraints.
    59  frc::TrapezoidProfile<units::meters>::Constraints m_constraints{
    60      kMaxVelocity, kMaxAcceleration};
    61  frc::ProfiledPIDController<units::meters> m_controller{kP, kI, kD,
    62                                                         m_constraints, kDt};
    63  frc::ElevatorFeedforward m_feedforward{kS, kG, kV};
    64};
    65
    66#ifndef RUNNING_FRC_TESTS
    67int main() {
    68  return frc::StartRobot<Robot>();
    69}
    70#endif

PYTHON

     8import wpilib
     9import wpimath.controller
    10import wpimath.trajectory
    11import math
    12
    13
    14class MyRobot(wpilib.TimedRobot):
    15    kDt = 0.02
    16
    17    def robotInit(self) -> None:
    18        self.joystick = wpilib.Joystick(1)
    19        self.encoder = wpilib.Encoder(1, 2)
    20        self.motor = wpilib.PWMSparkMax(1)
    21
    22        # Create a PID controller whose setpoint's change is subject to maximum
    23        # velocity and acceleration constraints.
    24        self.constraints = wpimath.trajectory.TrapezoidProfile.Constraints(1.75, 0.75)
    25        self.controller = wpimath.controller.ProfiledPIDController(
    26            1.3, 0, 0.7, self.constraints, self.kDt
    27        )
    28
    29        self.encoder.setDistancePerPulse(1 / 360 * 2 * math.pi * 1.5)
    30
    31    def teleopPeriodic(self) -> None:
    32        if self.joystick.getRawButtonPressed(2):
    33            self.controller.setGoal(5)
    34        elif self.joystick.getRawButtonPressed(3):
    35            self.controller.setGoal(0)
    36
    37        # Run controller and update motor output
    38        self.motor.set(self.controller.calculate(self.encoder.getDistance()))