IMU orientation and calibration viewer

Free, browser-based tool to visualize IMU orientation in real time. Connect your sensor over USB and compare Complementary, Madgwick, and Extended Kalman Filter algorithms live in 3D.

Check it out

PID implementation in C and Motor Control Example

Oct 13, 2025
PID implementation in C

 

PID Control Explained: The Complete Guide to PID Implementation in C for Motor Control

Embedded Systems · Control Theory · Tutorial

PID Control Explained: The Complete Guide to PID Implementation in C

PID is the workhorse of motor control — used to hold a speed, hit an exact position, or keep a robot balanced. This guide builds the intuition from the ground up, derives the math you actually need, gives you a clean, production-ready PID implementation in C, walks through running it on an STM32, and shows you how to tune it without the usual trial-and-error frustration. By the end, PID won't feel like a black box — it'll feel obvious.

What Is PID Control?

PID control — short for Proportional–Integral–Derivative — is a feedback control algorithm that automatically drives a system toward a desired target and keeps it there, even when the world pushes back. It is, by a wide margin, the most widely used control method in industry: an estimated 90%+ of all control loops in everything from cruise control and thermostats to drones, 3D printers, CNC machines, and industrial robots are some flavor of PID.

The reason for its dominance is simple: PID is model-free. You don't need a mathematical model of your motor, your heater, or your robot. You only need to measure the output and know the target. PID figures out the rest from three intuitive ideas — react to the error now, account for error that has built up over time, and respond to how fast the error is changing. That's it. Three numbers to tune, and a control loop that adapts to disturbances it was never explicitly told about.

In one sentence A PID controller continuously calculates the error between where you want to be and where you are, and produces a corrective command from three terms — present, past, and predicted-future error.

A Everyday Analogy: Driving a Car

Before any math, let's ground PID in something you already do without thinking. Imagine you're driving and want to hold a steady 100 km/h. Your eyes read the speedometer (the measurement), your target is 100 km/h (the setpoint), and your foot on the throttle is the command. Watch how you naturally use all three PID terms:

🅿️
Proportional — react to how far off you are. You're at 80 km/h and want 100. That's a big 20 km/h gap, so you press the throttle hard. As you approach 100, the gap shrinks and you ease off proportionally. Bigger error → bigger correction.
🅸
Integral — account for a persistent shortfall. You're driving uphill, and a steady throttle holds you at 97 km/h — close, but always 3 km/h short, every second. That nagging, persistent gap accumulates in your mind until you press a little harder to finally reach a true 100. The integral erases lingering offset that proportional alone can't.
🅳
Derivative — anticipate from the rate of change. You crest the hill and the car suddenly starts accelerating fast toward 110. You don't wait until you've overshot — you ease off now, because you can see how quickly the speed is rising. Derivative reacts to the trend, not just the value.

A good driver blends all three automatically. A PID controller does exactly the same thing, just with arithmetic instead of intuition.

Open Loop vs. Closed Loop Control

Every control system answers one question: does it watch its own output and correct itself? That single difference splits all of control into two camps, and understanding it is the foundation everything else rests on.

An open-loop system fires off a command and hopes for the best. Tell a motor "apply 5 volts" and walk away — if the load gets heavier, the battery sags, or a bearing wears, the speed drifts and nothing notices. Its accuracy depends entirely on calibration and stable conditions. It's simple, cheap, and fast, but fundamentally blind. A microwave running for two minutes is open-loop: it never checks whether your food is actually hot.

A closed-loop system measures the real output with a sensor, compares it to the setpoint, and continuously nudges the command to close the gap. That feedback is what lets it shrug off disturbances it was never told to expect. A thermostat that turns the heater on and off to hold a room at 21 °C is closed-loop. PID lives here.

Open loop

  • No feedback mechanism
  • Simple and cost-effective
  • Fast, but less accurate
  • Can't compensate for disturbances or drift

