How the PID Controller Works
This page explains how Pid turns an error into a
command, one term at a time: proportional, then derivative, then integral.
Two refinements a flight controller cannot do without come after that:
filtering the derivative, and anti-windup.
Every animation plays a trace recorded from the controller’s own C++, and the scenario is linked under each player. The three widgets with sliders rerun that C++, compiled to WebAssembly, in your browser. The plants the controller drives are simple models, the same ones its closed-loop tests use. The controls and keys are described on the Trace Player page.
The loop
Section titled “The loop”The first three sections fly a height loop. The plant is a double integrator, . The command is an acceleration, and m/s² is an offset the controller does not know about: the vehicle is heavier than the controller believes. The controller sees the height , and its setpoint is m. It runs at 1 kHz.
The controller computes
and the three runs below add one term at a time.
1. Proportional: push in proportion to the error
Section titled “1. Proportional: push in proportion to the error”The P term pushes toward the setpoint with a force proportional to the distance from it. That is a spring.
Before you play: a spring with nothing to slow it down. Does the height settle at 1 m?
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
The vehicle starts at rest at 0 m, and the setpoint is 1 m. With only the P term (k_p = 16 /s²), the command is a spring force. The height overshoots and swings between 0 and 1.63 m with a period of 1.6 s, and keeps swinging: nothing removes energy. The swing is centred below the setpoint, at 0.81 m (dashed), because the P term can only hold up the vehicle's extra weight with an error: at 0.19 m below the setpoint it pushes 16 × 0.19 = 3 m/s².
- setpoint 1 m; the vehicle is 3 m/s² heavier than the controller believes
- ✓ passBB-V1 pd_height_error_vs_closed_form_offset = 1.22e-14 m (<= 0.001), computed by the scenario harness
- ✓ passBB-T1 pid_steady_state_error = 2.22e-4 m (<= 0.001), computed by the scenario harness
Two problems show. The loop does not settle, and it is centred in the wrong place. The next two terms fix one each.
2. Derivative: damp the motion
Section titled “2. Derivative: damp the motion”The D term pushes against the vehicle’s speed, like a shock absorber. Pid takes the rate of the measurement, , rather than of the error. While the setpoint is constant the two are equal, but a jump in the setpoint does not make the D term jump. The command still jumps, through the P term, but without a derivative kick on top. This is called derivative on measurement.
Before you play: with damping, the height settles. Where?
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
The same start, with a D term added (k_d = 8 /s). The P-only run swings as before. The PD run rises without overshoot and settles within about 2 s. It settles at 0.81 m, the same offset the P-only run swings about: damping removes the swing, not the offset. The check under the player compares the PD height at 10 s with the closed-form offset r + d/k_p.
- setpoint 1 m; the vehicle is 3 m/s² heavier than the controller believes
- ✓ passBB-V1 pd_height_error_vs_closed_form_offset = 1.22e-14 m (<= 0.001), computed by the scenario harness
- ✓ passBB-T1 pid_steady_state_error = 2.22e-4 m (<= 0.001), computed by the scenario harness
The gains here give a critically damped loop: with . A smaller lets it overshoot, a larger one makes it slower.
3. Integral: remove the offset
Section titled “3. Integral: remove the offset”The I term adds up the error over time. While any error remains, the integral keeps growing, so the only place the loop can come to rest is where the error is zero. There, the integral alone supplies the 3 m/s² the P term had to hold up with an error.
Before you play: how large does the integral grow?
Predict, then try it: set k_d to 0. What happens to the PID run? Then set k_d back to 8 and make k_i large, say 30. For the ideal continuous loop, the rule is that k_d·k_p must exceed k_i.
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
The same start, with the full PID (k_i = 8 /s³). At first the PID run follows the PD run, and then the integral pulls it on up to the setpoint. It overshoots by 6 %, then settles: within 2 % of the setpoint after 4.1 s, and within 0.3 mm at 10 s. The integral rises to the 3 m/s² the extra weight takes (dashed) and stays there. The P term then contributes nothing, because the error is zero.
- setpoint 1 m; the vehicle is 3 m/s² heavier than the controller believes
- ✓ passBB-V1 pd_height_error_vs_closed_form_offset = 1.22e-14 m (<= 0.001), computed by the scenario harness
- ✓ passBB-T1 pid_steady_state_error = 2.22e-4 m (<= 0.001), computed by the scenario harness
The integral makes the loop slower to settle and adds overshoot, the price of removing the offset. It is also what can make the loop unstable. For the ideal loop, continuous and with no derivative filter, the characteristic polynomial is , which is stable only while . The widget’s loop runs at 1 kHz with a 5 ms filter, which moves that boundary a little, so treat the rule as a guide near its edge. With the PID run swings wider and wider. At with it swings too, between −0.9 and 2.2 m, with its command hitting the ±20 m/s² limit, while the PD run settles. The checks under the player then fail, which is the point: the gains are outside what the loop tolerates.
4. Filter the derivative
Section titled “4. Filter the derivative”A real sensor is noisy, and the D term differentiates the noise. Height noise of ±1 mm, read at 1 kHz, is a rate of up to ±2 m/s from one sample to the next. Times , that is a command that jumps by up to 16 m/s² every millisecond.
Pid filters the measured rate with a first-order low-pass of time
constant (derivative_tau). The rate is then smooth, but late by
about .
Before you play: the filtered run uses τ = 5 ms. How much smoother is its command, and how much later does it settle?
Predict, then try it: make τ larger. Up to about 300 ms the step response hardly changes. Past that, watch the overshoot.
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
Two copies of the height loop hold 1 m, with ±1 mm of sensor noise from a seeded generator, the same noise in both. At 1 s the setpoint steps to 1.2 m. The raw run (τ = 0) differentiates every sample of noise: its command has an RMS of 6.8 m/s², with peaks of 16 m/s². The filtered run (τ = 5 ms) has a command eight times smaller in RMS, while its height traces the same path. Both overshoot the step by 17 %, from the integral, not the noise. At the setpoint step, each command jumps by 3.2 m/s²: that is the P term, k_p × 0.2 m. There is no derivative kick on top, because the D term reads the measurement, which does not jump.
- setpoint 1.2 m
The filter costs nothing while is small against the loop’s own time scale, about = 0.13 s here. At τ = 0.5 s the damping arrives so late that the overshoot doubles, to 34 %. The cascade’s rate loop, which runs much faster, uses τ = 2 ms, a tenth of the motor’s time constant.
5. Saturation, windup, and anti-windup
Section titled “5. Saturation, windup, and anti-windup”Every actuator has a limit. When the controller asks for more than the actuator can give, the error stays large, and the integral keeps adding it up. When the setpoint finally comes back within reach, the wound-up integral holds the command at the limit until it has unwound. This is integrator windup.
Pid prevents it by conditional integration. When the output is at a limit and the error pushes further into it, the integral is frozen for that step. When the error pulls back, it integrates as usual.
This section flies a different loop: a first-order plant, with T = 0.1 s, such as a body rate behind a motor lag. Its actuator saturates at , so it can hold at most . Two PI controllers fly two copies of it. One has its output limits set to the actuator’s, so its anti-windup engages. The other has its limits set wide open, so its integral is never stopped, which is what a controller without anti-windup does.
Before you play: the setpoint is 1.5 for 2 s, beyond the plant’s reach, and then 0.5. How long does each run take to get to 0.5?
Predict, then try it: make the hold time longer. Does the anti-windup run take longer to recover? Does the other one?
Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed
Both runs climb to x = 1, the most the actuator can hold, and sit there. The run without anti-windup keeps integrating the 0.5 error, and its integral reaches 11 by 2 s. Its command, far past the actuator's limit of 1 (dashed), means nothing to the plant. The anti-windup run's integral stops at 0.49, where P and I together just reach the limit. At 2 s the setpoint drops to 0.5. The anti-windup run's command leaves the limit at once, and the output comes within 0.02 of 0.5 after 0.09 s. It dips to 0.43 and is within 0.01 for good by 0.5 s: that is the PI loop's own response, with its low k_p. The other run's command has to unwind from 11.5 to below 1 first, so it stays at the limit and comes within 0.02 of 0.5 only after 2.40 s.
- setpoint 1.5, beyond the actuator's reach
- setpoint 0.5, within reach
- anti-windup: within 0.02 of 0.5, 0.09 s after the drop
- no anti-windup: within 0.02 of 0.5, 2.40 s after the drop
- ✓ passBB-T1 recovery_time_to_within_0.02 = 0.088 s (<= 0.2), computed by the scenario harness
The wound-up integral grows with the time spent at the limit, and so does the recovery: about 1 s after a 0.5 s hold, and 3.5 s after a 3 s hold. The anti-windup run comes within 0.02 of 0.5 in 0.09 s whatever the hold. The check under the player is the closed-loop test’s criterion: within 0.02 of 0.5 no later than 0.2 s after the drop.
A stage after the controller can also cut its command, as the mixer does when it runs out of authority. Pid then needs to be told, or its integral winds up behind that cut. The deployment guide shows it.
Where to go next
Section titled “Where to go next”- The developer guide has the control law, the contracts, and the API.
- The V&V plan lists what is verified, and how.
- The deployment guide covers choosing limits and the filter, wiring external saturation, and timing.