Combining Motion Profiling and PID in Command-Based

Note

For a description of the WPILib PID control features used by these command-based wrappers, see PID Control in WPILib.

A common FRC® controls solution is to pair a trapezoidal motion profile for setpoint generation with a PID controller for setpoint tracking. To facilitate this, WPILib includes its own ProfiledPIDController class. The following example is from the RapidReactCommandBot example project (Java, C++) and shows how ProfiledPIDController can be used within the command-based framework to turn a drivetrain to a specified angle:

  5package edu.wpi.first.wpilibj.examples.rapidreactcommandbot.subsystems;
  6
  7import edu.wpi.first.epilogue.Logged;
  8import edu.wpi.first.epilogue.NotLogged;
  9import edu.wpi.first.math.controller.ProfiledPIDController;
 10import edu.wpi.first.math.controller.SimpleMotorFeedforward;
 11import edu.wpi.first.math.trajectory.TrapezoidProfile;
 12import edu.wpi.first.util.sendable.SendableRegistry;
 13import edu.wpi.first.wpilibj.ADXRS450_Gyro;
 14import edu.wpi.first.wpilibj.Encoder;
 15import edu.wpi.first.wpilibj.RobotController;
 16import edu.wpi.first.wpilibj.drive.DifferentialDrive;
 17import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.DriveConstants;
 18import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
 19import edu.wpi.first.wpilibj2.command.Command;
 20import edu.wpi.first.wpilibj2.command.SubsystemBase;
 21import java.util.function.DoubleSupplier;
 22
 23@Logged
 24public class Drive extends SubsystemBase {
 25  // The motors on the left side of the drive.
 26  private final PWMSparkMax m_leftLeader = new PWMSparkMax(DriveConstants.kLeftMotor1Port);
 27  private final PWMSparkMax m_leftFollower = new PWMSparkMax(DriveConstants.kLeftMotor2Port);
 28
 29  // The motors on the right side of the drive.
 30  private final PWMSparkMax m_rightLeader = new PWMSparkMax(DriveConstants.kRightMotor1Port);
 31  private final PWMSparkMax m_rightFollower = new PWMSparkMax(DriveConstants.kRightMotor2Port);
 32
 33  // The robot's drive
 34  @NotLogged // Would duplicate motor data, there's no point sending it twice
 35  private final DifferentialDrive m_drive =
 36      new DifferentialDrive(m_leftLeader::set, m_rightLeader::set);
 37
 38  // The left-side drive encoder
 39  private final Encoder m_leftEncoder =
 40      new Encoder(
 41          DriveConstants.kLeftEncoderPorts[0],
 42          DriveConstants.kLeftEncoderPorts[1],
 43          DriveConstants.kLeftEncoderReversed);
 44
 45  // The right-side drive encoder
 46  private final Encoder m_rightEncoder =
 47      new Encoder(
 48          DriveConstants.kRightEncoderPorts[0],
 49          DriveConstants.kRightEncoderPorts[1],
 50          DriveConstants.kRightEncoderReversed);
 51
 52  private final ADXRS450_Gyro m_gyro = new ADXRS450_Gyro();
 53  private final ProfiledPIDController m_controller =
 54      new ProfiledPIDController(
 55          DriveConstants.kTurnP,
 56          DriveConstants.kTurnI,
 57          DriveConstants.kTurnD,
 58          new TrapezoidProfile.Constraints(
 59              DriveConstants.kMaxTurnRateDegPerS,
 60              DriveConstants.kMaxTurnAccelerationDegPerSSquared));
 61  private final SimpleMotorFeedforward m_feedforward =
 62      new SimpleMotorFeedforward(
 63          DriveConstants.ksVolts,
 64          DriveConstants.kvVoltSecondsPerDegree,
 65          DriveConstants.kaVoltSecondsSquaredPerDegree);
 66
 67  /** Creates a new Drive subsystem. */
 68  public Drive() {
 69    SendableRegistry.addChild(m_drive, m_leftLeader);
 70    SendableRegistry.addChild(m_drive, m_rightLeader);
 71
 72    m_leftLeader.addFollower(m_leftFollower);
 73    m_rightLeader.addFollower(m_rightFollower);
 74
 75    // We need to invert one side of the drivetrain so that positive voltages
 76    // result in both sides moving forward. Depending on how your robot's
 77    // gearbox is constructed, you might have to invert the left side instead.
 78    m_rightLeader.setInverted(true);
 79
 80    // Sets the distance per pulse for the encoders
 81    m_leftEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);
 82    m_rightEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);
 83
 84    // Set the controller to be continuous (because it is an angle controller)
 85    m_controller.enableContinuousInput(-180, 180);
 86    // Set the controller tolerance - the delta tolerance ensures the robot is stationary at the
 87    // setpoint before it is considered as having reached the reference
 88    m_controller.setTolerance(
 89        DriveConstants.kTurnToleranceDeg, DriveConstants.kTurnRateToleranceDegPerS);
 90  }
 91
 92  /**
 93   * Returns a command that drives the robot with arcade controls.
 94   *
 95   * @param fwd the commanded forward movement
 96   * @param rot the commanded rotation
 97   */
 98  public Command arcadeDriveCommand(DoubleSupplier fwd, DoubleSupplier rot) {
 99    // A split-stick arcade command, with forward/backward controlled by the left
100    // hand, and turning controlled by the right.
101    return run(() -> m_drive.arcadeDrive(fwd.getAsDouble(), rot.getAsDouble()))
102        .withName("arcadeDrive");
103  }
104
105  /**
106   * Returns a command that drives the robot forward a specified distance at a specified speed.
107   *
108   * @param distanceMeters The distance to drive forward in meters
109   * @param speed The fraction of max speed at which to drive
110   */
111  public Command driveDistanceCommand(double distanceMeters, double speed) {
112    return runOnce(
113            () -> {
114              // Reset encoders at the start of the command
115              m_leftEncoder.reset();
116              m_rightEncoder.reset();
117            })
118        // Drive forward at specified speed
119        .andThen(run(() -> m_drive.arcadeDrive(speed, 0)))
120        // End command when we've traveled the specified distance
121        .until(
122            () ->
123                Math.max(m_leftEncoder.getDistance(), m_rightEncoder.getDistance())
124                    >= distanceMeters)
125        // Stop the drive when the command ends
126        .finallyDo(interrupted -> m_drive.stopMotor());
127  }
128
129  /**
130   * Returns a command that turns to robot to the specified angle using a motion profile and PID
131   * controller.
132   *
133   * @param angleDeg The angle to turn to
134   */
135  public Command turnToAngleCommand(double angleDeg) {
136    return startRun(
137            () -> m_controller.reset(m_gyro.getRotation2d().getDegrees()),
138            () ->
139                m_drive.arcadeDrive(
140                    0,
141                    m_controller.calculate(m_gyro.getRotation2d().getDegrees(), angleDeg)
142                        // Divide feedforward voltage by battery voltage to normalize it to [-1, 1]
143                        + m_feedforward.calculate(m_controller.getSetpoint().velocity)
144                            / RobotController.getBatteryVoltage()))
145        .until(m_controller::atGoal)
146        .finallyDo(() -> m_drive.arcadeDrive(0, 0));
147  }
148}
 5#pragma once
 6
 7#include <functional>
 8
 9#include <frc/ADXRS450_Gyro.h>
