Step 3: Creating a Drive Subsystem

Now that our drive is characterized, it is time to start writing our robot code proper. As mentioned before, we will use the command-based framework for our robot code. Accordingly, our first step is to write a suitable drive subsystem class.

The full drive class from the RamseteCommand Example Project (Java, C++) can be seen below. The rest of the article will describe the steps involved in writing this class.

  5package edu.wpi.first.wpilibj.examples.ramsetecommand.subsystems;
  6
  7import edu.wpi.first.math.geometry.Pose2d;
  8import edu.wpi.first.math.kinematics.DifferentialDriveOdometry;
  9import edu.wpi.first.math.kinematics.DifferentialDriveWheelSpeeds;
 10import edu.wpi.first.wpilibj.ADXRS450_Gyro;
 11import edu.wpi.first.wpilibj.Encoder;
 12import edu.wpi.first.wpilibj.drive.DifferentialDrive;
 13import edu.wpi.first.wpilibj.examples.ramsetecommand.Constants.DriveConstants;
 14import edu.wpi.first.wpilibj.interfaces.Gyro;
 15import edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup;
 16import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
 17import edu.wpi.first.wpilibj2.command.SubsystemBase;
 18
 19public class DriveSubsystem extends SubsystemBase {
 20  // The motors on the left side of the drive.
 21  private final MotorControllerGroup m_leftMotors =
 22      new MotorControllerGroup(
 23          new PWMSparkMax(DriveConstants.kLeftMotor1Port),
 24          new PWMSparkMax(DriveConstants.kLeftMotor2Port));
 25
 26  // The motors on the right side of the drive.
 27  private final MotorControllerGroup m_rightMotors =
 28      new MotorControllerGroup(
 29          new PWMSparkMax(DriveConstants.kRightMotor1Port),
 30          new PWMSparkMax(DriveConstants.kRightMotor2Port));
 31
 32  // The robot's drive
 33  private final DifferentialDrive m_drive = new DifferentialDrive(m_leftMotors, m_rightMotors);
 34
 35  // The left-side drive encoder
 36  private final Encoder m_leftEncoder =
 37      new Encoder(
 38          DriveConstants.kLeftEncoderPorts[0],
 39          DriveConstants.kLeftEncoderPorts[1],
 40          DriveConstants.kLeftEncoderReversed);
 41
 42  // The right-side drive encoder
 43  private final Encoder m_rightEncoder =
 44      new Encoder(
 45          DriveConstants.kRightEncoderPorts[0],
 46          DriveConstants.kRightEncoderPorts[1],
 47          DriveConstants.kRightEncoderReversed);
 48
 49  // The gyro sensor
 50  private final Gyro m_gyro = new ADXRS450_Gyro();
 51
 52  // Odometry class for tracking robot pose
 53  private final DifferentialDriveOdometry m_odometry;
 54
 55  /** Creates a new DriveSubsystem. */
 56  public DriveSubsystem() {
 57    // We need to invert one side of the drivetrain so that positive voltages
 58    // result in both sides moving forward. Depending on how your robot's
 59    // gearbox is constructed, you might have to invert the left side instead.
 60    m_rightMotors.setInverted(true);
 61
 62    // Sets the distance per pulse for the encoders
 63    m_leftEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);
 64    m_rightEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);
 65
 66    resetEncoders();
 67    m_odometry = new DifferentialDriveOdometry(m_gyro.getRotation2d());
 68  }
 69
 70  @Override
 71  public void periodic() {
 72    // Update the odometry in the periodic block
 73    m_odometry.update(
 74        m_gyro.getRotation2d(), m_leftEncoder.getDistance(), m_rightEncoder.getDistance());
 75  }
 76
 77  /**
 78   * Returns the currently-estimated pose of the robot.
 79   *
 80   * @return The pose.
 81   */
 82  public Pose2d getPose() {
 83    return m_odometry.getPoseMeters();
 84  }
 85
 86  /**
 87   * Returns the current wheel speeds of the robot.
 88   *
 89   * @return The current wheel speeds.
 90   */
 91  public DifferentialDriveWheelSpeeds getWheelSpeeds() {
 92    return new DifferentialDriveWheelSpeeds(m_leftEncoder.getRate(), m_rightEncoder.getRate());
 93  }
 94
 95  /**
 96   * Resets the odometry to the specified pose.
 97   *
 98   * @param pose The pose to which to set the odometry.
 99   */
