Changing a setpoint gently
Tell a 30 kW pump drive to go from 0 to 50 Hz in one scan and the drive will refuse, trip on overcurrent or slam the water column in the pipe hard enough to bang the check valves. Tell a furnace controller that the setpoint has jumped from 200 °C to 800 °C and the heating elements go to full power while the refractory lining cracks from thermal shock. Real equipment wants its targets to move, not jump.
A ramp generator sits between the target the operator asks for and the setpoint the equipment receives. Every scan it moves its output towards the target by at most one step, where the step is the ramp rate times the scan time. A 5 Hz/s ramp in a 100 ms task moves 0.5 Hz per scan, so 0 to 50 Hz takes 10 s.
PROGRAM Main
VAR
Target : REAL := 50.0; (* input: speed the operator asks for, Hz *)
RampRate : REAL := 5.0; (* input: Hz per second *)
Step : REAL;
SpeedRef : REAL; (* output: setpoint to the drive, Hz *)
END_VAR
Step := RampRate * 0.1; (* this page scans every 0.1 s *)
IF SpeedRef < Target THEN
SpeedRef := MIN(SpeedRef + Step, Target); (* never step past the target *)
ELSIF SpeedRef > Target THEN
SpeedRef := MAX(SpeedRef - Step, Target);
END_IF;
END_PROGRAMWatch SpeedRef climb 0.5 Hz per scan and stop exactly at 50. Type a new target of 20 while it is still climbing: it turns round and ramps down. The MIN and MAX are the important part. Without them the ramp oversteps the target, and the next scan steps back:
The ramp rate is really a scan-time calculation. If the task runs every 10 ms instead of 100 ms, the step must be ten times smaller for the same Hz per second. Hard-code 0.1 and move the program to a faster task, and every ramp on the machine gets ten times steeper. In production code pass the cycle time in, or read it from the task configuration, and run ramps and controllers in a fixed-interval task: a cyclic interrupt OB (OB30–OB38) on an S7-1500, a cyclic task in CODESYS or TwinCAT, a periodic task in Studio 5000.
A drive already has its own ramp, so why ramp in the PLC as well? Because the two do different jobs. The drive's ramp-up and ramp-down times (on a SINAMICS G120, parameters p1120 and p1121) protect the drive and the mechanics, and are set once at commissioning. The PLC ramp shapes the process change, and can differ by recipe, by product or by mode. If both ramp, the slower one wins; make sure the drive's ramp is the faster of the two, or the PLC's carefully chosen rate never reaches the motor.
Closing the loop: proportional control
A ramp is open loop: it decides the output without looking at the result. Closed-loop control measures the process and corrects. Every controller in this chapter starts from the error:
Error = Setpoint − ProcessValue
The simplest controller makes its output proportional to the error: Out = Kp · Error. A big error gives a big correction, a small error a small one. Kp is the gain, in percent of output per unit of error.
To watch a loop you need a process, so the programs from here on include one. It is a small electrically heated water bath, modelled as a first-order process: the temperature moves towards the temperature the heater could sustain, at a rate set by a time constant. With the heater at 100 % it would settle 80 °C above a 20 °C ambient. The time constant is 5 s, compressed so that you can watch a loop settle in seconds rather than the half hour a real tank takes.
PROGRAM Main
VAR
SP : REAL := 60.0; (* input: setpoint, deg C *)
Kp : REAL := 5.0; (* input: gain, % per deg C *)
PV : REAL := 20.0; (* process value: bath temperature *)
Err : REAL;
Out : REAL; (* heater power, % *)
Dx : REAL;
END_VAR
(* The controller *)
Err := SP - PV;
Out := LIMIT(0.0, Kp * Err, 100.0);
(* The process: first-order, gain 0.8 deg C per %, time constant 5 s *)
Dx := 20.0 + 0.8 * Out - PV; (* how far PV is from where Out would take it *)
PV := PV + Dx * 0.1 / 5.0; (* Ts / Tau of the way, every scan *)
END_PROGRAMLet it run for ten seconds. The temperature rises fast, then slows down, and stops short of the setpoint.
P-only control is still the right answer in some places: a level in a surge tank, where "somewhere near the middle" is all you want, is often run P-only on purpose. For temperature, pressure and flow, the offset is not acceptable, and something has to remember the error.
Gain, proportional band and units
"Kp = 5" is meaningless until you know the units. In this example it is 5 % of heater output per °C of error. Many controllers, PID_Compact among them, work instead with the process value and the output both normalised, so the gain is dimensionless: 1 % of output per 1 % of the measurement span. Older controllers and many process engineers speak of proportional band (PB): the change in process value, as a percentage of span, that moves the output from 0 to 100 %. PB = 100 / Kp when Kp is dimensionless.
For the bath, suppose the transmitter is ranged 0–100 °C. Then 1 °C is 1 % of span, the dimensionless gain is also 5, and the proportional band is 20 %: an error of 20 °C drives the heater from 0 to full power. Range the same transmitter 0–200 °C and the identical controller needs a dimensionless gain of 10 to behave the same way, because each percent of span is now 2 °C. That is the trap in copying gains from one machine to another: the transmitter ranges must match before the numbers mean the same thing.
Which way does the controller push?
So far, more heater power means a higher temperature, so a positive error (process too cold) should increase the output. That is a reverse-acting controller in ISA terms: the output moves the opposite way to the process value. A cooling-water valve is the other case. Open it further and the temperature falls, so when the process is too hot the output must increase: a direct-acting controller, where output rises as the process value rises.
In code the difference is the sign of the error: Err := SP - PV for a heater, Err := PV - SP for a cooler. Vendor blocks have a setting for it: PID_Compact has Invert control logic, and PIDE in Studio 5000 has a control action setting that chooses between SP − PV and PV − SP.
Get it wrong and the loop does something unmistakable. A cooling loop configured like a heating loop sees the temperature rise above its setpoint, reads that as a negative error and closes the valve further, so the temperature rises faster. Within a minute the output is pinned at one limit and the process value is running away. If you see an output sitting at 0 % or 100 % while the error grows, check the action before you touch the tuning. No gain value can fix the wrong sign.
The action also depends on the actuator. An air-to-close valve (fails open) reverses the relationship between the 4–20 mA signal and the flow, which chapter 19 put in the I/O list. Decide the action from the whole chain: controller output, analog card, positioner, valve, process. Write it down, and confirm it at commissioning with a small manual step before the loop goes to auto.
Integral action: remembering the error
The integral term adds up the error over time. As long as any error remains, the integral keeps growing, and the output keeps moving, until the error is zero. Then the integral stops changing and holds exactly the output the process needs. That is how a PI controller removes the offset.
In a PLC the integral is a running sum, updated once per scan:
I := I + Kp · (Ts / Ti) · Error, then Out := Kp · Error + I
Ts is the scan time and Ti is the integral (reset) time in seconds: the time the integral would take to repeat the proportional action for a constant error. Smaller Ti means stronger, faster integral action. PID_Compact uses this form with Ti in seconds, so the numbers you learn here are numbers you can type into it. Other blocks express the same idea in other units, which the table below sorts out. Because I must survive between scans, a real controller is a function block, never a function (chapter 18).
PROGRAM Main
VAR
SP : REAL := 60.0; (* input: setpoint, deg C *)
Kp : REAL := 2.0; (* input: gain *)
Ti : REAL := 4.0; (* input: integral time, s *)
PV : REAL := 20.0;
Err : REAL;
P : REAL;
I : REAL;
Out : REAL;
Dx : REAL;
END_VAR
Err := SP - PV;
P := Kp * Err;
IF Ti > 0.0 THEN
I := I + Kp * 0.1 / Ti * Err; (* Ts = 0.1 s *)
END_IF;
Out := LIMIT(0.0, P + I, 100.0);
Dx := 20.0 + 0.8 * Out - PV;
PV := PV + Dx * 0.1 / 5.0;
END_PROGRAMThis time the temperature arrives at 60 °C and stays there. Watch the hand-over: at the start almost all of the output is P; at the end P is near zero and I holds the 50 % the bath needs at 60 °C. Change SP to 40 and watch the integral unwind to 25 %. Try Ti = 1 for a faster, overshooting response, or Ti = 20 for a slow crawl. Tuning is the craft of choosing Kp and Ti for the process you have; the mathematics is these two lines.
One controller, three ways to write it
The same PI controller appears in vendor manuals in different forms, and copying a number from one form into another is one of the commonest commissioning mistakes. The dependent (or "ideal", "standard") form is the one used above: Kp multiplies both terms, and the integral is set by a time Ti. The independent (or "parallel") form gives each term its own gain: Out = Kp · E + Ki · ∫E dt, with Ki = Kp / Ti.
Take the loop above, Kp = 2 and Ti = 4 s, and write it every way you will meet:
| Form and units | Integral setting for Kp = 2, Ti = 4 s |
|---|---|
| Dependent, Ti in seconds (PID_Compact, this chapter) | Ti = 4 s |
| Dependent, Ti in minutes per repeat | Ti = 0.0667 min |
| Dependent, repeats per minute (1 / Ti in minutes) | 15 repeats/min |
| Independent, Ki in 1/s | Ki = Kp / Ti = 0.5 /s |
| Independent, Ki in 1/min | Ki = 30 /min |
Studio 5000's PIDE works in minutes: in its dependent-gains form the integral setting is a time in minutes, and in its independent form the integral gain is per minute. So the 4 s you tuned here becomes 0.0667 in one PIDE form and 30 in the other. The classic Logix PID instruction states its independent integral gain per second, so the same loop is 0.5 there. A factor of 60 hidden in a copied tuning sheet produces either a loop that never removes its offset or one that oscillates on the first disturbance. Before you type a number into a vendor block, read which form it uses and in what time unit.
Windup, and how to stop it
The clamp creates a new problem. When the output is pinned at 100 % because the process cannot keep up, the error stays large and the integral keeps adding it up. By the time the process arrives, the integral holds an enormous value, and the output stays at 100 % long after it should have backed off. The temperature sails past the setpoint. This is integral windup, and it is the most common fault in home-made PI code.
The fix used here is conditional integration: stop integrating while the output is saturated and the error is pushing it further into saturation.
PROGRAM Main
VAR
SP : REAL := 90.0; (* input: a big step, so the heater saturates *)
AntiWindup : BOOL; (* input: restart with it on and off *)
Kp : REAL := 2.0;
Ti : REAL := 4.0;
PV : REAL := 20.0;
Err, P, I, Out, Dx : REAL;
PeakPV : REAL; (* highest temperature reached *)
Saturated : BOOL;
END_VAR
Err := SP - PV;
P := Kp * Err;
Saturated := (P + I >= 100.0 AND Err > 0.0) OR (P + I <= 0.0 AND Err < 0.0);
IF NOT (AntiWindup AND Saturated) THEN
I := I + Kp * 0.1 / Ti * Err;
END_IF;
Out := LIMIT(0.0, P + I, 100.0);
Dx := 20.0 + 0.8 * Out - PV;
PV := PV + Dx * 0.1 / 5.0;
PeakPV := MAX(PeakPV, PV);
END_PROGRAMRun it for twenty seconds with AntiWindup off: I climbs past 120 while the heater is already at full power, and the bath overshoots to about 97 °C before it comes back. Switch it on, press Restart, and run again: the peak is about 90.1 °C. The gains and the process are the same; the only difference is that the integral stopped growing when growing could not help.
Other anti-windup methods exist. Clamping the integral to the output range is the crudest. Back-calculation bleeds the integral back towards the clamped output with its own time constant and is what many vendor blocks use internally. All of them do the same job: keep the integral from promising an output the actuator cannot deliver.
Anything downstream that stops the output from acting causes windup too: a valve whose air supply is isolated, a drive that is not enabled, an interlock that forces the output to zero while the controller stays in auto. The controller keeps integrating an error it cannot influence. When the interlock clears, the output jumps to wherever the integral has wandered. The fix is to tell the controller: put it into manual, or tracking, whenever something else owns the actuator, so its integral follows the real output. The manual and auto section below shows the tracking.
The velocity form
Some controllers avoid storing the integral at all. The velocity (incremental) form computes the change in output each scan, ΔOut = Kp · (E − E_previous) + Kp · (Ts / Ti) · E, and adds it to the previous output. Because the output itself is the only state, clamping the output automatically stops it from winding up, and switching from manual to auto starts from wherever the output already is. Rockwell's PIDE uses the velocity form, which is why its manual/auto changes and output limits are bumpless by design. The two forms behave the same while nothing saturates; they differ in how they recover from limits and mode changes, which is where most loop trouble happens.
Dead time: what makes loops oscillate
The bath so far responds the instant the heater changes. Real processes rarely do. Hot water from a mixing valve takes seconds to travel down the pipe to the temperature sensor; material on a conveyor takes time to reach the weigh belt; a gas analyser takes a sample, conditions it and reports a minute later. That delay, before the measurement starts to move at all, is dead time, and it is what limits how hard a loop can be pushed.
The controller cannot see the effect of what it did until the dead time has passed. So it keeps pushing, sees nothing, pushes harder, and by the time the measurement finally responds, the output has gone too far. Then it overcorrects the other way. Here is the same PI controller and bath, with an adjustable delay between the heater and the bath:
PROGRAM Main
VAR
SP : REAL := 60.0; (* input: setpoint, deg C *)
Kp : REAL := 2.0; (* input: gain, % per deg C *)
Ti : REAL := 4.0; (* input: integral time, s *)
DeadTime : REAL := 0.0; (* input: transport delay, s (0 to 4.9) *)
PV : REAL := 20.0;
Err : REAL;
P : REAL;
I : REAL;
Out : REAL;
Sat : BOOL;
Delayed : REAL; (* the heater power the bath feels now *)
Pipe : ARRAY[0..49] OF REAL; (* 5 s of output history at 0.1 s *)
Head : INT;
Lag : INT;
PeakPV : REAL;
END_VAR
(* The controller, with conditional integration *)
Err := SP - PV;
P := Kp * Err;
Sat := (P + I >= 100.0 AND Err > 0.0) OR (P + I <= 0.0 AND Err < 0.0);
IF Ti > 0.0 AND NOT Sat THEN
I := I + Kp * 0.1 / Ti * Err;
END_IF;
Out := LIMIT(0.0, P + I, 100.0);
(* The process: the same bath, but the heat arrives DeadTime seconds late *)
Lag := REAL_TO_INT(LIMIT(0.0, DeadTime, 4.9) * 10.0);
Pipe[Head] := Out;
Delayed := Pipe[(Head - Lag + 50) MOD 50];
Head := (Head + 1) MOD 50;
PV := PV + (20.0 + 0.8 * Delayed - PV) * 0.1 / 5.0;
PeakPV := MAX(PeakPV, PV);
END_PROGRAMWith no dead time the loop peaks at about 60.5 °C. Restart with DeadTime at 2 s and it peaks near 63.6 °C and rings once before it settles. At 3 s the peak is about 68.6 °C with several swings. At 4 s the same gains that were fine a moment ago swing the temperature between the mid-forties and the mid-seventies and take minutes to calm down. Nothing changed in the controller.
The rule of thumb that falls out of this: the ratio of dead time to time constant decides how difficult a loop is. A process whose dead time is small compared with its time constant (this bath with no delay) tolerates high gains. Once the dead time approaches the time constant, the gains have to come down, the loop gets slower, and no tuning will make it fast. On a real plant the cheapest improvement is often mechanical: move the sensor closer to the mixing point, or shorten the sample line, and the loop can be tuned twice as fast.
Tuning from a step test
You can tune by trial and error, and many loops are. A step test is quicker and leaves a record. With the loop in manual and the process steady, step the output by a known amount and record the response. Three numbers describe a self-regulating process well enough to tune it:
- Process gain K: how far the process value moves per percent of output, once it has settled.
- Dead time θ: how long after the step before the process value starts to move.
- Time constant τ: how long after it starts moving before it has covered 63 % of its total change.
Here is the bath with a 1 s dead time, in manual at 30 % output and steady at 44 °C. Flip StepUp to step the output to 50 % and watch the trend:
PROGRAM Main
VAR
StepUp : BOOL; (* input: step the output from 30 % to 50 % *)
Out : REAL; (* manual output, % *)
PV : REAL := 44.0; (* steady at 30 %: 20 + 0.8 x 30 *)
Pipe : ARRAY[0..49] OF REAL;
Head : INT;
Delayed : REAL;
Primed : BOOL;
Since : TON; (* time since the step *)
Elapsed : TIME;
END_VAR
IF NOT Primed THEN
FOR Head := 0 TO 49 DO
Pipe[Head] := 30.0; (* the history is 30 % before the test *)
END_FOR;
Head := 0;
Primed := TRUE;
END_IF;
Out := SEL(StepUp, 30.0, 50.0);
Since(IN := StepUp, PT := T#1h);
Elapsed := Since.ET;
(* Bath: gain 0.8 deg C per %, time constant 5 s, dead time 1 s *)
Pipe[Head] := Out;
Delayed := Pipe[(Head - 10 + 50) MOD 50];
Head := (Head + 1) MOD 50;
PV := PV + (20.0 + 0.8 * Delayed - PV) * 0.1 / 5.0;
END_PROGRAMRead the three numbers off the response:
| Step | Reading | Result |
|---|---|---|
| Output change | 30 % → 50 % | ΔOut = 20 % |
| Final process value | 44 °C → 60 °C | ΔPV = 16 °C |
| Process gain | 16 °C ÷ 20 % | K = 0.8 °C/% |
| Dead time | PV first moves about 1 s after the step | θ = 1 s |
| 63 % point | 44 + 0.63 × 16 ≈ 54.1 °C, reached about 6 s after the step | τ = 6 − 1 = 5 s |
Lambda tuning (also called IMC tuning) turns those numbers into PI settings with one choice: λ, the closed-loop time constant you want. For a self-regulating process:
Kp = τ / (K · (λ + θ)) and Ti = τ
Choose λ = τ = 5 s, a cautious and forgiving starting point, and you get Kp = 5 / (0.8 × 6) ≈ 1.04 %/°C and Ti = 5 s. In the dead-time example above, with DeadTime set to 1, those settings bring the bath from 20 °C to 60 °C with no overshoot in about 30 s. Halve λ to 2.5 s and Kp becomes 1.79: the loop is faster and still does not overshoot on this ideal process. On a real one, with valve deadband, noise and a time constant that changes with load, the cautious setting usually holds up better. And for the 4 s dead-time case that oscillated, the same rule with λ = 5 s gives Kp ≈ 0.69, Ti = 5 s, which peaks at about 60.4 °C.
Two cautions from experience. Do the step in the direction and at the operating point the loop will run at, because many processes have a different gain at 20 % output than at 80 %. And do not do it at all without the operator's agreement and a defined limit: a step test on a reactor is a process change. Chapter 23 covers testing against a simulated plant first, which is where this chapter's bath came from.
Level is the common exception to all of this. A tank with a pumped outlet does not settle after a step in inflow: the level keeps rising for as long as inflow exceeds outflow. That is an integrating process, with no K and no τ in the sense above, and it is tuned differently (the step test measures the rate of rise per percent of output instead). Lambda tuning has a version for integrating processes; the vendor's documentation or a control textbook gives the formulas.
Manual, auto and the bumpless transfer
Every real loop has a manual mode. The operator takes the valve, sets 40 %, fixes something, then hands the loop back to the controller. If the controller's integral is still holding last week's value, the output jumps the moment auto is selected, from 40 % to whatever the integral says. Valves slam, levels surge, operators lose trust in the loop.
A bumpless transfer prevents the jump. The simple version has two parts:
- In manual, the output follows the operator's value, and the integral tracks it: set
I := ManualOut - Pevery scan, so that P + I already equals the manual output. - When auto takes over, the first scan computes
P + Iand gets the value the operator left, plus one scan's worth of integral. There is no bump. From there the controller moves the output smoothly towards what the process needs.
Going the other way, most HMIs copy the controller output into the manual value at the moment of switching. Vendor blocks do the tracking for you; the second challenge below makes you build it once so you know what they are doing.
A related decision is what the setpoint does in manual. With setpoint tracking, the setpoint follows the process value while the loop is in manual, so on return to auto the error is zero and the controller starts from rest. Without it, the operator's old setpoint is still there, and the controller starts correcting the moment it is in auto, smoothly, from the manual output, but correcting all the same. Both are valid. Setpoint tracking suits loops where the operator steers the process by hand to a new point; a fixed setpoint suits loops where the setpoint is a specification, such as a pasteuriser temperature. Make the choice per loop and put it in the functional specification.
Vendor PID blocks, and when to use them
You will rarely write a PID controller from scratch for a production machine. Every platform ships a proven one:
| Platform | Block | Worth knowing |
|---|---|---|
| Siemens S7-1200 / S7-1500 | PID_Compact | Call it in a cyclic interrupt OB. Pre-tuning and fine tuning from the TIA commissioning screen; outputs as REAL, analog or PWM. Invert control logic sets the action. PID_3Step for motorised valves. |
| Rockwell ControlLogix / CompactLogix | PIDE | Velocity form, so manual/auto and output limits are bumpless by design. Integral settings in minutes. Usually used in function block diagrams; the older PID instruction exists in ladder. |
| CODESYS | PID / PID_FIXCYCLE (Util library) | PID_FIXCYCLE takes the cycle time as an input, which makes it deterministic in a fixed task. |
| Beckhoff TwinCAT 3 | FB_CTRL_PID, FB_CTRL_PI (Tc2_ControllerToolbox) | Configured through a parameter structure that includes the cycle time and anti-windup limits. |
Use them. They handle the corner cases (derivative filtering, bumpless mode changes, output rate limits, the cycle-time maths) that a home-made controller takes months of commissioning to discover. But a vendor block is only as good as the gains you type in and the scan time you call it at. The most common PID_Compact fault I meet in the field is not the algorithm. It is a block called from OB1, whose scan time varies, with gains copied from another machine.
How fast should the task be? Fast enough that the controller sees the process move many times during one time constant. A loop executing ten or more times per process time constant behaves like the continuous controller the tuning rules assume; slower than that, the sampling itself adds a delay that acts like extra dead time. A temperature loop with a 5-minute time constant is comfortable at 1 s. A flow loop with a time constant of a second or two wants 100 ms or faster. Running every loop in the plant at 10 ms because it is the fastest task wastes CPU time and gains nothing.
The derivative term (the D in PID) reacts to how fast the error is changing. It helps on processes with long lags, but it amplifies measurement noise, and most level, flow and pressure loops run as PI. If you add it, apply it to the process value, not to the error, so a setpoint change does not produce a kick, and filter it: every vendor block has a derivative filter (a lag on the D term, often set as a fraction of Td), and on a noisy signal it is not optional.
A PI controller you can reuse
With the pieces from this chapter, you can write a PI function block that would survive a commissioning job: scan time as an input, either action, output limits, conditional integration and manual tracking. Here it is controlling two zones of a small oven, each with its own process and its own tuning:
FUNCTION_BLOCK FB_PI
VAR_INPUT
Sp : REAL; (* setpoint, engineering units *)
Pv : REAL; (* process value *)
Kp : REAL; (* gain, % output per unit of error *)
Ti : REAL; (* integral time, s; 0 = P only *)
Ts : REAL := 0.1; (* task period, s *)
Direct : BOOL; (* TRUE = output rises as Pv rises (cooling) *)
Auto : BOOL; (* FALSE = manual *)
ManOut : REAL; (* manual output, % *)
OutLo : REAL := 0.0;
OutHi : REAL := 100.0;
END_VAR
VAR_OUTPUT
Out : REAL; (* clamped output, % *)
Saturated : BOOL; (* output held at a limit by the error *)
END_VAR
VAR
Err : REAL;
P : REAL;
I : REAL;
END_VAR
IF Direct THEN
Err := Pv - Sp;
ELSE
Err := Sp - Pv;
END_IF;
P := Kp * Err;
IF NOT Auto THEN
Out := LIMIT(OutLo, ManOut, OutHi);
I := Out - P; (* track for a bumpless return *)
Saturated := FALSE;
ELSE
Saturated := (P + I >= OutHi AND Err > 0.0) OR (P + I <= OutLo AND Err < 0.0);
IF Ti > 0.0 AND NOT Saturated THEN
I := I + Kp * Ts / Ti * Err;
END_IF;
Out := LIMIT(OutLo, P + I, OutHi);
END_IF;
END_FUNCTION_BLOCK
PROGRAM Main
VAR
Sp1 : REAL := 120.0; (* input: zone 1 setpoint, deg C *)
Sp2 : REAL := 90.0; (* input: zone 2 setpoint, deg C *)
Auto2 : BOOL := TRUE; (* input: zone 2 auto/manual *)
Man2 : REAL := 30.0; (* input: zone 2 manual output, % *)
Zone1 : FB_PI;
Zone2 : FB_PI;
Zone1Pv : REAL := 20.0;
Zone2Pv : REAL := 20.0;
END_VAR
Zone1(Sp := Sp1, Pv := Zone1Pv, Kp := 1.5, Ti := 6.0, Auto := TRUE);
Zone2(Sp := Sp2, Pv := Zone2Pv, Kp := 2.5, Ti := 10.0, Auto := Auto2, ManOut := Man2);
(* Two simulated zones: zone 1 has the bigger heater, zone 2 the bigger mass *)
Zone1Pv := Zone1Pv + (20.0 + 1.6 * Zone1.Out - Zone1Pv) * 0.1 / 6.0;
Zone2Pv := Zone2Pv + (20.0 + 1.0 * Zone2.Out - Zone2Pv) * 0.1 / 12.0;
END_PROGRAMBoth zones come up to temperature independently, each instance with its own integral. Switch Auto2 off and move Man2: zone 2 follows the manual output while its integral tracks; switch it back on and the output carries on from where the operator left it. The block calls omit Ts, OutLo and OutHi, which keep their declared defaults; on a real controller I would pass Ts from the task configuration so nobody can move the block to another task and forget it.
Three things this block still does not do, and a production version would: it has no derivative term; it has no setpoint ramp (put the ramp generator from the start of this chapter in front of Sp); and it trusts Pv completely. That last one is the subject of the next section.
Time-proportioning outputs for heaters
The oven zones above have a 0–100 % output, but many heaters are switched by a contactor or a solid-state relay: on or off, nothing between. A time-proportioning output turns the percentage into an on/off pattern. Over a fixed cycle, the heater is on for the controller's percentage of the cycle: at 40 % with a 2 s cycle, on for 0.8 s and off for 1.2 s. PID_Compact offers this directly as its PWM output.
PROGRAM Main
VAR
Out : REAL := 40.0; (* input: controller output, % *)
Phase : TIME; (* position in the 2 s cycle; this is a fixed 100 ms task *)
OnMs : DINT; (* on-time in this cycle, ms *)
HeaterOn : BOOL; (* output: SSR or contactor *)
PV : REAL := 20.0; (* the bath, driven on/off *)
Power : REAL;
END_VAR
OnMs := REAL_TO_DINT(LIMIT(0.0, Out, 100.0) * 20.0); (* 2000 ms x Out / 100 *)
HeaterOn := TIME_TO_DINT(Phase) < OnMs;
Phase := Phase + T#100ms;
IF Phase >= T#2s THEN
Phase := T#0s; (* start the next cycle *)
END_IF;
IF HeaterOn THEN
Power := 100.0;
ELSE
Power := 0.0;
END_IF;
PV := PV + (20.0 + 0.8 * Power - PV) * 0.1 / 5.0;
END_PROGRAMWatch HeaterOn follow the 2 s cycle (eight scans on, twelve off at 40 %) and PV climb into a sawtooth that swings between about 48 and 56 °C. Its average is 52 °C, the same place 40 % of steady power would take it. The cycle counts scans of a fixed-period task, so it is exactly 2 s. A timer that restarts itself (Cycle(IN := NOT Cycle.Q, ...)) spends a scan at each restart with its input off, so the cycle runs slightly longer than its preset and the duty shifts a little at every output level. The sawtooth is visible here because this bath's 5 s time constant is only a few cycles long. On a real oven with a time constant of many minutes, the same 2 s cycle produces a ripple too small to measure.
Choosing the cycle time is a trade-off. A short cycle keeps the ripple small but switches more often. A solid-state relay does not mind that; a contactor does, because its contacts wear with every operation, and a contactor switching every 2 s on a heater that runs all year will need replacing within months. Contactors want cycles of tens of seconds and a process slow enough to absorb them. Solid-state relays allow a cycle of a second or two and give much finer control. Whichever you use, the cycle time belongs in the specification, and it has to be short compared with the process time constant.
When the measurement goes bad
A controller trusts its process value absolutely. If the transmitter fails low, a heating controller sees a cold process and drives the heater to 100 %. If it fails high, a cooling controller does the same with the cooling. A broken wire on a temperature loop is, from the controller's point of view, a very large error that needs a very large correction.
So every loop needs a rule for a bad measurement, written into the code and the specification. The usual choices:
- Switch to manual and hold the last output, with an alarm. The process keeps roughly where it was while someone fixes the transmitter. This is the common default.
- Switch to manual and drive a defined safe output: heater off, cooling valve fully open. Use this where holding is not safe.
- Hold for a short time, then one of the above, for signals that drop out briefly (a radio link, a network value).
With the analog block from chapter 19, this is a few lines: when LT101.Bad is TRUE, call the PI block with Auto := FALSE and ManOut set to the held or safe output. Because the block tracks in manual, the loop returns to auto bumplessly when the signal is good again and the operator selects auto.
None of this is the over-temperature protection. A heater whose PLC controller fails to full power, for whatever reason (a failed transmitter, a welded SSR, a program error), must be stopped by an independent over-temperature limiter with its own sensor, which removes power from the heater without asking the PLC. On a process plant, where people or the environment are at risk, that protection is a safety instrumented function designed to IEC 61511; on a machine, it comes out of the risk assessment. The PLC's high-temperature alarm and the control loop are not substitutes for it.
From loops to well-built programs
The programs in this chapter are getting long enough that their structure counts as much as their logic. The next chapter is about exactly that: naming, commenting and organising ST so that it reads like the machine, and so that it survives the move between CODESYS, TwinCAT, TIA Portal and Studio 5000.