On this page
PID Controller Implementation v2.4+
The PIDController class provides a discrete-time PID controller with anti-windup, output limiting, and rate limiting capabilities. It is used throughout SimpleFOC for velocity and current control loops.
Class Definition
From common/pid.h:
class PIDController {
public:
PIDController(float P, float I, float D, float ramp = NOT_SET,
float limit = NOT_SET, float sampling_time = NOT_SET);
float operator() (float error);
void reset();
float P; // Proportional gain
float I; // Integral gain
float D; // Derivative gain
float output_ramp; // Maximum speed of change of output value
float limit; // Maximum output value
float Ts; // Fixed sampling time (optional)
protected:
float error_prev; // Last tracking error value
float output_prev; // Last PID output value
float integral_prev; // Last integral component value
unsigned long timestamp_prev; // Last execution timestamp
};
Constructor
PIDController::PIDController(float P, float I, float D, float ramp,
float limit, float sampling_time)
: P(P)
, I(I)
, D(D)
, output_ramp(ramp) // output derivative limit [ex. volts/second]
, limit(limit) // output supply limit [ex. volts]
, Ts(sampling_time) // sampling time [seconds]
, error_prev(0.0f)
, output_prev(0.0f)
, integral_prev(0.0f)
{
timestamp_prev = _micros();
}
Parameters:
P: Proportional gainI: Integral gainD: Derivative gainramp: Maximum rate of change of output (units/second)limit: Maximum absolute output valuesampling_time: Fixed time step (optional)
PID Algorithm
The operator() method calculates the control output:
float PIDController::operator() (float error) {
// Initialize elapsed time with fixed sampling time Ts
float dt = Ts;
// If Ts not set, use adaptive sampling time
if(!_isset(dt)){
unsigned long timestamp_now = _micros();
dt = (timestamp_now - timestamp_prev) * 1e-6f;
// Quick fix for strange cases (micros overflow)
if(dt <= 0 || dt > 0.5f) dt = 1e-3f;
timestamp_prev = timestamp_now;
}
// Proportional part: u_p = P * e(k)
float proportional = P * error;
// Tustin transform of integral part
// u_ik = u_ik_1 + I*Ts/2*(e_k + e_k_1)
float integral = integral_prev + I*dt*0.5f*(error + error_prev);
// Antiwindup - limit the integral
if(_isset(limit)) integral = _constrain(integral, -limit, limit);
// Discrete derivative: u_dk = D(e_k - e_k_1)/Ts
float derivative = D*(error - error_prev)/dt;
// Sum all components
float output = proportional + integral + derivative;
// Antiwindup - limit the output
if(_isset(limit)) output = _constrain(output, -limit, limit);
// Output ramping (rate limiting)
if(_isset(output_ramp) && output_ramp > 0){
float output_rate = (output - output_prev)/dt;
if (output_rate > output_ramp)
output = output_prev + output_ramp*dt;
else if (output_rate < -output_ramp)
output = output_prev - output_ramp*dt;
}
// Save for next iteration
integral_prev = integral;
output_prev = output;
error_prev = error;
return output;
}
Control Equation
The continuous-time PID equation:
\[u(t) = K_p e(t) + K_i \int_0^t e(\tau) d\tau + K_d \frac{de(t)}{dt}\]Discrete Implementation
Proportional: \(u_p[k] = P \cdot e[k]\)
Integral (Tustin/Trapezoidal): \(u_i[k] = u_i[k-1] + \frac{I \cdot \Delta t}{2}(e[k] + e[k-1])\)
Derivative (Backward difference): \(u_d[k] = D \cdot \frac{e[k] - e[k-1]}{\Delta t}\)
Total output: \(u[k] = u_p[k] + u_i[k] + u_d[k]\)
Key Features
1. Tustin Integration
The integral term uses the Trapezoidal (Tustin) method instead of simple Euler:
integral = integral_prev + I*dt*0.5f*(error + error_prev);
Benefits:
- Better numerical stability
- More accurate for varying sample times
- Reduced integration drift
- Improved frequency response
Comparison with Euler:
- Euler: \(u_i[k] = u_i[k-1] + I \cdot \Delta t \cdot e[k]\)
- Tustin: \(u_i[k] = u_i[k-1] + \frac{I \cdot \Delta t}{2}(e[k] + e[k-1])\)
2. Anti-Windup
Integral windup occurs when the integral term accumulates beyond useful limits. SimpleFOC implements clamping anti-windup:
// Limit integral component
if(_isset(limit)) integral = _constrain(integral, -limit, limit);
// Limit total output
if(_isset(limit)) output = _constrain(output, -limit, limit);
How it works:
- Integral clamped to output limit
- Total output also clamped
- Prevents excessive overshoot
- Faster recovery from saturation
3. Output Rate Limiting
Constrains how fast the output can change:
if(_isset(output_ramp) && output_ramp > 0){
float output_rate = (output - output_prev)/dt;
if (output_rate > output_ramp)
output = output_prev + output_ramp*dt;
else if (output_rate < -output_ramp)
output = output_prev - output_ramp*dt;
}
Purpose:
- Limits acceleration
- Prevents sudden jumps
- Smoother motor operation
- Reduces mechanical stress
4. Adaptive Time Step
When Ts is not set:
dt = (timestamp_now - timestamp_prev) * 1e-6f;
if(dt <= 0 || dt > 0.5f) dt = 1e-3f; // Safety check
Handles:
- Variable execution rates
- Micros() overflow
- Missed cycles
Reset Function
void PIDController::reset() {
integral_prev = 0.0f;
output_prev = 0.0f;
error_prev = 0.0f;
}
When to use:
- Switching control modes
- After long pauses
- To clear accumulated error
- Mode transitions
Usage in SimpleFOC
Velocity Control
// Configuration
motor.PID_velocity.P = 0.2;
motor.PID_velocity.I = 20.0;
motor.PID_velocity.D = 0.0;
motor.PID_velocity.output_ramp = 1000; // [A/s] or [V/s]
motor.PID_velocity.limit = 12; // [A] or [V]
// in FOCMotor.cpp move() function
current_sp = PID_velocity(target_velocity - shaft_velocity);
Position Control (P Controller)
// Typically only P gain used for position
motor.P_angle.P = 20.0;
motor.P_angle.I = 0.0; // Usually zero
motor.P_angle.D = 0.0; // Usually zero
motor.P_angle.limit = 0; // velocity_limit used instead
// in FOCMotor.cpp move() function
// Calculates velocity setpoint
shaft_velocity_sp = P_angle(target_angle - shaft_angle);
FOC Current Control
// Q-axis (torque) current
motor.PID_current_q.P = 5.0;
motor.PID_current_q.I = 300.0;
motor.PID_current_q.D = 0.0;
motor.PID_current_q.output_ramp = 0;
motor.PID_current_q.limit = motor.voltage_limit;
// D-axis (flux) current
motor.PID_current_d.P = 5.0;
motor.PID_current_d.I = 300.0;
motor.PID_current_d.D = 0.0;
motor.PID_current_d.output_ramp = 0;
motor.PID_current_d.limit = motor.voltage_limit;
// Usage in FOCMotor.cpp - loopFOC() function
voltage.q = PID_current_q(current_sp - current.q);
voltage.d = PID_current_d(0 - current.d); // d-current target = 0
DC Current Control
motor.PID_current_q.P = 5.0;
motor.PID_current_q.I = 300.0;
motor.PID_current_q.limit = motor.voltage_limit;
// in FOCMotor.cpp loopFOC() function
// Controls magnitude of current vector
voltage_magnitude = PID_current_q(current_sp - current_magnitude);
Quick Reference: Parameter Effects
| Parameter | Too Low | Too High |
|---|---|---|
| P | Slow response, steady-state error | Overshoot, oscillation, instability |
| I | Steady-state error | Overshoot, oscillation, slow settling |
| D | Allows overshoot | Noise amplification, instability |
| output_ramp | Jerky motion | Slow response, sluggish |
| limit | Output saturation issues | No protection, potential damage |
PID Tuning
For detailed tuning procedures and practical guidance, see the dedicated tuning guide:
PID Controller Tuning Guide Velocity Loop Tuning Guide
Related Documentation
Motion control implementation Torque control implementation Low-pass filter implementation