100  public void resetOdometry(Pose2d pose) {
101    resetEncoders();
102    m_odometry.resetPosition(pose, m_gyro.getRotation2d());
103  }
104
105  /**
106   * Drives the robot using arcade controls.
107   *
108   * @param fwd the commanded forward movement
109   * @param rot the commanded rotation
110   */
111  public void arcadeDrive(double fwd, double rot) {
112    m_drive.arcadeDrive(fwd, rot);
113  }
114
115  /**
116   * Controls the left and right sides of the drive directly with voltages.
117   *
118   * @param leftVolts the commanded left output
119   * @param rightVolts the commanded right output
120   */
121  public void tankDriveVolts(double leftVolts, double rightVolts) {
122    m_leftMotors.setVoltage(leftVolts);
123    m_rightMotors.setVoltage(rightVolts);
124    m_drive.feed();
125  }
126
127  /** Resets the drive encoders to currently read a position of 0. */
128  public void resetEncoders() {
129    m_leftEncoder.reset();
130    m_rightEncoder.reset();
131  }
132
133  /**
134   * Gets the average distance of the two encoders.
135   *
136   * @return the average of the two encoder readings
137   */
138  public double getAverageEncoderDistance() {
139    return (m_leftEncoder.getDistance() + m_rightEncoder.getDistance()) / 2.0;
140  }
141
142  /**
143   * Gets the left drive encoder.
144   *
145   * @return the left drive encoder
146   */
147  public Encoder getLeftEncoder() {
148    return m_leftEncoder;
149  }
150
151  /**
152   * Gets the right drive encoder.
153   *
154   * @return the right drive encoder
155   */
156  public Encoder getRightEncoder() {
157    return m_rightEncoder;
158  }
159
160  /**
161   * Sets the max output of the drive. Useful for scaling the drive to drive more slowly.
162   *
163   * @param maxOutput the maximum output to which the drive will be constrained
164   */
165  public void setMaxOutput(double maxOutput) {
166    m_drive.setMaxOutput(maxOutput);
167  }
168
169  /** Zeroes the heading of the robot. */
170  public void zeroHeading() {
171    m_gyro.reset();
172  }
173
174  /**
175   * Returns the heading of the robot.
176   *
177   * @return the robot's heading in degrees, from -180 to 180
178   */
179  public double getHeading() {
180    return m_gyro.getRotation2d().getDegrees();
181  }
182
183  /**
184   * Returns the turn rate of the robot.
185   *
186   * @return The turn rate of the robot, in degrees per second
187   */
188  public double getTurnRate() {
189    return -m_gyro.getRate();
190  }
191}

Configuring the Drive Encoders

The drive encoders measure the rotation of the wheels on each side of the drive. To properly configure the encoders, we need to specify two things: the ports the encoders are plugged into, and the distance per encoder pulse. Then, we need to write methods allowing access to the encoder values from code that uses the subsystem.

Encoder Ports

The encoder ports are specified in the encoder’s constructor, like so:

35  // The left-side drive encoder
36  private final Encoder m_leftEncoder =
37      new Encoder(
38          DriveConstants.kLeftEncoderPorts[0],
39          DriveConstants.kLeftEncoderPorts[1],
40          DriveConstants.kLeftEncoderReversed);
41
42  // The right-side drive encoder
43  private final Encoder m_rightEncoder =
44      new Encoder(
45          DriveConstants.kRightEncoderPorts[0],
46          DriveConstants.kRightEncoderPorts[1],
47          DriveConstants.kRightEncoderReversed);

