In the world of robotics, moving a motor from point A to point B seems simple, but ensuring it stops precisely at point B without oscillating or lagging is a complex engineering challenge. This is where the Proportional-Integral-Derivative (PID) controller becomes essential.
A PID controller is a feedback loop component that compares a measured process variable against a desired setpoint to calculate the necessary “correction” [1]. Whether you are maintaining the flight stability of a drone or the position of a robotic arm, understanding how to implement this mathematically-driven logic on a microcontroller is a foundational skill for any developer.
Table of Contents
- The Mathematical Foundation of PID
- Step 1: Discretization for Embedded Systems
- Step 2: Structure Your Code
- Step 3: Handling Real-World Constraints
- Step 4: Tuning Your Gains
- Summary of Key Takeaways
- Sources
The Mathematical Foundation of PID
Before writing code, you must understand the three terms that give the controller its name. According to documentation from WPILib, the controller calculates an “error” ($e$), which is the difference between where your robot is and where you want it to be.
- Proportional (P): This term produces an output proportional to the current error. If the error is large, the output is large. However, P-control alone often results in “steady-state error,” where the system never quite reaches the target because the output becomes too small to move the hardware as the error shrinks.
- Integral (I): This term accounts for the history of the error. It integrates (accumulates) the error over time, providing the “push” needed to eliminate that stubborn steady-state error.
- Derivative (D): This term predicts future error by looking at the rate of change. It acts as a brake, slowing down the system as it approaches the setpoint to prevent overshoot and oscillations [2].
This logic is crucial when you build a remote control robot that requires smooth steering and speed regulation.
The main disadvantage is steady-state error, where the system never quite reaches the exact target. This happens because the output becomes too small to overcome physical friction or resistance as the error values decrease.
The Derivative term acts as a damper or brake by predicting future error based on its current rate of change. It reduces the system’s speed as it approaches the setpoint, which prevents excessive overshoot and oscillations.
Step 1: Discretization for Embedded Systems
Calculus happens in continuous time, but microcontrollers operate in discrete “ticks” or “cycles.” To implement PID on an embedded system, you must convert the continuous formula into a discrete-time version.
In a digital system, the integral is replaced by a running sum, and the derivative is replaced by the difference between the current error and the previous error, divided by the sample time ($dt$).
The Discrete Formula:
Output = (Kp * error) + (Ki * error_sum * dt) + (Kd * (error - last_error) / dt)
Microcontrollers operate in discrete cycles or clock ticks rather than continuous time. To work on an embedded system, calculus operations like integration and differentiation must be converted into sums and differences calculated at specific time intervals (dt).
In a digital system, the integral is replaced by a running sum of the error multiplied by the sample time (dt), while the derivative is calculated as the difference between the current error and the previous error divided by dt.
Step 2: Structure Your Code
A clean implementation avoids “spaghetti code” by encapsulating the PID logic into a structure or class. This allows you to run multiple PID loops (e.g., one for each wheel) simultaneously.
Basic C Implementation
typedef struct {
float Kp, Ki, Kd;
float setpoint;
float integral;
float last_error;
} PIDController;
float computePID(PIDController *pid, float measurement, float dt) {
float error = pid->setpoint - measurement;
pid->integral += error * dt;
float derivative = (error - pid->last_error) / dt;
float output = (pid->Kp * error) + (pid->Ki * pid->integral) + (pid->Kd * derivative);
pid->last_error = error;
return output;
}
Research from community discussions on Reddit suggests that for most 8-bit or 32-bit microcontrollers, using a fixed loop timing (e.g., every 10ms) is more stable than trying to measure a varying $dt$ during execution.
Encapsulating the logic into a structure allows you to maintain multiple independent PID loops simultaneously, such as controlling multiple wheels or joints, without the code becoming messy or tangled.
While variable timing is possible, research suggests that using a fixed loop timing (e.g., every 10ms via a timer interrupt) is generally more stable for 8-bit or 32-bit microcontrollers than measuring dt during every execution.
Step 3: Handling Real-World Constraints
Standard PID equations assume infinite power and perfect sensors. In embedded systems, you must account for hardware realities:
Integral Windup: If your motor is stalled but the controller keeps adding to the integral term, the “sum” becomes massive. When the motor is freed, it will fly past the target at max speed. Solution: Limit the maximum value the integral term can reach.
Derivative Noise: Sensors like inexpensive encoders or IMUs often have “jitter.” Because the D-term calculates the rate of change, even small noise can cause massive spikes in output. Solution: Apply a low-pass filter to the sensor data before passing it to the PID function [4].
Output Saturation: Most Pulse Width Modulation (PWM) signals on microcontrollers range from 0 to 255 or -100 to 100. Always clamp your final PID output to these hardware limits.
For those interested in high-level integration, you can explore how to use ChatGPT in robotics to help generate boilerplate PID structures for specific microcontrollers like ESP32 or STM32.
| Constraint | Recommended Solution |
|---|---|
| Integral Windup | Clamping/Saturation Limits |
| Sensor Noise | Low-pass Filtering |
| Actuator Limits | Output Normalization (PWM Clamping) |
Integral windup occurs when the error accumulator grows too large while an actuator is stalled or limited, causing a massive overshoot when released. It is prevented by clamping the maximum value the integral term can reach.
Sensor noise can cause spikes in the D-term because it calculates the rate of change between samples. The best solution is to apply a low-pass filter to the sensor data to smooth it out before it is processed by the PID function.
Hardware actuators have physical limits, such as PWM ranges (0-255). Clamping the PID output ensures that the controller doesn’t attempt to send values the hardware cannot handle, maintaining system stability.
Step 4: Tuning Your Gains
Tuning is the process of choosing the values for $Kp$, $Ki$, and $Kd$. A common method is the Ziegler-Nichols approach, but for most hobbyist projects, manual tuning is safer:
- Set all gains to zero.
- Increase $Kp$ until the system oscillates around the target.
- Increase $Kd$ to dampen the oscillation and make the system stop quickly.
- Increase $Ki$ only if the system fails to reach the exact setpoint (to fix steady-state error) [5].
Start with all gains at zero, then increase Kp until the system oscillates. Follow this by increasing Kd to dampen those oscillations, and finally add Ki only if necessary to eliminate remaining steady-state error.
Ziegler-Nichols is a formal mathematical approach useful for complex systems, but for most hobbyist robotics projects, manual tuning is often safer and easier to implement effectively.
Summary of Key Takeaways
Logic: PID uses Proportional (present), Integral (past), and Derivative (future) terms to minimize error.
Implementation: Use discrete-time math where the integral is a sum and the derivative is a difference.
Safety: Always implement anti-windup for the integral term and output clamping for your actuators.
Tuning: Start with $Kp$, then $Kd$, then $Ki$.
Action Plan
- Hardcode your $dt$: Set a timer interrupt to run your PID loop at a consistent frequency (e.g., 50Hz or 100Hz).
- Clamp everything: Add
ifstatements to ensure your integral and motor outputs never exceed safe hardware limits. - Log Data: If possible, output your setpoint and measurement to a serial plotter to visualize the “ringing” or “lag” as you tune.
- Test Under Load: A robot arm behaves differently when it is empty vs. when it is holding an object; tune for your most common use case.
Understanding these controls is a major part of the introduction to mechanics, planning, and control in robotics. mastering the PID loop is often the difference between a jerky, vibrating machine and a smooth, professional-grade robot.
| Term | Function | Tuning Order |
|---|---|---|
| Proportional (Kp) | Current Error correction | 1st – Increase until oscillation |
| Derivative (Kd) | Future Error (Braking) | 2nd – Increase to dampen |
| Integral (Ki) | Past Error (Steady-state) | 3rd – Use only if needed |
The two most important safety measures are implementing anti-windup for the integral term and output clamping for the actuators. These prevent the hardware from behaving erratically or exceeding physical limits.
Physical systems behave differently with varying weights; for example, a robot arm requires different power when empty versus when carrying an object. Tuning for the most common use case ensures the smoothest performance during operation.