When you don't know the count
A FOR loop is the right tool when you know how many elements there are: eight tanks, twelve zones, sixty-four alarms. Some jobs are shaped differently. You are searching a table and want to stop at the right row. You are stepping round a ring of pumps until you find one that is healthy. You are walking a calibration table until the reading falls inside a segment. The number of iterations depends on the data, and the natural way to say it is "keep going until".
Structured Text has two loops for that:
WHILE condition DO
(* body *)
END_WHILE;
REPEAT
(* body *)
UNTIL condition
END_REPEAT;
The difference is where the test sits. WHILE checks its condition before each pass, so the body may run zero times. REPEAT checks after each pass, so the body always runs at least once. Notice also that the two conditions mean opposite things: WHILE runs as long as its condition is TRUE, REPEAT runs until its condition becomes TRUE.
Everything you learned about FOR in chapter 13 still applies. The whole loop runs inside one scan, every iteration costs scan time, and no input changes while it runs. That last fact is the one this chapter keeps coming back to, because it is behind the most expensive loop mistake in PLC programming.
WHILE: checked before
A horizontal diesel tank is a cylinder lying on its side, so its volume is not proportional to its level. The tank supplier provides a strapping table: volume at a list of level points. The PLC finds the segment the radar reading falls in and interpolates between its two ends. Finding the segment is a natural WHILE:
PROGRAM Main
VAR CONSTANT
POINTS : INT := 6;
END_VAR
VAR
LevelMm : REAL := 900.0; (* input: radar level, mm *)
TabLevel : ARRAY[1..6] OF REAL := [0.0, 400.0, 800.0, 1200.0, 1600.0, 2000.0];
TabVol : ARRAY[1..6] OF REAL := [0.0, 1.9, 5.2, 8.8, 12.1, 14.0]; (* m³ *)
L : REAL;
Seg : INT; (* table segment: between point Seg and Seg + 1 *)
Steps : INT; (* loop passes this scan *)
Frac : REAL;
VolumeM3 : REAL;
END_VAR
L := LIMIT(0.0, LevelMm, 2000.0); (* keep the search inside the table *)
Seg := 1;
Steps := 0;
WHILE Seg < POINTS - 1 AND TabLevel[Seg + 1] < L DO
Seg := Seg + 1;
Steps := Steps + 1;
END_WHILE;
Frac := (L - TabLevel[Seg]) / (TabLevel[Seg + 1] - TabLevel[Seg]);
VolumeM3 := TabVol[Seg] + Frac * (TabVol[Seg + 1] - TabVol[Seg]);
END_PROGRAMAt 900 mm the loop steps twice and stops in segment 3, between 800 and 1200 mm: 6.1 m³. Type 150 and the condition is false at the very first test, so the body runs zero times and Seg stays 1. Type 1950 and it walks to segment 5. Then try 5000: the LIMIT holds the search at the top of the table instead of walking off the end of it.
Two things make this loop safe, and both are deliberate. The counter Seg changes in every pass, so the condition must eventually become false. And the condition has its own upper limit, Seg < POINTS - 1, which ends the loop at the last segment whatever the level reads. Never trust the data alone to end a loop — the day the radar returns NaN, a comparison that "must" become false doesn't.
One more detail hides in that condition. IEC 61131-3 does not promise that AND stops evaluating once the left side is FALSE. CODESYS and TwinCAT evaluate both sides of AND and offer AND_THEN for the cases where you need the short-circuit; other platforms differ, and some optimise one way in one release and another way in the next. So Seg < 5 AND TabLevel[Seg + 1] < L must be safe even when Seg is 5 — and it is, because index 6 exists. Write every loop condition so that each half is valid on its own.
REPEAT: checked after
A booster station has four pumps on duty rotation. Once a week, or when an operator asks, duty passes to the next pump in the ring — skipping any that are faulted. "Move at least one step, then keep moving while the pump you landed on is faulted" is exactly what REPEAT says:
PROGRAM Main
VAR
Changeover : BOOL; (* input: duty changeover request *)
P2Fault : BOOL; (* input: pump 2 drive fault *)
P3Fault : BOOL; (* input: pump 3 drive fault *)
Faulted : ARRAY[1..4] OF BOOL;
Edge : R_TRIG;
Duty : INT := 1; (* the pump currently on duty *)
Tries : INT;
NoPump : BOOL;
END_VAR
Faulted[2] := P2Fault;
Faulted[3] := P3Fault;
Edge(CLK := Changeover);
IF Edge.Q THEN
Tries := 0;
REPEAT
Duty := Duty MOD 4 + 1; (* 1 -> 2 -> 3 -> 4 -> 1 *)
Tries := Tries + 1;
UNTIL NOT Faulted[Duty] OR Tries >= 4
END_REPEAT;
END_IF;
NoPump := Faulted[Duty];
END_PROGRAMFlip Changeover on and off: duty moves 1 → 2 → 3 → 4 → 1. Now switch P2Fault on and change over from pump 1. The loop lands on pump 2, finds it faulted, takes a second step and stops on pump 3, so Tries reads 2. A WHILE could do the same, but it would need a separate first step outside the loop; REPEAT says "at least once" without it.
Two habits are on show again. The rotation runs only on the rising edge of the request (chapter 8) — without the R_TRIG, duty would race round the ring ten times a second. And the Tries >= 4 limit means that with every pump faulted the loop stops after one full lap instead of spinning for ever. Take that limit out, fault all four pumps, and the scan never ends.
EXIT and CONTINUE
EXIT leaves the innermost loop immediately; the program carries on after its END_FOR, END_WHILE or END_REPEAT. It is how you stop a search once you have the answer. A carton labeller looks up the scanned article code in its product table:
PROGRAM Main
VAR
Scanned : DINT := 40213; (* input: article code from the barcode reader *)
Codes : ARRAY[1..8] OF DINT := [40101, 40102, 40213, 40214, 40350, 40351, 40400, 40999];
Recipe : INT; (* row of the table, 0 = unknown article *)
Looked : INT; (* rows compared this scan *)
i : INT;
END_VAR
Recipe := 0;
Looked := 0;
FOR i := 1 TO 8 DO
Looked := Looked + 1;
IF Codes[i] = Scanned THEN
Recipe := i;
EXIT; (* found it: stop searching *)
END_IF;
END_FOR;
END_PROGRAMWith 40213 the loop compares three rows and leaves. Type 40999 and it needs all eight; type 12345 and it needs all eight and reports 0 — unknown article, reject the carton. Note that the answer is stored in Recipe before the EXIT; the loop counter itself is not a reliable result after the loop, as chapter 13 explained.
CONTINUE skips the rest of the current pass and goes straight to the next one. It is handy for leaving out elements that should not count — here, a level probe taken out of service for maintenance:
PROGRAM Main
VAR
P3Bypass : BOOL; (* input: probe 3 out of service *)
Probe : ARRAY[1..5] OF REAL := [41.0, 40.5, 47.0, 39.5, 42.0]; (* % *)
InService : ARRAY[1..5] OF BOOL := [TRUE, TRUE, TRUE, TRUE, TRUE];
Sum : REAL;
Used : INT;
Avg : REAL;
i : INT;
END_VAR
InService[3] := NOT P3Bypass;
Sum := 0.0;
Used := 0;
FOR i := 1 TO 5 DO
IF NOT InService[i] THEN
CONTINUE; (* skip this probe, go to the next *)
END_IF;
Sum := Sum + Probe[i];
Used := Used + 1;
END_FOR;
IF Used > 0 THEN
Avg := Sum / INT_TO_REAL(Used);
ELSE
Avg := 0.0; (* no valid probe: report it, don't divide by zero *)
END_IF;
END_PROGRAMBypass probe 3 and the average drops from 42.0 to 40.75 % over four probes. Be aware that CONTINUE arrived with the 3rd edition of IEC 61131-3. CODESYS, TwinCAT and TIA Portal SCL support it; Studio 5000's Structured Text has EXIT but no CONTINUE. Where it is missing, invert the test and wrap the rest of the body in IF InService[i] THEN … END_IF; — which many engineers prefer anyway, because the body then reads top to bottom with no jumps.
Use both statements sparingly. One EXIT at the point where a search succeeds is clear. Three EXITs and a CONTINUE scattered through a long body make a loop that nobody can reason about at 3 a.m.
The watchdog: when a loop never ends
A loop whose condition never becomes false does not hang "for a while". It hangs the scan, and with it the whole controller:
(* never run this on a machine *)
WHILE Pressure < 6.0 DO
Compressor := TRUE;
END_WHILE;
Every controller guards against this with a cycle-time watchdog. An S7-1500 has a default maximum cycle time of 150 ms; exceed it and the CPU calls the time-error OB80, and goes to STOP if that OB is missing or if the cycle runs to twice the limit. A ControlLogix task has its own watchdog, 500 ms by default, and a trip is major fault type 6, code 1. CODESYS and TwinCAT let you set a watchdog time per task and stop the application when it expires. The engine on this page counts iterations instead: a WHILE that passes 100,000 iterations in one scan is stopped with a cycle watchdog fault.
A stopped CPU is the good outcome. Every output goes to its configured safe state, the HMI shows a red banner and someone comes to look. The bad outcome is a loop that nearly never ends: one that takes 120 ms on the day a table grows, stretches the scan of a 10 ms task by a factor of twelve, and makes every timer, edge and PID loop in the program misbehave without a single fault in the diagnostic buffer.
Never loop waiting for the machine
Look again at that compressor loop. It is not just slow. It cannot work, ever, on any PLC. Inside the loop the program reads Pressure — but Pressure is a value in the input image, copied at the start of the scan. Nothing refreshes it until the scan ends, and the scan cannot end until the loop does. Worse, Compressor := TRUE only changes memory; the output terminal is written after the program finishes. The compressor is never even started.
The same mistake turns up in more innocent clothes: waiting for a valve limit switch, waiting for a part to arrive, waiting for a drive to report "ready". Desktop programmers bring the habit with them, because on a PC the loop runs while the world changes around it. On a PLC the world is frozen for the length of your scan.
The correct pattern is the one you already know from chapter 12: remember where you are in a variable, check the condition once per scan, and let the scan be your loop. A timer turns "waiting" into "waiting, but not for ever":
PROGRAM Main
VAR
Start : BOOL; (* input: run request *)
ValveOpen : BOOL; (* input: valve open limit switch *)
Step : INT; (* 0 idle, 1 opening, 2 running, 9 fault *)
OpenTimer : TON;
ValveCmd : BOOL; (* output: suction valve solenoid *)
Pump : BOOL; (* output: pump contactor *)
ValveFault : BOOL; (* output: alarm *)
END_VAR
CASE Step OF
0: IF Start THEN Step := 1; END_IF;
1: IF NOT Start THEN Step := 0;
ELSIF ValveOpen THEN Step := 2;
ELSIF OpenTimer.Q THEN Step := 9;
END_IF;
2: IF NOT Start THEN Step := 0;
ELSIF NOT ValveOpen THEN Step := 9;
END_IF;
9: IF NOT Start THEN Step := 0; END_IF;
END_CASE;
OpenTimer(IN := Step = 1, PT := T#3s);
ValveCmd := Step = 1 OR Step = 2;
Pump := Step = 2;
ValveFault := Step = 9;
END_PROGRAMSwitch Start on: the valve is commanded and OpenTimer starts. Switch ValveOpen on within three seconds and the pump starts. Leave it off and after three seconds the program raises ValveFault instead of waiting in the dark. Every scan of this program is short and complete. Outputs are written, the HMI updates and the other ninety per cent of the program keeps running while the valve moves.
Bounded loops, as a rule
Put together, the chapter boils down to one rule that experienced ST programmers apply without thinking: every loop must have a bound you can state, independent of the data. In practice:
- Prefer FOR whenever there is a natural maximum, and use EXIT to stop early.
FOR i := 1 TO 8 … EXITcannot run more than eight times, whatever the data does. - When you do use WHILE or REPEAT, give the condition a hard limit —
AND i <= MAX_ROWS,OR Tries >= 4— and make sure something in the body moves towards it on every pass. - Clamp anything that controls a loop and comes from outside: an HMI row count, a recipe step number, a message length.
- Loop over data, never over time. If the plant has to change before you can continue, that is a state, a timer and a later scan — not a loop.
Some sites go further and ban WHILE and REPEAT altogether in their coding standard, allowing only FOR with constant bounds. It is a defensible rule; the loops in this chapter are all bounded, but a rule that makes the unbounded kind impossible to write is easier to check in a code review than one that relies on everyone being careful.
Next: data that looks like the equipment
You now have all three loops and know when each one fits: FOR for a known count, WHILE when the body may not be needed at all, REPEAT when it always is. EXIT stops a search, CONTINUE skips an element, and every loop has a bound that does not depend on the data behaving. Above all, a loop works on data that is already in memory. It never waits for the machine, because during your scan the machine cannot change.
So far the arrays have held one kind of value each — levels, codes, fault bits. A real pump has a run command, a speed, a fault and an hours counter, and keeping those in four parallel arrays is how mistakes creep in. The next chapter introduces STRUCT to keep them together, and ENUM to give states and modes proper names.