← Back to blog

What a PID controller is and how to implement it in firmware

If your product has to keep a variable at a target value —a motor's speed, an oven's temperature, an arm's position, a pump's pressure— sooner or later you'll meet PID control. It's the most widely used control algorithm in industry, and understanding it well is the difference between a product that responds smoothly and one that oscillates, overshoots or reacts too late.

What a PID controller is

A PID controller (Proportional-Integral-Derivative) is an algorithm that continuously adjusts an output so a measured value gets close to a desired value, called the reference or setpoint. It does this by computing the error (the difference between what you want and what you have) and correcting it through three components working together.

Setpoint Σ + error e(t) Controller P · I · D u(t) Process / Plant output y(t) Sensor
Closed-loop control: the sensor measures the real output, it's compared with the setpoint and the PID corrects the error.

The three components: P, I and D

  • Proportional (P): reacts to the current error. The further you are from target, the harder it corrects. It speeds up the response, but on its own leaves a small steady-state error and, if too high, causes oscillation.
  • Integral (I): accumulates past error. This is what removes that steady-state error: as long as a gap remains, it keeps pushing until it nails the value. In exchange, overdoing it slows things down and can cause overshoot.
  • Derivative (D): anticipates future error by looking at how fast it changes. It acts like a brake that smooths the response and reduces overshoot, but it amplifies sensor noise, so use it carefully.

The controller output is the sum of the three: u(t) = Kp·e + Ki·∫e·dt + Kd·de/dt. Tuning the gains Kp, Ki and Kd is what defines the system's character.

A simple example: a motor's speed

Picture a motor that must spin at 1,000 rpm. You measure the real rpm with an encoder, subtract them from the desired 1,000 and get the error. The PID turns that error into a PWM signal driving the motor: if it's slow, raise the PWM; if it overshoots, lower it. The encoder measures again and the cycle repeats tens or hundreds of times per second. The result is a motor that holds its speed even as the load changes —exactly what a car's cruise control does on a hill—.

How to implement it in firmware

On a microcontroller, the PID is computed in a fixed-interval loop (for example, every 10 ms from a timer interrupt). A basic discrete implementation in C looks like this:

typedef struct {
    float kp, ki, kd;
    float integral, prev_error;
    float out_min, out_max;   // output limits
} pid_t;

float pid_update(pid_t *pid, float setpoint, float measured, float dt) {
    float error = setpoint - measured;
    pid->integral += error * dt;
    float derivative = (error - pid->prev_error) / dt;
    pid->prev_error = error;

    float out = pid->kp * error
              + pid->ki * pid->integral
              + pid->kd * derivative;

    // Saturation + anti-windup: don't let the integral grow unbounded
    if (out > pid->out_max) { out = pid->out_max; pid->integral -= error * dt; }
    if (out < pid->out_min) { out = pid->out_min; pid->integral -= error * dt; }
    return out;
}

The details that separate a toy PID from a professional one live around that formula: a constant, well-measured dt, output saturation with anti-windup (so the integral doesn't blow up when the actuator hits its limit), filtering of the derivative term, and the right arithmetic (integer or fixed-point) on MCUs without a floating-point unit.

How to tune a PID

  • Start with P only: raise it until the system responds quickly but just before it starts to oscillate.
  • Add I gradually to remove the steady-state error, without overdoing it (it causes overshoot).
  • Use D sparingly to dampen, only if sensor noise allows it.
  • Methods like Ziegler-Nichols give a starting point, but fine tuning is almost always empirical and depends on your real hardware.

Common mistakes

  • Computing the PID with a variable dt (for example inside a loop() with no timing control): results become unpredictable.
  • Forgetting anti-windup: the system responds with a huge delay after saturating.
  • Applying the derivative directly on a noisy signal, injecting vibration into the actuator.
  • Trying to solve everything with PID when the process has large delays or nonlinearities: sometimes another strategy is needed.

Conclusion

A well-implemented PID is the difference between a product that feels solid and one that vibrates, overheats or reacts late. The concept is simple, but making it robust on a real microcontroller —with its timing, its noise and the actuator's limits— is fine firmware work. At Regular Solids we design and implement control loops for motors, temperature, power and motion in embedded products. If you have a product that needs to control something precisely, tell us about it.