Closed loop

  • Uses sensor feedback
  • Self-correcting and accurate
  • Rejects disturbances automatically
  • More complex; can become unstable if mistuned
  Open loop — no feedback   command   Controller     Motor   output Closed loop — feedback corrects error   setpoint   +   error   PID     Motor   output     measured output (sensor feedback)
The feedback wire is the whole difference: it turns a blind command into self-correction.

The Core Idea: One Number, Three Questions

PID works on a single value — the error, the difference between where you want to be (the setpoint) and where you actually are (the measurement):

error = setpoint − measurement

From that one number, PID asks three different questions and blends the answers into a single command. This is the mental model to lock in — everything else is detail:

P

How far off am I now?

Reacts to the present error. Bigger gap → harder push.

I

How long have I been off?

Accumulates past error to erase stubborn, lingering offset.

D

How fast is it changing?

Anticipates the future from the trend, damping overshoot.

Present, past, future. That's the whole philosophy. Each term has a gain — Kp, Ki, Kd — that sets how loudly its answer counts. Now let's take each term apart, one at a time.

  setpoint       error         Kp · error     Ki · ∫ error dt       Kd · d(error)/dt             Σ   command   Motor  
Inside a PID controller: the error feeds three parallel terms that are summed into one command.

The Proportional Term (P): React to the Present

The proportional term is the engine of the controller. Its output is simply the error multiplied by a gain:

P = Kp × error

The logic is exactly what your intuition says: the further you are from the target, the harder you correct. A motor that's 500 RPM below setpoint gets a much stronger push than one that's only 10 RPM short. As the error shrinks, the correction automatically eases off, which is what lets the system settle smoothly instead of slamming into the target.

Raise Kp and the system responds faster and more aggressively. Raise it too far and it overshoots, then oscillates back and forth, and eventually becomes unstable — buzzing or hunting around the setpoint without ever settling.

The catch: steady-state error A proportional-only controller almost never reaches the target exactly. Here's why: if the error were truly zero, then P = Kp × 0 = 0, meaning the controller would output nothing. But most systems need a constant, non-zero command just to hold position — to fight gravity, friction, or a steady load. So the system settles at a point where a small residual error produces exactly the command needed to hold there. That permanent gap is called steady-state error, and no amount of Kp fully removes it. Fixing it is precisely the integral term's job.

The Integral Term (I): Erase the Past

The integral term watches the error over time. Every loop cycle, it adds the current error to a running total. As long as any error persists — even a tiny one — that accumulator keeps growing, steadily increasing the controller's output until the error is finally driven to zero.

I = Ki × Σ(error × dt)

This is the cure for steady-state error. Where the proportional term gives up once the error gets small, the integral term is relentless: a persistent 3 RPM shortfall might be invisible to P, but I will keep accumulating it cycle after cycle until that last little gap is closed. This is what lets a PID controller land exactly on the setpoint and stay there.

The trade-off is that the integral term is slow and can make the system sluggish or oscillatory if Ki is too high — it's reacting to history, so it lags. And it has one notorious failure mode you must guard against:

Integral windup — the classic PID bug Suppose your motor is commanded to full speed but a jammed load holds it back. The error stays large, so the integral accumulates… and accumulates… and accumulates, growing to an enormous value even though the output is already pinned at 100% and can't do any more. When the jam clears, that bloated integral takes a long time to "unwind," causing a massive overshoot and a sluggish recovery. The fix is anti-windup: stop accumulating the integral whenever the output is saturated. The C code below includes it.

The Derivative Term (D): Predict the Future

The derivative term looks at how fast the error is changing — its slope — and pushes back against rapid change. If the error is closing quickly, D applies a braking action before the system overshoots, acting like a shock absorber that damps oscillation and lets you settle faster.

D = Kd × (error − previous_error) / dt

A well-chosen Kd lets you run a higher Kp for a snappy response while keeping overshoot under control — the best of both worlds. It's the term that makes a fast system feel smooth rather than twitchy.

