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. Translating one into the other means negating the condition, and a negated compound condition is where people slip: the opposite of a AND b is NOT a OR NOT b, never NOT a AND NOT b.
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.
Not every platform has every statement. This is what you can count on:
| Platform | WHILE, REPEAT | EXIT | CONTINUE | Jumps |
|---|---|---|---|---|
| CODESYS / TwinCAT 3 | yes | yes | yes | JMP to a label |
| Siemens TIA Portal SCL (S7-1200/1500) | yes | yes | yes | GOTO to a label |
| Rockwell Studio 5000 ST | yes | yes | no | none in ST |
| IEC 61131-3, 3rd edition | yes | yes | yes | none in ST |
The last column gets a sidebar of its own near the end of the chapter. The short version: you will read jumps in other people's SCL and CODESYS code, and you should not write them.
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.
Here is the 900 mm case as the controller sees it:
| Test | Seg | Seg < 5 | TabLevel[Seg + 1] < 900 | Result |
|---|---|---|---|---|
| 1 | 1 | TRUE | 400 < 900, TRUE | run body, Seg becomes 2 |
| 2 | 2 | TRUE | 800 < 900, TRUE | run body, Seg becomes 3 |
| 3 | 3 | TRUE | 1200 < 900, FALSE | leave the loop |
Then the interpolation: Frac = (900 − 800) / (1200 − 800) = 0.25, and Volume = 5.2 + 0.25 × (8.8 − 5.2) = 6.1 m³. Three tests, two passes of the body. With a level of 150 mm there is one test and no pass at all, which is exactly the case a WHILE exists for.
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, the comparisons stop meaning what you think: every comparison with NaN is FALSE, so this loop stops at once in segment 1, and the same search written with the opposite test, WHILE NOT (TabLevel[Seg + 1] >= L), walks all the way to the top of the table instead. Only the Seg < POINTS - 1 limit keeps that version inside the array. Either way the interpolation then calculates with NaN, and the LIMIT in front does not rescue it, because what LIMIT returns for a NaN is not something to rely on. Validate the measurement before it reaches the loop; chapter 19 does that properly.
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.
The Duty MOD 4 + 1 line is worth a second look, because it is the standard way to step round a ring numbered from 1. MOD gives 0 to 3; adding 1 gives 1 to 4. Pump 4 becomes 4 MOD 4 + 1 = 1. The zero-based version on a Logix array is (Duty + 1) MOD 4. Both break if Duty ever holds a value outside the ring, for example 0 after a cold start with no retained value, which is why the second challenge at the end of this chapter checks Duty before it uses it.
In my experience REPEAT is the rarer of the two by a long way. Most "at least once" jobs turn out to be "zero or more times" once you think about the empty case: the table with no rows, the buffer with nothing in it, the ring where the current pump is already the right one. When you reach for REPEAT, ask what should happen if the first pass has nothing to do, and check that the body copes with it.
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, an unknown article, so the carton is rejected. 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 it skips 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;. Many engineers prefer that 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.
There is a third way out, and it leaves more than the loop. RETURN ends the whole POU (the function, function block or program) at once. Inside a function that searches a table, RETURN straight after setting the result is clean and common. Inside a program it is a trap, because every line below it is skipped for that scan, including the lines that write outputs. Use it in small functions, and keep it out of anything that owns outputs.
Fewer passes over a sorted table
The labeller's search compares rows one after another, so a table of 8 rows costs up to 8 comparisons and a table of 2,000 rows costs up to 2,000. When a table is kept in order, a WHILE can do much better. A binary search looks at the middle row, decides which half the answer must be in, and throws the other half away. Every pass halves what is left.
PROGRAM Main
VAR CONSTANT
ROWS : INT := 16;
END_VAR
VAR
Scanned : DINT := 41477; (* input: article code from the scanner *)
Code16 : DINT := 42010; (* input: last row, as edited on the HMI *)
Codes : ARRAY[1..16] OF DINT := [40101, 40102, 40213, 40214, 40350, 40351, 40400, 40999,
41020, 41100, 41250, 41300, 41477, 41500, 41800, 42010];
TableOk : BOOL; (* codes strictly ascending *)
Lo : INT;
Hi : INT;
Mid : INT;
Row : INT; (* 0 = not found *)
Steps : INT; (* comparisons this scan *)
i : INT;
END_VAR
Codes[16] := Code16;
TableOk := TRUE;
FOR i := 2 TO ROWS DO
IF Codes[i] <= Codes[i - 1] THEN
TableOk := FALSE;
END_IF;
END_FOR;
Row := 0;
Steps := 0;
Lo := 1;
Hi := ROWS;
WHILE TableOk AND Lo <= Hi AND Row = 0 AND Steps < ROWS DO
Mid := Lo + (Hi - Lo) / 2;
Steps := Steps + 1;
IF Codes[Mid] = Scanned THEN
Row := Mid;
ELSIF Codes[Mid] < Scanned THEN
Lo := Mid + 1;
ELSE
Hi := Mid - 1;
END_IF;
END_WHILE;
END_PROGRAMThe code 41477 is in row 13, and the search finds it in four comparisons:
| Pass | Lo | Hi | Mid | Codes[Mid] | Decision |
|---|---|---|---|---|---|
| 1 | 1 | 16 | 8 | 40999 | smaller than 41477, so Lo := 9 |
| 2 | 9 | 16 | 12 | 41300 | smaller, so Lo := 13 |
| 3 | 13 | 16 | 14 | 41500 | larger, so Hi := 13 |
| 4 | 13 | 13 | 13 | 41477 | found, Row := 13 |
A linear search would have needed 13. Try 42010, the last row: five steps. Try 12345, which is not in the table: four steps, and Row stays 0 because Hi falls below Lo. Sixteen rows never need more than five comparisons, 1,000 rows never more than ten, and 30,000 rows never more than fifteen. That is the difference between a lookup you can afford every scan and one you cannot.
Three details in that code are there because they have failed on real projects.
The middle is computed as Lo + (Hi - Lo) / 2, not (Lo + Hi) / 2. Both give the same answer here. In a table of more than 16,383 rows indexed by an INT, Lo + Hi can pass 32,767 and wrap negative, and the search reads a row that does not exist. The same bug sat for years in widely used library code on desktop computers before anyone hit a table big enough to trigger it.
The loop has a guard, Steps < ROWS, that has nothing to do with the data. A correct binary search never reaches it. A binary search with a typo in one of the Lo := or Hi := lines can loop for ever, and the guard turns that into a wrong answer you can see instead of a watchdog trip.
And the search refuses to run on a table that is out of order. Type 40000 into Code16, the way an operator might append a new article at the end of the list instead of inserting it in its place. TableOk goes FALSE and every lookup reports 0. Without that check the search would still run, and it would fail only for some codes, depending on which halves it happened to throw away. That is the worst kind of fault to find on a night shift: most cartons are labelled correctly and a few are rejected for no visible reason. Checking order costs one FOR loop, and it only needs to run when the table changes; it runs every scan here to keep the example short.
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. Chapter 26 compares the watchdog settings per platform in detail.
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.
Do not mistake the watchdog for a safety function, either. It stops the controller when the program stops running; it says nothing about whether the machine is safe while it is running, and the "safe state" of a stopped CPU is whatever the output modules were configured to do. Anything that protects people, such as an emergency stop or a guard door, belongs in a safety relay or a safety PLC designed to IEC 62061 or ISO 13849, not in a loop guard or a watchdog setting.
A field story: the search that grew
A packaging machine looked up its format data by article code with a linear WHILE, much like the labeller above but without an EXIT. At commissioning the table had 40 rows and the lookup took a few hundred microseconds on a 10 ms task. Three years later the customer had loaded more than 2,000 articles. Nothing faulted. What the operators reported was that the film-tension loop "hunted" for a second or two every time a new carton type was selected, and that the occasional film break happened right after a changeover.
The task monitor told the story in one number: the maximum execution time of the task was 38 ms, against a 10 ms interval, and it happened only on the scan where the lookup ran. The tension PID, tuned for a 10 ms sample, got one sample nearly four times late on every changeover, with a jump in its output to match. The fix was two changes. The lookup got an EXIT, and more importantly it moved out of the fast task into a slower one that hands the result over with a "format ready" flag; the next section shows another way to do the same thing.
Spreading the work over several scans
Some jobs really are too big for one scan: summing a 10,000-sample log, checking a recipe database, rebuilding a statistics table. The answer is never a faster loop. Give the loop a fixed amount of work per scan, keep its position in a variable that survives between scans, and publish the result only when a whole pass is complete.
PROGRAM Main
VAR CONSTANT
SAMPLES : INT := 1000;
END_VAR
VAR
PerScan : INT := 100; (* input: samples processed per scan *)
Data : ARRAY[1..1000] OF INT;
Loaded : BOOL;
Chunk : INT;
Done : INT; (* samples processed this scan *)
Pos : INT := 1; (* next sample to process; survives between scans *)
Partial : DINT; (* running sum of the current pass *)
Total : DINT; (* last complete result, published once per pass *)
Passes : DINT; (* completed passes over the table *)
i : INT;
END_VAR
IF NOT Loaded THEN (* stand-in for a log filled by the process *)
FOR i := 1 TO SAMPLES DO
Data[i] := i MOD 10;
END_FOR;
Loaded := TRUE;
END_IF;
Chunk := LIMIT(1, PerScan, SAMPLES);
Done := 0;
WHILE Done < Chunk AND Pos <= SAMPLES DO
Partial := Partial + Data[Pos];
Pos := Pos + 1;
Done := Done + 1;
END_WHILE;
IF Pos > SAMPLES THEN (* pass complete: publish, then start again *)
Total := Partial;
Passes := Passes + 1;
Partial := 0;
Pos := 1;
END_IF;
END_PROGRAMWith 100 samples per scan, Pos walks through the table in steps of 100 and Passes goes up once every ten scans. Total reads 4500 throughout: the samples are the digits 0 to 9 repeated a hundred times, and each digit sums to 45 per hundred samples. Change PerScan to 1000 and the whole table is done every scan; change it to 7 and a pass takes 143 scans. Total stays at 4500 in every case, because nobody ever sees Partial, the half-finished sum.
That separation is what makes the pattern safe to use. A reader that picked up Partial mid-pass would see a number that is neither the old answer nor the new one. Keep the working variables private, publish the finished value in one assignment, and give consumers a counter like Passes (or a "result ready" flag) so they can tell a fresh result from an old one.
The cost is latency. With 100 samples per scan and a 10 ms task, a full pass takes 100 ms, so the total can be up to 100 ms out of date. For a shift report that does not matter at all; for a quality decision on the part in front of the camera it might. Choose the chunk size from the scan time you can spare, then check that the latency it implies is acceptable. On CODESYS, TwinCAT and the S7-1500 a slower cyclic task is the other way to do the same job, with the scheduler doing the slicing for you; chapter 26 covers what to watch for when data crosses between tasks.
Never loop waiting for the machine
Look again at that compressor loop. On a controller that works from an input image, which covers Siemens, CODESYS and TwinCAT, it cannot work at all. 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.
Two platform details make this less absolute than it sounds, and neither rescues the pattern. An S7-1500 lets you read an input straight from the module in the middle of a scan with peripheral access, such as %IW64:P, and write an output the same way. A ControlLogix goes further: its I/O tags are updated asynchronously at each module's RPI, independently of the program scan, so a loop there can in principle see a limit switch change. In both cases you would have built a busy-wait. Every other line of the program stands still while the valve moves: the other outputs, the HMI handshakes, the alarm timers. A 0.6 s valve stroke is already longer than the S7-1500's default 150 ms cycle limit, and longer than the 500 ms Logix task watchdog. The asynchronous update is also why Logix programmers copy their inputs into a buffer at the start of a routine (with CPS, so the copy cannot be interrupted) when they need a consistent snapshot.
A field story: it only faults on a cold start
A machine builder's start-up routine contained WHILE NOT DriveReady DO END_WHILE;, meant to hold the program until the servo drives had booted. On the test bench the drive-ready signals were simulated and already TRUE, so the loop never ran. On site the controller went into an exception every morning, at the first power-up of the day, and ran perfectly after someone restarted it. The pattern of the symptom pointed straight at the cause: after a warm restart the drives were already up, and after a cold start of the whole panel they needed a second and a half to report ready. The controller's own boot was faster than that, so the loop waited on an input that could not change inside the scan and the watchdog stopped it. The rewrite was a start-up state with a timeout, exactly the pattern below.
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.
A guard that reports itself
The loops so far end because of their own logic, with a guard behind it in case the logic is wrong. On data you do not control, make the guard tell you when it fired. A loop that stops silently at its limit hides the problem behind an answer that looks normal.
A common case is a chain stored in an array: each row says which row comes next. Conveyor routing tables, linked alarm groups and recipe step tables are often built this way, with 0 meaning "end of chain". Following the chain is a WHILE. If somebody edits the table so that a row points back to an earlier one, the chain becomes a circle, and an unguarded loop goes round it until the watchdog trips. The last challenge in this chapter asks you to write that guard and report the fault.
When a guard fires in a real program, do three things: stop using the result, raise an alarm that names the table and the row where the loop was when it stopped, and keep the machine in a defined state. A guard that sets the answer to "not found" and carries on is better than a hang. A guard that also tells maintenance which row of which table is wrong saves them an hour.
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, never a loop.
- Know the worst case in scan time. Multiply the bound by the cost of one pass, and if the answer is more than a small share of the task, slice the work or move it to a slower task.
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. The binary search above can be written as a FOR with an EXIT just as well: FOR Steps := 1 TO ROWS DO … EXIT … END_FOR. Try it once and see which version you find easier to review.
Sidebar: jumps, labels and why ST left them out
Instruction List and ladder logic have always had jumps, and some ST dialects kept them. TIA Portal SCL has GOTO to a label, CODESYS ST has JMP, and both are used in older converted code to build loops by hand: a label at the top, a test, a jump back. IEC 61131-3 gives Structured Text no jump statement at all, and Studio 5000 ST follows the standard there. The reason is the same one that makes jumps unpopular in every structured language: a loop made from a jump has no visible beginning and end, so you cannot see its bound by reading it. When you meet one in a migrated project, rewrite it as a FOR or WHILE with an explicit limit, and test the old and new versions side by side with the same inputs before you delete the original.
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. You have seen a binary search turn 2,000 comparisons into eleven, and a long job split across scans with its result published only when it is complete. 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.