Picture a heat exchanger loop that's been "fine" for years. Then someone changes a pump, the outlet temperature starts swinging a few degrees either side of setpoint, and the operators put it in Manual and leave it there. The usual next step is somebody nudging the gains by feel until it looks a little better, without writing down what they started with.
This tutorial is the other way to do it: check that the loop can be tuned at all, measure how the process actually responds, start from gains you can explain, and keep a trend of before and after. It uses a heat exchanger outlet temperature (TT101) controlled by a hot water valve (TV101) as the example, but the same steps work for pump pressure, flow, and most temperature loops. Tank level is a little different, and we'll point out where.
What P, I and D do, in plain words
The controller looks at the error, the gap between setpoint (SP) and process value (PV), and decides where to put the output (CV), usually a valve or a drive speed.
- P, proportional: "how far off am I right now?" The output moves in proportion to the error. More gain means a harder push. P alone usually settles a little away from setpoint (offset), and too much P makes the loop oscillate.
- I, integral: "how long have I been off?" As long as there's any error, integral keeps adding to the output, which is what removes the offset. Too much integral (too short an integral time) makes slow, rolling overshoot.
- D, derivative: "how fast is it changing?" Derivative pushes against fast changes, like a brake. It can help some slow temperature loops, but it amplifies noise. Most flow, pressure and level loops run fine with D at zero, and that's where we start.
Almost every loop you'll meet on a plant floor is a PI loop. Get P and I right and you've solved most of it.
Check the loop before you touch the gains
No gain setting fixes a mechanical or signal problem; it only hides it until conditions change. Start here. Do all of this with the loop in Manual, with operations' OK and someone watching the process.
- Valve or actuator stiction. Make small output steps, about 0.5–1% at a time, in one direction, then reverse. If the valve position (or the PV) doesn't move until a few steps pile up, and then jumps, the valve is sticking or has deadband. In Auto, that shows up as a PV that moves in squarish steps while the output saws up and down. That's a job for the valve packing or positioner, not for tuning.
- Sensor noise. Hold the output still and watch the PV for a few minutes. If it jitters by more than the swing you're trying to fix, look at grounding, shielding, and the transmitter's damping before you filter it in the PLC. Filter lightly if you must: every filter adds lag, and lag is what makes loops hard to tune.
- Scaling and units. Confirm the PV range in the PLC matches the transmitter range (what 4 mA and 20 mA really mean), and that 0–100% output really drives the valve closed-to-open. PIDE works in percent of span, so a wrong span means every gain you calculate is off by the same factor.
- Direction. Decide which way the output has to move when the PV is below setpoint. For a heating valve, PV low means open more: error is SP − PV, which is PIDE's default (
ControlAction= 0). A fail-open valve, or a cooling loop, may need the other setting. Getting this wrong makes the loop run away the moment you go to Auto. - Update rate. Run the loop in a periodic task that's fast compared to how quickly the process responds. For most temperature, level and pressure loops, a period in the 100–500 ms range is plenty; fast flow or pressure loops may want faster.
- A manual bump. Finally, make one small step in the output and watch. Does the PV move the way you expect, by roughly the amount you expect, without a long wait? If it does, you're ready for the step test.
A simple step test
The step test gives you three numbers that describe the process. They come from the process, not from anybody's opinion, which is why they make a good starting point.
- With the loop in Manual and the PV steady, step the output by enough to clearly see the PV move above the noise, typically 5–10%. Note the time and both values.
- Wait until the PV settles at its new value. Note the new PV.
- Step back, and if you can, repeat in both directions. If the results differ a lot, the process is nonlinear (valves often are), and you should tune for the range it normally runs in.
From the trend, read:
- Process gain K: the change in PV divided by the change in output, with both in percent. Convert the PV change to percent of the PV span: a 12 °F rise on a 0–200 °F transmitter is 6%.
- Dead time θ: the time from the step until the PV first starts to move.
- Time constant τ: the time from when the PV starts moving until it has made 63% of its total change.
Then pick a closed-loop time constant λ (lambda): how quickly you want the loop to bring the PV back to setpoint. That's the one tuning choice you make; the rest is arithmetic. A cautious first choice is λ equal to τ, so the closed loop responds about as fast as the open loop did. Faster loops (smaller λ) are tighter but less forgiving when the process changes; don't go below θ.
For a process that settles at a new value (most temperature, pressure and flow loops), Skogestad's SIMC rules give PI settings:
- Controller gain: Kc = τ ÷ (K × (λ + θ))
- Integral time: Ti = the smaller of τ and 4 × (λ + θ)
- Derivative: 0 to start
Source: S. Skogestad, "Simple analytic rules for model reduction and PID controller tuning," Journal of Process Control 13 (2003) 291–309. With Ti = τ these are the same as the classic lambda (IMC) tuning rules.
A worked example (made-up numbers, to show the arithmetic)
Transmitter range 0–200 °F. The output is stepped from 40% to 50% (ΔCV = 10%). The outlet temperature goes from 140 °F to 152 °F, a 12 °F change, which is 6% of span, so K = 6 ÷ 10 = 0.6. The PV starts moving 0.5 minutes after the step (θ = 0.5 min) and reaches 147.6 °F, 63% of the way, 2.0 minutes after that (τ = 2.0 min).
Choose λ = τ = 2.0 min. Then Kc = 2.0 ÷ (0.6 × 2.5) = 1.33, and Ti = the smaller of 2.0 and 10 = 2.0 min. In PIDE's dependent form, that's PGain = 1.33 and IGain = 2.0 (minutes per repeat).
Tank level is different. A level with a pump or valve on the outlet usually doesn't settle at a new value after a step; it keeps ramping. That's an integrating process. Measure the change in ramp rate instead (percent of span per minute, per percent of output) and call it K′. The SIMC rules for that case are Kc = 1 ÷ (K′ × (λ + θ)) and Ti = 4 × (λ + θ). Many level loops are surge tanks where a slow, smooth level is the goal, so a long λ is often the right choice.
Starting gains are a starting point. Put the loop in Auto, make a small setpoint change, and watch the trend. If it's sluggish, shorten λ a little and recalculate; if it overshoots or rolls, lengthen it. Change one thing at a time, and write each change down (the Ignition section below does that for you).
In the PLC (Studio 5000 / Logix)
The example uses the Enhanced PID instruction, PIDE, the better choice for new work on ControlLogix and CompactLogix. It has proper operator and program modes, output limits, PV fault handling, and anti-windup built in. Older code often uses the PID instruction instead; that's covered at the end of this section.
Set up the PIDE once
These are set in the PIDE tag (or its properties dialog), not every scan. The values are for the example loop; use your own ranges.
The loop
Both versions below do the same thing in the same order, with the same tag names. Ladder is shown first. Switch to Structured Text if that's your plant standard.
Ladder
Seven rungs in a ladder routine in a periodic task. PIDE isn't available in ladder (Rockwell offers it in function block and structured text only), so rung 5 calls a one-line structured text routine that runs it. Everything around the loop stays in ladder, where your maintenance team can read it. Open "as text" under any rung to copy it into the Studio 5000 rung editor.
Drawn the way the Studio 5000 ladder editor lays it out: inputs on the left, outputs pushed to the right rail. The small numbers with arrows are where the editor shows live values; they read 0 here because nothing is online.
On a phone, swipe sideways to see the whole rung.
Rung 0 as text
XIC(S:FS)MOV(TT101_Raw,TT101_Filt);Rung 1 as text
CPT(TT101_Filt,TT101_Filt+FiltK*(TT101_Raw-TT101_Filt));Rung 2 as text
[XIC(TT101_ChFault),LES(TT101_Raw,TT101_BadLo),GRT(TT101_Raw,TT101_BadHi)]OTE(TT101_Bad);Rung 3 as text
[XIO(P101_Running),XIO(HX_Permit)]OTE(TIC101.ProgOverrideReq);Rung 4 as text
[MOV(TT101_Filt,TIC101.PV),XIC(TT101_Bad)OTE(TIC101.PVFault)];Rung 5 as text
JSR(TIC101_PIDE,0);Rung 6 as text
MOV(TIC101.CVEU,TV101_Out);All 7 rungs as text
XIC(S:FS)MOV(TT101_Raw,TT101_Filt);
CPT(TT101_Filt,TT101_Filt+FiltK*(TT101_Raw-TT101_Filt));
[XIC(TT101_ChFault),LES(TT101_Raw,TT101_BadLo),GRT(TT101_Raw,TT101_BadHi)]OTE(TT101_Bad);
[XIO(P101_Running),XIO(HX_Permit)]OTE(TIC101.ProgOverrideReq);
[MOV(TT101_Filt,TIC101.PV),XIC(TT101_Bad)OTE(TIC101.PVFault)];
JSR(TIC101_PIDE,0);
MOV(TIC101.CVEU,TV101_Out);The routine that rung 5 calls, TIC101_PIDE, is structured text with one line. A function block routine with a single PIDE block works the same way.
(* TIC101_PIDE: structured text routine called by the JSR in rung 5 *)
PIDE(TIC101);
Structured Text
The same seven steps in one structured text routine, in a periodic task. Here the PIDE call sits right in the routine.
(* TIC101_Main: structured text routine in a periodic task (for example 100 ms) *)
(* 1. First scan: start the filter at the live reading, not at zero *)
IF S:FS THEN
TT101_Filt := TT101_Raw;
END_IF;
(* 2. Light first-order filter on the temperature (FiltK 0 to 1, 1 = no filter) *)
TT101_Filt := TT101_Filt + FiltK * (TT101_Raw - TT101_Filt);
(* 3. Bad PV: channel fault, or a reading outside the transmitter's range *)
TT101_Bad := TT101_ChFault OR (TT101_Raw < TT101_BadLo) OR (TT101_Raw > TT101_BadHi);
(* 4. Process can't respond: hold the loop in Override at CVOverride *)
TIC101.ProgOverrideReq := NOT P101_Running OR NOT HX_Permit;
(* 5. Hand the PV and its health to the PIDE *)
TIC101.PV := TT101_Filt;
TIC101.PVFault := TT101_Bad;
(* 6. Run the PIDE *)
PIDE(TIC101);
(* 7. Send the output to the valve *)
TV101_Out := TIC101.CVEU;
One routine, in a periodic task. Don't put a PID loop in the continuous task: its scan time wanders, and the loop's math assumes a steady update.
What the pieces are doing:
- Bad PV goes to Manual. When
PVFaultis on, PIDE stops calculating and drops to Manual, holding the output instead of chasing a broken signal. Alarm onTT101_Badso someone knows why. - Anti-windup. PIDE uses the velocity form: it calculates a change in output each update and clamps the result at
CVLLimit/CVHLimit, so integral can't pile up past the limits. The windup you still have to handle is the loop staying in Auto while the process can't respond (pump off, a hand valve closed). It ramps the output to the limit, and when the process comes back the valve slams. Rung 3 holds the loop in Override at a known output instead. - Bumpless manual/auto. While not in operator Manual, PIDE keeps
CVOperequal to the output, so going to Manual doesn't move the valve. Going back to Auto starts from the current output, and withPVTrackingon, the setpoint starts where the PV is. The operator then moves the setpoint deliberately. - Output limits.
CVLLimitandCVHLimitapply in Auto. In Manual they only apply ifCVManLimitingis on; decide with operations whether an operator should be able to go outside them.
Starting gains from the step test
You can do this math on a phone calculator. Putting it in the PLC means the step-test numbers, the starting gains, and the trend can sit on one Ignition screen, and nobody types a gain into the wrong box.
Ladder
Five rungs. Rungs 0 to 2 only do math; rung 3 copies the result into the loop only when someone presses the button that sets ApplyGainsReq, and rung 4 always clears that request.
Drawn the way the Studio 5000 ladder editor lays it out: inputs on the left, outputs pushed to the right rail. The small numbers with arrows are where the editor shows live values; they read 0 here because nothing is online.
On a phone, swipe sideways to see the whole rung.
Rung 0 as text
NEQ(StepCV,0)GRT(PVSpan,0)CPT(Proc_Gain,ABS(StepPV*100.0/PVSpan/StepCV));Rung 1 as text
GRT(Proc_Gain,0)GRT(LambdaMin,0)[ADD(LambdaMin,DeadMin,LamDead),CPT(Kc_Start,TauMin/(Proc_Gain*LamDead)),CPT(Ti_Limit,4.0*LamDead)];Rung 2 as text
[MOV(TauMin,Ti_Start),GRT(Ti_Start,Ti_Limit)MOV(Ti_Limit,Ti_Start)];Rung 3 as text
XIC(ApplyGainsReq)XIC(TIC101.DependIndepend)GRT(Ti_Start,0)[MOV(Kc_Start,TIC101.PGain),MOV(Ti_Start,TIC101.IGain),MOV(0.0,TIC101.DGain)];Rung 4 as text
XIC(ApplyGainsReq)OTU(ApplyGainsReq);All 5 rungs as text
NEQ(StepCV,0)GRT(PVSpan,0)CPT(Proc_Gain,ABS(StepPV*100.0/PVSpan/StepCV));
GRT(Proc_Gain,0)GRT(LambdaMin,0)[ADD(LambdaMin,DeadMin,LamDead),CPT(Kc_Start,TauMin/(Proc_Gain*LamDead)),CPT(Ti_Limit,4.0*LamDead)];
[MOV(TauMin,Ti_Start),GRT(Ti_Start,Ti_Limit)MOV(Ti_Limit,Ti_Start)];
XIC(ApplyGainsReq)XIC(TIC101.DependIndepend)GRT(Ti_Start,0)[MOV(Kc_Start,TIC101.PGain),MOV(Ti_Start,TIC101.IGain),MOV(0.0,TIC101.DGain)];
XIC(ApplyGainsReq)OTU(ApplyGainsReq);Structured Text
The same math and the same apply-on-request logic:
(* Starting PI gains from a manual step test. Run it anywhere; it only does math until asked. *)
(* 1. Process gain K, in % of PV span per % of output *)
IF (StepCV <> 0) AND (PVSpan > 0) THEN
Proc_Gain := ABS(StepPV * 100.0 / PVSpan / StepCV);
END_IF;
(* 2. SIMC starting gains: Kc = Tau / (K * (Lambda + Dead)), and the 4 * (Lambda + Dead) limit *)
IF (Proc_Gain > 0) AND (LambdaMin > 0) THEN
LamDead := LambdaMin + DeadMin;
Kc_Start := TauMin / (Proc_Gain * LamDead);
Ti_Limit := 4.0 * LamDead;
END_IF;
(* 3. Ti = the smaller of Tau and 4 * (Lambda + Dead) *)
Ti_Start := TauMin;
IF Ti_Start > Ti_Limit THEN
Ti_Start := Ti_Limit;
END_IF;
(* 4. Apply on request: dependent form only (PGain = Kc, IGain = Ti in minutes), and only once Ti is set *)
IF ApplyGainsReq THEN
IF TIC101.DependIndepend AND (Ti_Start > 0) THEN
TIC101.PGain := Kc_Start;
TIC101.IGain := Ti_Start;
TIC101.DGain := 0.0;
END_IF;
ApplyGainsReq := 0; (* always clear it, so a refused apply can't fire later *)
END_IF;
Applying only in the dependent form matters: in the independent form, IGain is in repeats per minute (Kc ÷ Ti), and copying Ti straight in would be wrong. If your plant standard is independent gains, set PGain = Kc and IGain = Kc ÷ Ti instead. PIDE's velocity form takes gain changes without a bump in the output, but have someone watching the trend when you apply them anyway.
If your code uses the older PID instruction
PID runs in ladder and structured text, and plenty of working plants still use it. It needs a periodic task, with .UPD (loop update time, in seconds) set to match the task period. Manual is software manual (.SWM) with the output set by .SO; output limits are .MAXO / .MINO, in percent; PV scaling is .MINS / .MAXS.
Both versions run the same instruction with the same operands. Tieback, master loop and inhold aren't used here, so they're 0.
Ladder
One rung in a periodic task. The box shows the seven operands you enter; the last three rows are what the editor displays while online.
Drawn the way the Studio 5000 ladder editor lays it out: inputs on the left, outputs pushed to the right rail. The small numbers with arrows are where the editor shows live values; they read 0 here because nothing is online.
On a phone, swipe sideways to see the whole rung.
Rung 0 as text
PID(TIC101_PID,TT101_Filt,0,TV101_Out,0,0,0);The rung as text
PID(TIC101_PID,TT101_Filt,0,TV101_Out,0,0,0);Structured Text
The same instruction called from structured text:
(* Older PID instruction, in a periodic task. Unused operands are 0. *)
(* TIC101_PID.UPD must equal the task period in seconds, for example 0.1 for 100 ms *)
PID(TIC101_PID, TT101_Filt, 0, TV101_Out, 0, 0, 0);
Watch the units if you move gains between PID and PIDE. In PID's independent form (.PE = 0), .KI is per second and .KD is in seconds. In PIDE's independent form, IGain is per minute and DGain is in minutes. In the dependent forms both use minutes. With the worked example above (Kc 1.33, Ti 2.0 min), PID independent would be .KP = 1.33 and .KI = 1.33 ÷ 120 s ≈ 0.011, while PIDE independent would be PGain = 1.33 and IGain = 0.67. Copy the numbers straight across and the integral is off by a factor of 60.
PIDE also has a built-in autotuner, but it needs a separate activation and only works in function block routines. A manual step test teaches you more about the process anyway.
In Ignition: trend it, and log every change
Tuning without a trend is guessing. Bring the PIDE members in as OPC tags in a TIC101 folder: PV, SP, CV, Auto, Manual, PGain, IGain, DGain, plus SPOper, CVOper, OperAutoReq and OperManualReq for the faceplate. The Oper...Req bits are momentary: the PIDE clears them itself after each execution, so a button only has to write a 1.
- Historize PV, SP and CV. Turn on history for all three, plus
Autoand the three gains. For tuning work, use a periodic sample mode around 1 second, or "on change" with a small historical deadband. A large deadband or a slow sample rate flattens exactly the wiggles you're trying to see. - One chart, two axes. In a Perspective Power Chart (or a Vision Easy Chart), put PV and SP on one axis in °F and CV on a second axis, 0–100%. Most tuning problems are obvious once you can see the output and the PV together: a sawtooth output with a squarish PV is stiction; slow rolling waves are usually too much integral; fast chatter is noise or derivative.
- Before and after. Save the chart's time range from before you change anything, and compare the same kind of upset (a setpoint change, or a normal load change) after. Same window length, same axes, so the comparison is fair.
- Log every gain change. A tag event script on each gain tag writes a row whenever the value changes, from any source: the HMI, Studio 5000, or the apply button.
# Tag Editor > the PGain tag (and IGain, DGain) > Tag Events > Value Changed
def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
if initialChange or previousValue is None:
return
if previousValue.value == currentValue.value:
return
system.db.runPrepUpdate(
"INSERT INTO tuning_log (t_stamp, tag_path, old_value, new_value) VALUES (?, ?, ?, ?)",
[system.date.now(), str(tagPath), previousValue.value, currentValue.value],
"PlantDB")
-- One row per gain change. Adjust types for your database.
CREATE TABLE tuning_log (
t_stamp DATETIME,
tag_path VARCHAR(255),
old_value FLOAT,
new_value FLOAT
);
Change PlantDB to your database connection's name. Tag event scripts run on the Gateway, so they log changes even when no client is open. Put the tuning_log rows in a table next to the trend and the history of "who tuned this and what did they start with" stops being tribal knowledge. If you also want to know who made a change from a screen, turn on auditing for the project.
This is also how we approach tuning on our own jobs, with more data: our in-house, read-only PLC recorder captures setpoint, process value and output over real production runs, and we use that data to help tune loops instead of tweaking by feel. More on that on how we troubleshoot.
Gotchas
Tuning around a sticky valve
A valve with stiction makes the loop cycle no matter what the gains are. Detuning it just makes the cycle slower and bigger. Check the valve in Manual first, and fix it mechanically.
PID and PIDE units differ
PID independent .KI is per second; PIDE independent IGain is per minute. Dependent forms use minutes in both. Converting a loop from PID to PIDE without converting the gains is one of the most common ways to make a good loop bad.
Windup with nothing to control
A loop left in Auto with the pump off ramps its output to the limit, then slams the valve when the pump starts. Hold it in Override or Manual whenever the process can't respond, and use PVFault so a broken transmitter doesn't drive the valve.
Over-filtering
A heavy filter makes the trend look smooth and the loop act worse, because it adds lag. Keep any filter short compared to the dead time, and fix noise at the source first. Derivative on a noisy signal chatters the valve; leave DGain at 0 unless you've got a clean, slow loop.
Wrong span, wrong gains
PIDE works in percent of PVEUMin–PVEUMax. If someone re-ranges the transmitter and not the PLC (or the other way around), the loop's effective gain changes overnight. Re-ranging is a tuning change; log it like one.
Program vs. operator control
PIDE takes mode and setpoint requests from either the program or the operator. If logic holds ProgProgReq on, the HMI's OperAutoReq and SPOper writes are ignored, and operators think the screen is broken. Decide who owns the loop and stick to it.
The process changes with load
Valves and heat exchangers often respond differently at 20% open than at 70%. Step-test in the range the loop normally runs, and recheck after a pump, valve or recipe change. Tuning that was right last year may not be right now.
Continuous task, wandering update
A loop in the continuous task gets a different update time every scan. Use a periodic task, and for the older PID make sure .UPD matches the period. A periodic task that overlaps itself is a problem too; check the task's monitor for overlaps.
The short version
Check the valve, the signal, the span and the direction in Manual first. Step the output, read K, θ and τ off the trend, pick a cautious λ, and start from Kc = τ ÷ (K × (λ + θ)) and Ti = τ, with no derivative. Hold the loop in Override when the process can't respond, trend PV, SP and CV together in Ignition, and log every gain change so the next person knows where you started.