The catch: derivative amplifies noise Differentiation magnifies high-frequency content. A noisy encoder or ADC reading that jitters by a few counts each sample looks, to the derivative term, like a series of violent slope changes — and it will react to all of them, injecting jitter straight into your actuator. This is why Kd is used sparingly, why many real controllers low-pass-filter the derivative, and why a huge number of practical loops are PI only, with no D at all.
Pro tip: derivative on measurement Computing D from the error means a sudden setpoint change creates an instantaneous spike — the infamous "derivative kick" that jolts the motor. The standard fix is to take the derivative of the measurement instead of the error. Since the gain is identical in steady state, you get the same damping without the kick whenever the operator changes the target.

The PID Equation

Put the three terms together and you get the classic continuous-time PID law:

u(t) = Kp·e(t) + Ki·∫e(t)dt + Kd·de(t)/dt

Read it left to right and it's just the three questions again: Kp·e(t) is the present error, Ki·∫e(t)dt is the accumulated past error, and Kd·de(t)/dt is the rate of change. The output u(t) is the command you send to the motor — a PWM duty cycle, a voltage, a current setpoint, whatever your actuator takes.

From Math to Code: The Discrete PID

A microcontroller can't integrate or differentiate continuously — it runs your code at a fixed rate, say once every 1 millisecond. So we approximate the calculus with simple arithmetic on each loop tick, where dt is the time between ticks:

  • The integral ∫e dt becomes a running sum: integral += error × dt
  • The derivative de/dt becomes a difference: (error − prev_error) / dt
Why fixed sample time matters Both the integral and derivative depend on dt. If your loop runs at an irregular rate, both terms become unpredictable and your tuning falls apart. Run the PID from a timer interrupt at a constant frequency — never inside a main() loop whose timing varies with whatever else is happening.

PID Implementation in C

Here's a compact, reusable controller. It's plain C — no STM32 dependencies — so you can drop it into any project and call pid_update() once per loop tick. It includes output clamping and anti-windup, the two things most tutorial implementations leave out.

typedef struct {
    float Kp, Ki, Kd;        // gains
    float integral;          // accumulated error
    float prev_error;        // for the derivative term
    float out_min, out_max;   // actuator limits
} PID;

void pid_init(PID *pid, float Kp, float Ki, float Kd,
              float out_min, float out_max)
{
    pid->Kp = Kp;  pid->Ki = Ki;  pid->Kd = Kd;
    pid->integral = 0.0f;
    pid->prev_error = 0.0f;
    pid->out_min = out_min;  pid->out_max = out_max;
}

float pid_update(PID *pid, float setpoint, float measurement, float dt)
{
    float error = setpoint - measurement;

    // Proportional — react to the error right now
    float P = pid->Kp * error;

    // Integral — accumulate to kill steady-state offset
    pid->integral += error * dt;
    float I = pid->Ki * pid->integral;

    // Derivative — respond to the rate of change
    float D = pid->Kd * (error - pid->prev_error) / dt;
    pid->prev_error = error;

    float output = P + I + D;

    // Clamp + anti-windup: stop integrating once saturated
    if (output > pid->out_max) {
        output = pid->out_max;
        pid->integral -= error * dt;   // undo this step's accumulation
    } else if (output < pid->out_min) {
        output = pid->out_min;
        pid->integral -= error * dt;
    }
    return output;
}
What makes this implementation solid:
  • It's stateless to the caller. All memory (integral, previous error) lives in the struct, so you can run many independent loops — one per motor — from the same code.
  • Output clamping keeps the command within what your actuator can physically deliver.
  • Anti-windup rolls back the integral whenever the output saturates, so it can't bloat and cause overshoot.

Motor Control on STM32

On an STM32, the pattern is always the same three steps, executed at a fixed rate from a timer interrupt: read the sensor → compute PID → drive the actuator.

Speed control (closed-loop RPM)

Read the motor speed from an encoder, run the PID, and write the result to a PWM channel driving the H-bridge:

PID motor_pid;
float target_rpm = 1500.0f;

void setup(void)
{
    // duty cycle clamped to 0..1000 (timer ARR)
    pid_init(&motor_pid, 0.8f, 12.0f, 0.01f, 0.0f, 1000.0f);
}

