18 min read

The Hidden Magic of PID Controllers

An interactive dive into proportional, integral, derivative (PID) control systems.

controls engineering interactive

You’ve probably benefited from multiple PID controllers today without realizing it. They’re in your car’s cruise control, your home thermostat, and the robotic arm that assembled your phone. PID controllers are everywhere because they provide an elegantly simple solution to a common problem: how do you get something to go where you want, and stay there?

In this post, we’re going to build an intuition for how PID controllers work. We’ll start with the simplest possible controller and gradually add complexity. By the end, you’ll understand not just what each term does, but why it’s needed.


Pointer to Target

Let’s start with a concrete example. Imagine you have a motor attached to a pointer, like a speedometer needle. Your goal is to get the pointer to a target angle. Sounds simple enough: just power the motor until it arrives, right?

The catch is inertia. The pointer has mass, and mass resists changes in motion. If you cut the power the moment you reach the target, your momentum carries you right past it.

Try dragging the orange handle to set a new target angle and see how the system responds:

Drag orange handle to move target
Angle90.0°
Target135.0°
Error45.0°

The motor has no idea it should slow down as it approaches the target! It provides full torque up to the target angle, then switches to full reverse torque. Without a way to slow down near the target, the pointer will oscillate right around it forever. We need a smarter control system.


Proportional Control

The first insight is simple: the further you are from the target, the harder you should push. If you’re 90° away, apply a lot of power. If you’re only 2° away, apply just a little. This is called proportional control because the output is proportional to the error.

