|
| 1 | +<script |
| 2 | + src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" |
| 3 | + type="text/javascript"> |
| 4 | +</script> |
| 5 | + |
| 6 | +## Feedback Control: PID Algorithm |
| 7 | +PID (Proportional Integral Derivative) control is a method that issue corrective commands to move a mechanism from where it actually is, to where you want it to be using sensors that detect the difference between the two states. WPILIB and vendor libraries usually have PID implemented with a few additional features. We will first cover the underlying theory, then the implementation. |
| 8 | +## Theory |
| 9 | +Here is an intuitive explanation of PID: |
| 10 | +<iframe width="640" height="360" src="https://www.youtube.com/embed/wkfEZmsQqiA" title="What Is PID Control? | Understanding PID Control, Part 1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> |
| 11 | + |
| 12 | +As a summary, the controller has 3 parts: |
| 13 | + |
| 14 | +- kP (Proportional term): drives the position error to zero by contributing to the control signal proportionally to the current position error |
| 15 | + |
| 16 | +- kI (Integral term): drives the total accumulated error to zero by contributing to the control signal proportionally to the sum of all past errors |
| 17 | + |
| 18 | +- kD (Derivative term): drive the change of the error to zero by contributing to the control signal proportionally to the change of the error |
| 19 | + |
| 20 | +The three quantities are added together to drive the error to zero. The three constants can be "tuned" (by changing around their values) for the error to be driven faster, slower, or to be less volatile. |
| 21 | + |
| 22 | +In code, it looks something like this: |
| 23 | +``` Java |
| 24 | + |
| 25 | +// kP, kI, kD can be any value, for example kP can = 0.5 |
| 26 | +// You "tune" these values by finding the optimal value |
| 27 | +double kP = 0.2; |
| 28 | +double kI = 0.01; |
| 29 | +double kD = 0.05; |
| 30 | + |
| 31 | +double lastTimestamp = Timer.getFPGATimestamp(); |
| 32 | +double accumulatedError = 0; // for the kI term |
| 33 | +double previousError = 0; // for the kD term |
| 34 | + |
| 35 | +// runs every 20 ms |
| 36 | +@Override |
| 37 | +void periodic() { |
| 38 | + double targetAltitude = 100; |
| 39 | + drone.fly(calculate(drone.getAltitude(), targetAltitude)) |
| 40 | +} |
| 41 | + |
| 42 | +/* calculates the output of the PID controller given the error (input) */ |
| 43 | +double calculate(double currentPosition, double targetPosition) { |
| 44 | + double error = targetPosition - currentPosition; |
| 45 | + double currentTimestamp = Timer.getFPGATimestamp(); |
| 46 | + // amount of time that passed since the last time the controller was run |
| 47 | + double deltaTime = currentTimestamp - lastTimestamp; |
| 48 | + accumulatedError += kI * error * deltaTime |
| 49 | + double derivative = (error - previousError) / deltaTime; |
| 50 | + |
| 51 | + // update values for next time this method is called |
| 52 | + lastTimestamp = currentTimestamp; |
| 53 | + previousError = error; |
| 54 | + |
| 55 | + return kP * error + accumulatedError + kD * derivative; |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +For those that know calculus, the formal definition of PID control is: |
| 60 | + |
| 61 | +$$ u(t) = K_{p}e(t) + \int_{0}^{t}e(\tau)d\tau + K_{d}\frac{de}{dt} $$ |
| 62 | +where u(t) is the control effort (amount of feedback at time t), e(t) is the error at current time t, and \\(\tau\\) is the integration variable. Do not worry about the formula as long as you understand the video and how the code works. |
| 63 | + |
| 64 | +### **PID in a nutshell:** |
| 65 | + |
| 66 | +(I'm too lazy to draw all the red lines representing kI) |
| 67 | + |
| 68 | +If you want more details, WPIlib has an [article of PID](https://docs.wpilib.org/en/stable/docs/software/advanced-control/introduction/introduction-to-pid.html). |
| 69 | + |
| 70 | +## Tuning |
| 71 | +How do you find kP, kI, and kD? What are the optimal values of these constants? What is optimal? |
| 72 | + |
| 73 | +In general, the "optimal" PID controller gets the error to zero as fast as possible, while not overshooting or oscillating. For the following graphs, the red line represents the target/goal while the purple line represents the current position. With the ideal constants, you should get close to the 3rd graph. |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | +To get those ideal constants... you kind of just guess and check. WPILIB provides a nice simulation of how it is like to tune these constants. Go to this [page](https://docs.wpilib.org/en/stable/docs/software/advanced-controls/introduction/tuning-flywheel.html) and skip everything until you get to "Pure Feedback Control". Follow the instructions and see if you can get the optimal tuning solution. |
| 78 | + |
| 79 | +## Other constants/terms |
| 80 | + |
| 81 | +### kIz or "I-Zone" |
| 82 | +Sometimes, the I term can accumulate error very fast and overshoot the target. In that case, we only want the I term to be accumulating error when the error is smaller than a certain threshold. The "I-Zone", typically denoted as kIz, lets you set that threshold. |
| 83 | + |
| 84 | +!!! warning |
| 85 | + Generally, it is discouraged to use the I term because it is easy for the mechanism to overshoot. Feedforward should be used instead of the I term. We will explain why in the Feedforward section. |
| 86 | + |
| 87 | +### kF or "Feed-Forward Gain" |
| 88 | +This is a sneak peek to what will be discussed in the Feedforward section, but given the target, what is the estimated motor output necessary to get to the target? Say for example we need get a motor to a certain velocity. It probably needs a set amount of voltage to get to that speed, so we should just add it. Note that this is different from kP since kP contributes proportionally to the error, not the target. kF is usually only used for velocity or current (draw) closed loop control, and even then not really used most of the time. The content discussed in the Feed-forward section essentially does what kF does, but better. However, sometimes you will see this constant in other documentation so it is nice to know what it is. |
| 89 | + |
| 90 | +### kMaxOutput, kMinOutput |
| 91 | +Should be self-explanatory, used to make sure you don't give your mechanism too much power |
| 92 | + |
| 93 | +### `setReference()`, `setSetpoint()`, etc |
| 94 | +There are many names for "target", such as "reference", "setpoint", and more. When tuned properly, after you set the target the motor should drive to the setpoint. |
| 95 | + |
| 96 | +There are many other methods and configurations that are used to PID control which you can find in the documentation. |
| 97 | + |
| 98 | +## Implementation |
| 99 | +You remember the code used to implement PID shown near the beginning of this page? Often, one does not need to implement PID Control from the ground-up. WPILIB and many motor controllers have PID controllers built into the class. All that needs to be done is set the setpoint, kP, kI, and kD (as well as other configurations that you may want). |
| 100 | + |
| 101 | +!!! warning |
| 102 | + Ever since the last time this website was updated (2024), the team has not used motors from the CTRE Phoenix API. The information regarding the PID implementation of the CTRE Phoenix motors may be outdated. However, SparkMaxes and other motors should be updated. |
| 103 | + |
| 104 | +For example, all [`BaseMotorControllers`](https://www.ctr-electronics.com/downloads/api/cpp/html classctre_1_1phoenix_1_1motorcontrol_1_1can_1_1_base_motor_controller.html) in the CTRE Phoenix API (Talon_SRX and Victor_SPX) can do PID as follows: |
| 105 | + |
| 106 | +``` Java |
| 107 | +// Config |
| 108 | +motor.config_kP(0, kP); |
| 109 | +motor.config_kI(0, kI); |
| 110 | +motor.config_kD(0, kD); |
| 111 | +// Tell the PID loop how close we want to get to the setpoint in sensor units |
| 112 | +motor.configAllowableClosedloopError(0, closeness); |
| 113 | +// Tell the PID loop which sensor to use. In this case use a quadrature encoder |
| 114 | +motor.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder); |
| 115 | +// Move to 0 on the selected sensor |
| 116 | +motor.set(ControlMode.Position, 0); |
| 117 | +``` |
| 118 | + |
| 119 | +And for SparkMax, SparkFlex, or other Rev motor controllers, we use the [`SparkPIDController`](https://codedocs.revrobotics.com/java/com/revrobotics/sparkpidcontroller) (Take a look at the javadocs). Notice how there are other methods that let you control the velocity, acceleration, setting target, and more: |
| 120 | +``` Java |
| 121 | +CANPIDController pidController = motor.getPIDController(); |
| 122 | +// Config |
| 123 | +pidController.setOutputRange(-1.0, 1.0); |
| 124 | +pidController.setP(kP, 0); |
| 125 | +pidController.setI(kI, 0); |
| 126 | +pidController.setD(kD, 0); |
| 127 | +// Tell the PID loop how close we want to get to the setpoint in rotations |
| 128 | +pidController.setSmartMotionAllowedClosedLoopError(closeness, 0); |
| 129 | + |
| 130 | +// Move to 0 on the quadrature encoder |
| 131 | +pidController.setReference(0.0, ControlType.kPosition, 0) |
| 132 | +``` |
| 133 | + |
| 134 | +For general purposes, you can use WPILIB's [`PIDController`](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/math/controller/PIDController.html) class: |
| 135 | +``` Java |
| 136 | +PIDController pidController = new PIDController(kP, kI, kD); |
| 137 | +pidController.setSetpoint(0); |
| 138 | +// Tell the PID loop how close we want to get to the setpoint in rotations |
| 139 | +pidController.setTolerance(closeness); |
| 140 | + |
| 141 | +// This code is assumed to be in a method or command called frequently. |
| 142 | +double state; |
| 143 | +if (!pidController.atSetpoint()) { |
| 144 | + // getError() and update() are not actual functions. They are placeholders for your own code. |
| 145 | + // Get the current state of what is being controlled |
| 146 | + error = getError(); |
| 147 | + // Do something with the adjustment |
| 148 | + update(pidController.calculate(error)); |
| 149 | +} |
| 150 | +// Only use this line once the error is "close enough" |
| 151 | +pidController.reset(); |
| 152 | +``` |
| 153 | + |
| 154 | +You need to check out the documentation for each of these classes and familiarize youself with them. |
| 155 | + |
| 156 | +# Conclusion |
| 157 | +Thankfully, most of the math is handled by WPIlib or motor controller firmware. However, it is important to understand what is actually happening so that you can properly tune your control loops. |
| 158 | + |
| 159 | +But PID is just the beginning. There are many more complicated and more powerful control methods built upon PID that will be discussed later in this section, including Following Trajectories using PathPlanner and Drivetrain Characterization. In addition, PID is often not enough to properly control a mechanism, feedforward algorithms are also needed... |
| 160 | + |
| 161 | +*** |
| 162 | + |
| 163 | +> **xkcd #689: FIRST Design** |
| 164 | +> |
| 165 | +>  |
| 166 | +> |
| 167 | +> You might use PID to control the elevator. |
| 168 | +> |
| 169 | +> _<https://xkcd.com/689/>_ |
0 commit comments