// Called from a 1 kHz timer interrupt (dt = 0.001 s)
void TIM2_IRQHandler(void)
{
    float rpm  = encoder_read_rpm();
    float duty = pid_update(&motor_pid, target_rpm, rpm, 0.001f);
    pwm_set_duty(TIM3, CH1, (uint16_t)duty);
}

Position control (closed-loop angle)

The exact same controller works for position — you just feed it the encoder count instead of speed, and a target angle as the setpoint. This is how servo positioning, robot joints, and gimbals work:

float target_deg = 90.0f;

void TIM2_IRQHandler(void)
{
    float angle = encoder_read_degrees();
    float cmd   = pid_update(&pos_pid, target_deg, angle, 0.001f);

    // signed command: sign picks direction, magnitude sets PWM
    motor_set_direction(cmd >= 0 ? FORWARD : REVERSE);
    pwm_set_duty(TIM3, CH1, (uint16_t)fabsf(cmd));
}
STM32 setup checklist Use one timer in encoder mode to count quadrature pulses, a second timer in PWM mode to drive the H-bridge, and a third as a periodic interrupt (e.g. 1 kHz) to run the control loop. Keep the ISR short — read, compute, write — and do nothing else heavy inside it.

What Each Gain Does to the Response

The clearest way to see PID is the step response: command a sudden change and watch how the output chases the new setpoint over time. Each gain reshapes that curve in a characteristic way.

      output time →   setpoint      
  P only — fast, but settles short (steady-state error)   PI — reaches setpoint, some overshoot   PID — fast and damped
Same step command, three controllers. Adding I removes the offset; adding D tames the overshoot.

The tuning cheat sheet

Turning up a single gain has a predictable — but coupled — effect:

Increase… Rise time Overshoot Settling time Steady-state error
Kp Faster ▾ Worse ▴ Small change Smaller ▾
Ki Faster ▾ Worse ▴ Worse ▴ Eliminated ▾▾
Kd Small change Better ▾ Better ▾ No effect

▾ = decreases / improves · ▴ = increases / worsens. These are general trends; because the terms interact, expect to iterate.

Try It Yourself: Interactive PID Simulator

Reading about gains is one thing — feeling them is another. Drag the sliders below to change Kp, Ki, and Kd and watch the step response redraw instantly. The simulated plant is a motor-like system with a little electrical and mechanical lag. With proportional-only control it settles short of the target (steady-state error); add the integral term and it lands exactly on the setpoint. Raise Kp and the response gets faster but overshoots more — exactly as the cheat sheet above predicts — and it stays stable across the whole slider range. Try the presets, then experiment.

 
Overshoot
Settling time (±2%)
Steady-state error

Pure JavaScript, no libraries — runs entirely in your browser with zero extra files or load time.

How to Tune a PID Controller

Tuning is where most people get stuck, but a methodical approach beats random guessing every time. Here are the two most common methods.

Method 1 — Manual tuning (the practical recipe)

  1. Start with Ki = 0 and Kd = 0.
  2. Raise Kp until the system responds quickly and just begins to oscillate. Then back it off to about half that value.
  3. Increase Ki until any steady-state error is driven to zero within an acceptable time. Too much Ki brings back oscillation and sluggishness.
  4. If overshoot or ringing remains, add a small Kd to damp it. Keep it modest — too much amplifies noise.
  5. Re-check under realistic disturbances (add load, change the setpoint) and fine-tune.

Method 2 — Ziegler–Nichols (a structured starting point)

A classic recipe that gives you ballpark gains. Set Ki = Kd = 0, then raise Kp until the output oscillates with a steady, constant amplitude. Call that gain the ultimate gain Ku and the oscillation period Tu. Then:

Controller Kp Ki Kd
P 0.50·Ku
PI 0.45·Ku 0.54·Ku/Tu
PID 0.60·Ku 1.2·Ku/Tu 0.075·Ku·Tu
Treat the result as a starting point Ziegler–Nichols tends to produce an aggressive, overshoot-prone response. Use it to get into the right ballpark, then refine by hand for your specific requirements.

