PID implementation in C and Motor Control Example
Oct 13, 2025
PID Control Explained: The Complete Guide to PID Implementation in C for Motor Control
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.
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:
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
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 − measurementFrom 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.
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 × errorThe 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.
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:
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) / dtA 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.
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.The PID Equation
Put the three terms together and you get the classic continuous-time PID law:
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
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;
}
- 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));
}
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.
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 |
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.
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)
- Start with
Ki = 0andKd = 0. - Raise
Kpuntil the system responds quickly and just begins to oscillate. Then back it off to about half that value. - Increase
Kiuntil any steady-state error is driven to zero within an acceptable time. Too muchKibrings back oscillation and sluggishness. - If overshoot or ringing remains, add a small
Kdto damp it. Keep it modest — too much amplifies noise. - 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 |
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:
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.