10#include <frc/Encoder.h>
11#include <frc/controller/ProfiledPIDController.h>
12#include <frc/controller/SimpleMotorFeedforward.h>
13#include <frc/drive/DifferentialDrive.h>
14#include <frc/motorcontrol/PWMSparkMax.h>
15#include <frc2/command/CommandPtr.h>
16#include <frc2/command/SubsystemBase.h>
17#include <units/angle.h>
18#include <units/length.h>
19
20#include "Constants.h"
21
22class Drive : public frc2::SubsystemBase {
23 public:
24  Drive();
25  /**
26   * Returns a command that drives the robot with arcade controls.
27   *
28   * @param fwd the commanded forward movement
29   * @param rot the commanded rotation
30   */
31  [[nodiscard]]
32  frc2::CommandPtr ArcadeDriveCommand(std::function<double()> fwd,
33                                      std::function<double()> rot);
34
35  /**
36   * Returns a command that drives the robot forward a specified distance at a
37   * specified speed.
38   *
39   * @param distance The distance to drive forward in meters
40   * @param speed The fraction of max speed at which to drive
41   */
42  [[nodiscard]]
43  frc2::CommandPtr DriveDistanceCommand(units::meter_t distance, double speed);
44
45  /**
46   * Returns a command that turns to robot to the specified angle using a motion
47   * profile and PID controller.
48   *
49   * @param angle The angle to turn to
50   */
51  [[nodiscard]]
52  frc2::CommandPtr TurnToAngleCommand(units::degree_t angle);
53
54 private:
55  frc::PWMSparkMax m_leftLeader{DriveConstants::kLeftMotor1Port};
56  frc::PWMSparkMax m_leftFollower{DriveConstants::kLeftMotor2Port};
57  frc::PWMSparkMax m_rightLeader{DriveConstants::kRightMotor1Port};
58  frc::PWMSparkMax m_rightFollower{DriveConstants::kRightMotor2Port};
59
60  frc::DifferentialDrive m_drive{
61      [&](double output) { m_leftLeader.Set(output); },
62      [&](double output) { m_rightLeader.Set(output); }};
63
64  frc::Encoder m_leftEncoder{DriveConstants::kLeftEncoderPorts[0],
65                             DriveConstants::kLeftEncoderPorts[1],
66                             DriveConstants::kLeftEncoderReversed};
67  frc::Encoder m_rightEncoder{DriveConstants::kRightEncoderPorts[0],
68                              DriveConstants::kRightEncoderPorts[1],
69                              DriveConstants::kRightEncoderReversed};
70
71  frc::ADXRS450_Gyro m_gyro;
72
73  frc::ProfiledPIDController<units::radians> m_controller{
74      DriveConstants::kTurnP,
75      DriveConstants::kTurnI,
76      DriveConstants::kTurnD,
77      {DriveConstants::kMaxTurnRate, DriveConstants::kMaxTurnAcceleration}};
78  frc::SimpleMotorFeedforward<units::radians> m_feedforward{
79      DriveConstants::ks, DriveConstants::kv, DriveConstants::ka};
80};
 5#include "subsystems/Drive.h"
 6
 7#include <utility>
 8
 9#include <frc/RobotController.h>