P, PI, PD, PID: Which One Do You Need?

You don't always need all three terms. Choosing the right combination keeps the controller simpler and often more robust.

Type Best for Notes
P Simple, fast systems that tolerate a small offset Always leaves steady-state error
PI Speed control, temperature, flow, pressure The most common choice; no D means it ignores noise
PD Position systems where a small offset is acceptable Fast and damped, but keeps steady-state error
PID Demanding motion control, balancing robots, drones Full accuracy and damping; needs careful tuning

Troubleshooting Common PID Problems

Symptom Likely cause Fix
Output oscillates and won't settle Kp or Ki too high Reduce gains; add or increase Kd
Slow to reach the setpoint Gains too conservative Increase Kp, then Ki
Settles close but never exactly on target No (or too little) integral action Add / increase Ki
Large overshoot after a disturbance clears Integral windup Add anti-windup; clamp the integral
Jittery, twitchy actuator Derivative amplifying sensor noise Lower Kd; filter the measurement
Violent jolt when setpoint changes Derivative kick Take the derivative of the measurement, not the error

Real-World Applications

Once you start looking, PID is everywhere:

Motor speed & position control — robotics joints, conveyor belts, electric vehicles, servo drives.
Drones & flight controllers — each axis (roll, pitch, yaw) runs its own PID loop hundreds of times per second.
Temperature control — 3D printer hotends, reflow ovens, HVAC, industrial process heaters.
Self-balancing systems — two-wheeled robots, Segways, and the classic inverted pendulum.
Manufacturing — CNC axes, pressure and flow regulation, tension control in web handling.
Automotive — cruise control, electronic throttle, idle-speed and traction control.

Frequently Asked Questions

What is PID control in simple terms?

It's a feedback algorithm that measures the difference (error) between a target and the actual output, then corrects it using three terms: Proportional (the error now), Integral (error built up over time), and Derivative (how fast the error is changing). Together they drive the system to the target and hold it there.

What do Kp, Ki, and Kd actually stand for?

They're the gains — tuning constants — for each term. Kp scales the proportional (present) term, Ki the integral (past) term, and Kd the derivative (future) term. Tuning a PID controller means choosing good values for these three numbers.

Why does a P-only controller leave a steady-state error?

Because its output is proportional to the error, it needs a non-zero error to produce any command at all. When a system requires a constant command to hold position (against gravity, friction, or load), it settles at a small residual error that generates exactly that command. The integral term removes this offset.

What is integral windup and how do I prevent it?

Windup happens when the actuator saturates (e.g. PWM already at 100%) but the integral keeps accumulating, growing far beyond what's useful and causing big overshoot when the error finally reverses. Prevent it with anti-windup: stop accumulating the integral, or clamp it, whenever the output is saturated.

Do I always need the derivative term?

No. Many real-world loops are PI only, because the derivative term amplifies sensor noise and can do more harm than good on noisy signals. Add D only when you need extra damping and your measurement is clean enough (or filtered).

How fast should my PID loop run?

Run it at a fixed rate, comfortably faster than your system's dynamics — typically 100 Hz to a few kHz for motor control. The exact rate matters less than keeping it constant, since both the integral and derivative depend on a steady dt.

Can I use this C code on Arduino or other microcontrollers?

Yes. The PID struct and pid_update() function are plain C with no hardware dependencies. Call it from any periodic context — an Arduino timer, an STM32 interrupt, an ESP32 task — passing your sensor reading, setpoint, and loop interval.


Key Takeaways

  • Closed-loop beats open-loop whenever accuracy or disturbances matter — feedback is the entire difference.
  • P handles the present, I the past, D the future. That mental model explains every behavior you'll ever see.
  • I eliminates steady-state error; D tames overshoot — but D amplifies noise, so use it sparingly (and often not at all).
  • On a microcontroller, run the loop at a fixed rate from a timer interrupt, and always include anti-windup.
  • Tune methodically: Kp first, then Ki, then a touch of Kd — and verify under real load.