Advanced
PLC PID Control Loop Tuning: Implementation & Examples in Structured Text
Implement and tune PID controllers in Structured Text — from basic proportional control to full PID with anti-windup, used in temperature, pressure, and flow regulation.
What a PID controller does
PID (Proportional-Integral-Derivative) control is the most widely used feedback control algorithm in industrial automation. Every cycle it calculates the error between the setpoint you want and the process variable you measure, then applies a correction made of three terms:
| Term | Symbol | Action | Corrects |
|---|---|---|---|
| Proportional | Kp | Reacts to current error | Present error |
| Integral | Ki | Accumulates past error | Steady-state offset |
| Derivative | Kd | Predicts future error | Overshoot/oscillation |
The PID formula
In continuous form:
Output = Kp × Error + Ki × ∫Error dt + Kd × dError/dt
In a PLC the controller is sampled once per task cycle, so the integral becomes a running sum and the derivative a difference between two samples:
Output = Kp × Error + Ki × ErrorSum × dt + Kd × (Error - PrevError) / dt
A basic PID loop in Structured Text
This version holds everything in one program, with the sample time dt fixed at 0.1 s and the output clamped to 0–100%:
PROGRAM BasicPID
VAR
// Process
Setpoint : REAL := 75.0; // Desired temperature °C
ProcessValue : REAL := 20.0; // Measured temperature °C
Output : REAL := 0.0; // Control output 0-100%
// Tuning parameters
Kp : REAL := 2.0;
Ki : REAL := 0.5;
Kd : REAL := 0.1;
// Internal
Error : REAL := 0.0;
PrevError : REAL := 0.0;
ErrorSum : REAL := 0.0;
ErrorDiff : REAL := 0.0;
dt : REAL := 0.1; // Sample time in seconds
// Output limits
OutMin : REAL := 0.0;
OutMax : REAL := 100.0;
END_VAR
// Calculate error
Error := Setpoint - ProcessValue;
// Proportional term
// Integral term (accumulated error)
ErrorSum := ErrorSum + (Error * dt);
// Derivative term (rate of change).
// dt is the sample period in seconds. It MUST be > 0 — set it to your
// actual cyclic task period (e.g. 0.1 for a 100 ms task) and never expose
// it as a tunable that can land on zero. Real PLCs fault on dt = 0.
ErrorDiff := (Error - PrevError) / dt;
// PID output
Output := (Kp * Error) + (Ki * ErrorSum) + (Kd * ErrorDiff);
// Clamp output
IF Output > OutMax THEN
Output := OutMax;
ELSIF Output < OutMin THEN
Output := OutMin;
END_IF;
// Store previous error
PrevError := Error;
A reusable PID function block
In a real project you wrap the same calculation in a function block, so every loop shares one tested implementation. This one adds manual mode with bumpless transfer, a reset input and anti-windup:
FUNCTION_BLOCK FB_PID
VAR_INPUT
Enable : BOOL := FALSE;
Setpoint : REAL := 0.0;
ProcessValue : REAL := 0.0;
Kp : REAL := 1.0;
Ki : REAL := 0.0;
Kd : REAL := 0.0;
dt : REAL := 0.1;
OutMin : REAL := 0.0;
OutMax : REAL := 100.0;
ManualMode : BOOL := FALSE;
ManualOutput : REAL := 0.0;
Reset : BOOL := FALSE;
END_VAR
VAR_OUTPUT
Output : REAL := 0.0;
Error : REAL := 0.0;
IsActive : BOOL := FALSE;
END_VAR
VAR
PrevError : REAL := 0.0;
Integral : REAL := 0.0;
Derivative : REAL := 0.0;
RawOutput : REAL := 0.0;
END_VAR
IF Reset THEN
Integral := 0.0;
PrevError := 0.0;
Output := 0.0;
RETURN;
END_IF;
IF ManualMode THEN
Output := ManualOutput;
// Bumpless transfer: pre-seed Integral so the first Auto-mode output
// equals ManualOutput. Rearranging Output = Kp·Error + Ki·Integral
// gives Integral = (ManualOutput - Kp·Error) / Ki. The divisor is Ki,
// so guard Ki — not Kp. A P-only controller (Ki = 0) has no integral
// term to track, so leave Integral cleared on every Manual scan.
IF Ki <> 0.0 THEN
Integral := (ManualOutput - Kp * (Setpoint - ProcessValue)) / Ki;
ELSE
Integral := 0.0;
END_IF;
RETURN;
END_IF;
IF NOT Enable THEN
Output := 0.0;
IsActive := FALSE;
RETURN;
END_IF;
IsActive := TRUE;
// Error
Error := Setpoint - ProcessValue;
// Integral with anti-windup
Integral := Integral + (Error * dt);
// Derivative — dt is a VAR_INPUT with default 0.1, but the caller can
// override it. Bind dt to the actual task period; if the FB is wired
// up with dt = 0 it will fault here on every scan.
Derivative := (Error - PrevError) / dt;
// Raw PID output
RawOutput := (Kp * Error) + (Ki * Integral) + (Kd * Derivative);
// Clamp and anti-windup
IF RawOutput > OutMax THEN
Output := OutMax;
// Anti-windup: stop integrating when saturated
Integral := Integral - (Error * dt);
ELSIF RawOutput < OutMin THEN
Output := OutMin;
Integral := Integral - (Error * dt);
ELSE
Output := RawOutput;
END_IF;
PrevError := Error;
How the anti-windup works
Integral windup happens when the output is saturated (at min or max) but the integral term keeps accumulating. When the error finally reverses, all that stored integral has to unwind first, and the result is a large overshoot.
The function block above stops accumulating the integral while the output is clamped: on a saturated scan it subtracts the step it has just added. The technique is called conditional integration or clamping anti-windup.
Example: oven temperature control
An oven held at 180 °C, with the heater forced off and the PID disabled if the thermocouple reads above 220 °C:
PROGRAM OvenControl
VAR
// Inputs
TempSensor : REAL := 25.0; // °C from thermocouple
TempSetpoint : REAL := 180.0; // Target °C
Enable : BOOL := TRUE;
// PID controller
TempPID : FB_PID;
HeaterOutput : REAL := 0.0;
// Safety
OverTempAlarm : BOOL := FALSE;
MaxTemp : REAL := 220.0;
END_VAR
// Safety check first
OverTempAlarm := TempSensor > MaxTemp;
// Run PID
TempPID(
Enable := Enable AND NOT OverTempAlarm,
Setpoint := TempSetpoint,
ProcessValue := TempSensor,
Kp := 3.0,
Ki := 0.2,
Kd := 0.5,
dt := 0.1,
OutMin := 0.0,
OutMax := 100.0
);
HeaterOutput := TempPID.Output;
// Override on alarm
IF OverTempAlarm THEN
HeaterOutput := 0.0;
END_IF;
Manual tuning with Ziegler-Nichols
The Ziegler-Nichols method is the best-known manual tuning approach:
| Controller | Kp | Ki | Kd |
|---|---|---|---|
| P only | 0.5 × Ku | not used | not used |
| PI | 0.45 × Ku | 1.2 × Kp / Tu | not used |
| PID | 0.6 × Ku | 2 × Kp / Tu | Kp × Tu / 8 |
Worked example
If Ku = 4.0 and Tu = 2.0 seconds, a full PID works out as:
Tuning tips from the field
Common PID mistakes
Cascaded PID loops
Some processes need an inner and an outer loop. Here a slow temperature loop (1 s) writes the setpoint for a fast flow loop (100 ms), and the flow loop drives the valve:
PROGRAM CascadeControl
VAR
// Outer loop: Temperature (slow)
OuterPID : FB_PID;
TempSetpoint : REAL := 180.0;
Temperature : REAL := 25.0;
// Inner loop: Flow (fast)
InnerPID : FB_PID;
FlowSetpoint : REAL := 0.0;
FlowRate : REAL := 0.0;
ValveOutput : REAL := 0.0;
END_VAR
// Outer loop sets flow setpoint
OuterPID(
Enable := TRUE,
Setpoint := TempSetpoint,
ProcessValue := Temperature,
Kp := 2.0, Ki := 0.1, Kd := 0.3,
dt := 1.0, // Slow loop: 1 second
OutMin := 0.0, OutMax := 100.0
);
FlowSetpoint := OuterPID.Output;
// Inner loop controls valve
InnerPID(
Enable := TRUE,
Setpoint := FlowSetpoint,
ProcessValue := FlowRate,
Kp := 1.5, Ki := 0.8, Kd := 0.0,
dt := 0.1, // Fast loop: 100ms
OutMin := 0.0, OutMax := 100.0
);
ValveOutput := InnerPID.Output;
Trying PID tuning in the browser
You can experiment with PID tuning in the online ST editor: adjust Kp, Ki and Kd in real time and watch how the process responds. The motion control lessons have guided PID exercises.