How to Implement PID Control on Embedded Systems

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

  1. The Mathematical Foundation of PID
  2. Step 1: Discretization for Embedded Systems
  3. Step 2: Structure Your Code
  4. Step 3: Handling Real-World Constraints
  5. Step 4: Tuning Your Gains
  6. Summary of Key Takeaways
  7. 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.

  1. 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.
  2. 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.
  3. 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.

PID Block DiagramA simplified diagram showing the feedback loop from Error to PID blocks to Output.ΣPIDErrorFeedback

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)

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.

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.

Table: Implementation challenges and their respective software solutions
ConstraintRecommended Solution
Integral WindupClamping/Saturation Limits
Sensor NoiseLow-pass Filtering
Actuator LimitsOutput Normalization (PWM Clamping)

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:

  1. Set all gains to zero.
  2. Increase $Kp$ until the system oscillates around the target.
  3. Increase $Kd$ to dampen the oscillation and make the system stop quickly.
  4. Increase $Ki$ only if the system fails to reach the exact setpoint (to fix steady-state error) [5].

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

  1. Hardcode your $dt$: Set a timer interrupt to run your PID loop at a consistent frequency (e.g., 50Hz or 100Hz).
  2. Clamp everything: Add if statements to ensure your integral and motor outputs never exceed safe hardware limits.
  3. Log Data: If possible, output your setpoint and measurement to a serial plotter to visualize the “ringing” or “lag” as you tune.
  4. 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.

Table: Quick reference for PID components and tuning priority
TermFunctionTuning Order
Proportional (Kp)Current Error correction1st – Increase until oscillation
Derivative (Kd)Future Error (Braking)2nd – Increase to dampen
Integral (Ki)Past Error (Steady-state)3rd – Use only if needed

Sources