Output    =    Kperror\textcolor{#e879f9}{Output} \;\;=\;\; \textcolor{#60a5fa}{K_p} \cdot \textcolor{#f87171}{error}

Where error=TargetCurrentPositionerror= Target - Current \: Position, and KpK_p is a gain that determines how aggressive the response is.

Here’s how the control loop works:

The controller continuously compares setpoint vs measured position, calculates error, and outputs torque command

The loop runs 60 times per second:

  1. Measure the current position (feedback)
  2. Compute: error=targetcurrenterror = target − current
  3. Output: torque command proportional to the error

This creates a closed-loop system: the controller constantly adjusts based on what it observes. Now let’s see it in action:

Click or drag to set target
1.5
Error45.0°
P0.00
Output0.00

Play with the KpK_p slider and target angle. Notice that there’s a tradeoff:

  • Too low: Sluggish response, and any resistance will leave you stuck short of the target (steady-state error)
  • Too high: Fast response, but overshoots and oscillates—potentially unstable

This is the fundamental tradeoff of proportional control. Higher gain gets you there faster but makes it more likely to overshoot and oscillate aggressively.


Derivative Control

The proportional term looks at where you are. But what if we could also look at how fast you’re approaching the target? If you’re racing toward the target at high speed, you should start braking early. If you’re crawling toward it, you don’t need as much braking.

This is what the derivative term does: it responds to the rate of change of the error. This is the “D” in PID.

Output    =    Kperror    +    Kdd(error)dt\textcolor{#e879f9}{Output} \;\;=\;\; \textcolor{#60a5fa}{K_p} \cdot \textcolor{#f87171}{error} \;\;+\;\; \textcolor{#c084fc}{K_d} \cdot \textcolor{#f87171}{\frac{d(error)}{dt}}

If you’re approaching the target quickly, the derivative term applies a braking force. The purple arrow shows the contribution of the D term to the total output.

Click or drag to set target
2.0
0.80
Error45.0°
P0.00
D0.00
Output0.00

Like magic, the oscillation is almost entirely gone! The system now settles smoothly to the target instead of bouncing back and forth. Try setting KdK_d to 0 to see the difference: the oscillation returns immediately.

Watch out for noise!
Try enabling Noise and increasing KdK_d to see why D can be problematic: it amplifies sensor noise. In real systems, it’s often better to start with just PI and only add D when needed.

Steady-State Error

We’ve solved the oscillation problem with PD control, but there’s another issue lurking. Let’s add a mass to the end of the pointer. Watch what happens: the pointer can’t reach the target:

Click or drag to set target
2.0
0.80
0.50
Error45.0°
P0.00
D0.00
Output0.00

This is steady-state error. Proportional control only pushes when there’s error. To counteract the constant pull of gravity, some error must remain — otherwise there’s nothing to push against. Higher KpK_p shrinks the gap but can’t close it.

Neither P nor D can fix this problem. We need something that remembers the past and builds up force over time.


Integral Control

Here’s the key insight: if the error persists, we should keep adding force. Even a small error, given enough time, should result in a large correction. This is what the integral term does: it sums up all the past errors.

Output    =    Kperror    +    Kdd(error)dt    +    Kierrordt\textcolor{#e879f9}{Output} \;\;=\;\; \textcolor{#60a5fa}{K_p} \cdot \textcolor{#f87171}{error} \;\;+\;\; \textcolor{#c084fc}{K_d} \cdot \textcolor{#f87171}{\frac{d(error)}{dt}} \;\;+\;\; \textcolor{#4ade80}{K_i} \cdot \textcolor{#f87171}{\int error \, dt}

The integral term keeps track of persistent error. If an error has been there for a while, the integral builds up and eventually overwhelms the disturbance. Watch the II term increase to counteract the constant pull of gravity:

Click or drag to set target
2.0
0.80
0.50
0.50
Error45.0°
P0.00
D0.00
I0.00
Output0.00

The integral term eliminates steady-state error beautifully! Now the pointer reaches the target exactly, even with the mass pulling it down.

Watch out for integral windup!
You may notice that the system is not as responsive as before. Why not increase the integral gain KiK_i? Try increasing it and click the lock icon to block the motor, move the target, wait 5-10s, then release the lock. Watch the overshoot! The integral accumulated while blocked. We have to be careful not to increase KiK_i too much, or the system will become unstable. We can use clamping or some other techniques to limit this effect.

Temperature Control

Let’s see how the same principles apply to a completely different system: controlling the temperature of an oven. This is close to what you’d find in a real kitchen oven, a reflow soldering station, or an industrial furnace.

5.0
0.50
Error280°
P0
I0
Heat0%

Notice something interesting: this controller only uses P and I - no derivative term at all! Why does that work here?

The key is that thermal systems have natural damping. Heat dissipates gradually, and the thermal mass of the oven acts like a built-in low-pass filter. There’s no “momentum” that would cause the temperature to overshoot wildly like our motor pointer. The system is inherently sluggish.

This sluggishness means:

  • D isn’t needed for damping—the physics provides it
  • I is essential—we need to eliminate steady-state error from heat loss
  • P provides the basic responsiveness

Try opening the door to see disturbance rejection! Watch how the integral term builds up to compensate for the increased heat loss.

Try conditional integration
Enable Conditional I to only accumulate integral when close to target—this prevents windup during initial heating. PI control is often preferable here since D would just amplify sensor noise.

Inverted Pendulum

For our final example, let’s look at one of the most dramatic demonstrations of PID control: balancing an inverted pendulum. This is the classic “broom balancing on your palm” problem, and it’s the same principle behind Segways and rocket landing.

Click the cart to poke it

PD Control
50
8
Angle2.9°
Force0.0

The pendulum balances! But wait, watch what happens over time. Click the cart to poke it and observe: eventually the cart crashes into the edge of the track. The PD controller is doing its job of keeping the pendulum upright, but it has no concept of where the cart is on the track.

This is the classic problem of cascaded control. We have one controller for the angle, but we need another controller to manage position. The solution is to add a second PD loop:

Both loops use Σ to compute error. The position loop's bias shifts the nominal 0° to create a target θ.

Without the outer position loop, the inner angle loop always tries to keep the pendulum at 0° (vertical). The additional outer loop biases that target slightly off-vertical to push the cart back toward center:

// Outer loop: where should the pendulum lean?
positionError = targetPosition - cartPosition
targetAngle = Kp_pos * positionError + Kd_pos * cartVelocity

// Inner loop: achieve that lean angle
angleError = targetAngle - pendulumAngle  // NOT just "0 - angle"!
motorForce = Kp_angle * angleError + Kd_angle * angularVelocity

If the cart drifts right, the position loop outputs a small negative target angle, telling the pendulum to lean slightly left. This creates a force that pushes the cart back. The inner angle loop doesn’t care why it’s being told to lean; it just does its job of achieving that angle smoothly. Try moving around the target position and play with the coefficients to see the response:

Click cart to poke · Drag handle to set target

Angle Control
50
8
Position Control
2.0
4.0
Angle2.9°
Pos0.00
Force0.0

Now we have two control loops working together:

  1. Angle Control (Kpθ, Kdθ): Fast inner loop that keeps the pendulum at whatever angle it’s told
  2. Position Control (Kpx, Kdx): Slower outer loop that adjusts that target angle to control cart position

The position controller adds a gentle bias to the angle controller’s target, nudging the cart back toward center without destabilizing the balance. Try removing the derivative term from the angle controller and see how quickly the system becomes unstable.

This cascaded structure - an inner loop for fast dynamics (angle) and an outer loop for slower dynamics (position) - is extremely common in real control systems. Quadcopters use it (attitude inner loop, position outer loop), as do robotic arms and self-balancing robots.


The PID Controller

Now you’ve seen all three terms in action:

TermLooks atEffectFixesCan cause
PPresent errorPush toward targetSlow responseOvershoot, oscillation
DFuture error (rate)Brake before arrivalOvershoot, oscillationNoise sensitivity
IPast error (sum)Eliminate steady-state errorSteady-state errorWindup, slow response

Tuning Challenge

The art of PID control is choosing Kp, Ki, and Kd to work in harmony. Each system is different: the mass of the pointer, the power of the motor, the amount of friction all affect what gains work best.

Can you tune a PID controller? Try to tune the controller for each challenge. Can you find values that work for all challenges?

No mass, simple step
Par time: 1.5s
Time
0.00
Status
Ready
0.5
0.00
0.00
Your Runs
Tune the gains and hit GO!

Hold within 1° of the target for 0.5s to complete

Tuning tip
Start with P only (increase until it oscillates), add D to dampen, then add I only if there’s persistent error. Basics needs just P+D; Weighted/Heavy need I for steady-state error.

Comparison Playground

Finally, here’s a playground where you can compare P, PD, PI, and PID controllers side-by-side. All four controllers are trying to reach the same target—watch how they differ:

P Error45.0°
PD Error45.0°
PI Error45.0°
PID Error45.0°
135°
0.30

Compare P, PD, and PID responses

Click the Randomize Target button to see how each handles step changes. Increase the Mass to add a disturbance and observe:

  • P (blue): Gets close but oscillates, and has steady-state error when mass is added
  • PD (purple): Settles quickly with minimal overshoot, but still has steady-state error with mass
  • PI (amber): Eliminates steady-state error but oscillates more than PD—no damping from D term
  • PID (green): The best of both worlds—smooth response AND no steady-state error

Notice how PI and PID both eliminate steady-state error (they reach the target exactly), but PID does it more smoothly because the D term provides damping. This is the power of combining all three terms: P provides responsiveness, D provides damping, and I eliminates persistent error.


What We Learned

Let’s recap the intuition we’ve built:

Proportional (P): “Push toward the target, harder when far away”

  • Good for: fast initial response
  • Bad at: reaching the target exactly (steady-state error), not overshooting

Derivative (D): “If we’re approaching fast, start braking”

  • Good for: reducing overshoot, stabilizing oscillation
  • Bad at: handling noisy measurements

Integral (I): “If we’ve been wrong for a while, try harder”

  • Good for: eliminating persistent error, overcoming disturbances
  • Bad at: responding quickly (can cause windup and slow oscillation)

The magic of PID is that these three simple terms, combined properly, can control an enormous variety of systems—from car engines to chemical plants to the autopilot in aircraft.


Going Further

PID is just the beginning. Once you understand these fundamentals, there’s a whole world of control theory to explore:

  • Feedforward control: Don’t just react to error—predict what input you’ll need
  • Gain scheduling: Use different gains depending on operating conditions
  • Model predictive control: Optimize over a future time horizon
  • State-space control: Control multiple variables simultaneously

But PID remains the workhorse of industrial control for good reason: it’s simple, robust, and with proper tuning, good enough for the vast majority of applications.


This post was inspired by the interactive explanations at samwho.dev and ciechanow.ski.