10#include <frc2/command/Commands.h>
11
12Drive::Drive() {
13  wpi::SendableRegistry::AddChild(&m_drive, &m_leftLeader);
14  wpi::SendableRegistry::AddChild(&m_drive, &m_rightLeader);
15
16  m_leftLeader.AddFollower(m_leftFollower);
17  m_rightLeader.AddFollower(m_rightFollower);
18
19  // We need to invert one side of the drivetrain so that positive voltages
20  // result in both sides moving forward. Depending on how your robot's
21  // gearbox is constructed, you might have to invert the left side instead.
22  m_rightLeader.SetInverted(true);
23
24  // Sets the distance per pulse for the encoders
25  m_leftEncoder.SetDistancePerPulse(DriveConstants::kEncoderDistancePerPulse);
26  m_rightEncoder.SetDistancePerPulse(DriveConstants::kEncoderDistancePerPulse);
27
28  // Set the controller to be continuous (because it is an angle controller)
29  m_controller.EnableContinuousInput(-180_deg, 180_deg);
30  // Set the controller tolerance - the delta tolerance ensures the robot is
31  // stationary at the setpoint before it is considered as having reached the
32  // reference
33  m_controller.SetTolerance(DriveConstants::kTurnTolerance,
34                            DriveConstants::kTurnRateTolerance);
35}
36
37frc2::CommandPtr Drive::ArcadeDriveCommand(std::function<double()> fwd,
38                                           std::function<double()> rot) {
39  return Run([this, fwd = std::move(fwd), rot = std::move(rot)] {
40           m_drive.ArcadeDrive(fwd(), rot());
41         })
42      .WithName("ArcadeDrive");
43}
44
45frc2::CommandPtr Drive::DriveDistanceCommand(units::meter_t distance,
46                                             double speed) {
47  return RunOnce([this] {
48           // Reset encoders at the start of the command
49           m_leftEncoder.Reset();
50           m_rightEncoder.Reset();
51         })
52      // Drive forward at specified speed
53      .AndThen(Run([this, speed] { m_drive.ArcadeDrive(speed, 0.0); }))
54      .Until([this, distance] {
55        return units::math::max(units::meter_t(m_leftEncoder.GetDistance()),
56                                units::meter_t(m_rightEncoder.GetDistance())) >=
57               distance;
58      })
59      // Stop the drive when the command ends
60      .FinallyDo([this](bool interrupted) { m_drive.StopMotor(); });
61}
62
63frc2::CommandPtr Drive::TurnToAngleCommand(units::degree_t angle) {
64  return StartRun(
65             [this] { m_controller.Reset(m_gyro.GetRotation2d().Degrees()); },
66             [this, angle] {
67               m_drive.ArcadeDrive(
68                   0, m_controller.Calculate(m_gyro.GetRotation2d().Degrees(),
69                                             angle) +
70                          // Divide feedforward voltage by battery voltage to
71                          // normalize it to [-1, 1]
72                          m_feedforward.Calculate(
73                              m_controller.GetSetpoint().velocity) /
74                              frc::RobotController::GetBatteryVoltage());
75             })
76      .Until([this] { return m_controller.AtGoal(); })
77      .FinallyDo([this] { m_drive.ArcadeDrive(0, 0); });
78}

turnToAngleCommand uses a ProfiledPIDController to smoothly turn the drivetrain. The startRun command factory is used to reset the ProfiledPIDController when the command is scheduled to avoid unwanted behavior, and to calculate PID and feedforward outputs to pass into the arcadeDrive method in order to drive the robot. The command is decorated using the until decorator to end the command when the ProfiledPIDController is finished with the profile. To ensure the drivetrain stops when the command ends, the finallyDo decorator is used to stop the drivetrain by setting the speed to zero.