Encoder Distance per Pulse

The distance per pulse is specified by calling the encoder’s setDistancePerPulse method. Note that for the WPILib Encoder class, “pulse” refers to a full encoder cycle (i.e. four edges), and thus will be 1/4 the value that was specified in the SysId config. Remember, as well, that the distance should be measured in meters!

63    m_leftEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);
64    m_rightEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse);

Encoder Accessor Method

To access the values measured by the encoders, we include the following method:

Important

The returned velocities must be in meters! Because we configured the distance per pulse on the encoders above, calling getRate() will automatically apply the conversion factor from encoder units to meters. If you are not using WPILib’s Encoder class, you must perform this conversion either through the respective vendor’s API or by manually multiplying by a conversion factor.

86  /**
87   * Returns the current wheel speeds of the robot.
88   *
89   * @return The current wheel speeds.
90   */
91  public DifferentialDriveWheelSpeeds getWheelSpeeds() {
92    return new DifferentialDriveWheelSpeeds(m_leftEncoder.getRate(), m_rightEncoder.getRate());
93  }

We wrap the measured encoder values in a DifferentialDriveWheelSpeeds object for easier integration with the RamseteCommand class later on.

Configuring the Gyroscope

The gyroscope measures the rate of change of the robot’s heading (which can then be integrated to provide a measurement of the robot’s heading relative to when it first turned on). In our example, we use the Analog Devices ADXRS450 FRC Gyro Board, which has been included in the kit of parts for several years:

50  private final Gyro m_gyro = new ADXRS450_Gyro();

Gyroscope Accessor Method

To access the current heading measured by the gyroscope, we include the following method:

174  /**
175   * Returns the heading of the robot.
176   *
177   * @return the robot's heading in degrees, from -180 to 180
178   */
179  public double getHeading() {
180    return m_gyro.getRotation2d().getDegrees();
181  }

Configuring the Odometry

Now that we have our encoders and gyroscope configured, it is time to set up our drive subsystem to automatically compute its position from the encoder and gyroscope readings.

First, we create a member instance of the DifferentialDriveOdometry class:

53  // Odometry class for tracking robot pose
54  private final DifferentialDriveOdometry m_odometry;

Updating the Odometry

The odometry class must be regularly updated to incorporate new readings from the encoder and gyroscope. We accomplish this inside the subsystem’s periodic method, which is automatically called once per main loop iteration:

70  @Override
71  public void periodic() {
72    // Update the odometry in the periodic block
73    m_odometry.update(
74        m_gyro.getRotation2d(), m_leftEncoder.getDistance(), m_rightEncoder.getDistance());
75  }

Odometry Accessor Method

To access the robot’s current computed pose, we include the following method:

74  /**
75   * Returns the currently-estimated pose of the robot.
76   *
77   * @return The pose.
78   */
79  public Pose2d getPose() {
80    return m_odometry.getPoseMeters();
81  }

Voltage-Based Drive Method

Finally, we must include one additional method - a method that allows us to set the voltage to each side of the drive using the setVoltage() method of the MotorController interface. The default WPILib drive class does not include this functionality, so we must write it ourselves:

125  /**
126   * Controls the left and right sides of the drive directly with voltages.
127   *
128   * @param leftVolts the commanded left output
129   * @param rightVolts the commanded right output
130   */
131  public void tankDriveVolts(double leftVolts, double rightVolts) {
132    m_leftMotors.setVoltage(leftVolts);
133    m_rightMotors.setVoltage(rightVolts);
134    m_drive.feed();
135  }

It is very important to use the setVoltage() method rather than the ordinary set() method, as this will automatically compensate for battery “voltage sag” during operation. Since our feedforward voltages are physically-meaningful (as they are based on measured identification data), this is essential to ensuring their accuracy.