One question, many answers
An ELSIF chain asks a series of different questions and takes the first one that says yes. Very often, though, the program is asking one question with many possible answers. Which position is the mode selector in? Which recipe did the operator load? Which lane should this carton go to? Writing that as IF Recipe = 1 THEN … ELSIF Recipe = 2 THEN … ELSIF Recipe = 3 THEN … works, but it repeats the selector on every line and hides the real structure. CASE says it directly:
CASE Selector OF
1:
(* statements for value 1 *)
2:
(* statements for value 2 *)
ELSE
(* statements for every other value *)
END_CASE;
The selector is an expression of an integer type (INT, DINT, SINT, UINT and the rest of the family) or an enumerated type, which arrives in chapter 15. It is evaluated once. The controller then runs the statements under the label that matches, and only those. Each label is followed by a colon, a branch can hold as many statements as you need, and the whole construct ends with END_CASE;.
A barcode scanner at the end of a sorter reads a product code. Codes 1 and 2 go to their own lanes; anything else goes to the reject lane.
PROGRAM Main
VAR
ProductCode : INT := 1; (* input: code read by the barcode scanner *)
Lane : INT; (* output: diverter lane number *)
END_VAR
CASE ProductCode OF
1:
Lane := 1; (* 330 ml cans *)
2:
Lane := 2; (* 500 ml cans *)
ELSE
Lane := 9; (* reject lane *)
END_CASE;
END_PROGRAMType 2, then 7, then 0 into ProductCode. Every scan, exactly one branch writes Lane. Like an IF, a CASE runs on every scan; it does not wait for the selector to change.
There is one thing CASE in Structured Text does not do, and C, C# and JavaScript programmers trip over it.
What the controller does with a CASE
The rules are short. The selector expression is computed once, at the top. Its value is compared with the labels, and the first label that contains it selects the branch. That branch runs to its end, and execution continues after END_CASE. If no label matches, the ELSE branch runs; if there is no ELSE, nothing runs at all.
How the compiler gets from the value to the branch is its own business, and the standard leaves it alone. For a handful of scattered labels it will usually generate the same compare-and-jump code as an ELSIF chain. For a dense run of labels (0, 1, 2 … 30) a compiler can build a jump table and go straight to the right branch with one indexed jump. You cannot see which one you got, and on a modern controller the difference is a few nanoseconds per scan. Do not choose between CASE and ELSIF for speed. Choose for what the code says.
Two consequences follow from "computed once". The first is that a selector with a side effect has it once per scan, not once per label: CASE ReadNextCode() OF calls the function one time. The second is that a branch which changes the selector does not cause another branch to run in the same scan. The CASE has already chosen. That property is what makes a CASE-based state machine move exactly one state per scan, and chapter 12 builds on it with a prediction of its own.
What each vendor accepts as a selector
The selector rules are one of the places where the platforms really do differ:
| Environment | Selector and label types | Worth knowing |
|---|---|---|
| IEC 61131-3, 2nd edition | integer types and enumerations | labels are integer literals, ranges or enumerated values |
| CODESYS, TwinCAT 3 | integer types and enumerations | named constants are accepted as labels |
| TIA Portal (SCL) | integer types | constants are accepted as labels |
| Studio 5000 (ST) | SINT, INT, DINT and REAL | label values must match the selector's type; Rockwell's documentation says only the first matching selector runs |
Studio 5000's acceptance of REAL stands out. Rockwell's own help recommends ranges rather than single values when you do it, because a REAL is far more likely to land inside a range than to equal one exact value, which is chapter 6 in a sentence. Code written that way does not port to the other three, and on Rockwell it still leaves the question of which side owns each boundary. I would convert to an integer band first, as shown later in this chapter, and keep the CASE on integers everywhere.
Lists and ranges
A label does not have to be a single number. Separate values with commas to give several values the same branch, and write low..high for an inclusive range. You can mix both on one label.
CASE ErrorCode OF
0: Status := 'OK';
1, 3, 5: Status := 'Warning';
10..19: Status := 'Drive fault';
20..29, 99: Status := 'Comms fault';
ELSE
Status := 'Unknown';
END_CASE;
Ranges include both ends: 10..19 matches 10, 19 and everything between. Ranges can be negative (-10..-1 is fine). Write the lower value first. A range written the other way round, 19..10, is an error on some compilers and on others simply matches nothing; the engine on this site treats it as matching nothing, so the value falls through to ELSE without a word of warning.
Labels must not overlap. If 15 appears in two labels, most compilers refuse to build, and that is a favour: overlap almost always means two people had different ideas about what code 15 is. Not every environment refuses, though. Studio 5000 documents that only the first matching selector executes, and the engine on these pages also takes the first match. Where the tool does not stop you, the review has to. Reading the labels in order and checking that each range starts above the end of the previous one takes a minute.
A carton erector handles several box sizes. The product master assigns size codes in blocks: 100–199 are small boxes, 200–299 large boxes, and codes 900 and 901 are test runs that use the small format but leave the glue off.
PROGRAM Main
VAR
SizeCode : INT := 120; (* input: size code from the order *)
FormatMm : INT; (* output: magazine width in mm *)
GlueOn : BOOL; (* output: hot-melt glue enabled *)
CodeValid : BOOL; (* output: the code was recognised *)
END_VAR
CASE SizeCode OF
100..199:
FormatMm := 300;
GlueOn := TRUE;
CodeValid := TRUE;
200..299:
FormatMm := 450;
GlueOn := TRUE;
CodeValid := TRUE;
900, 901:
FormatMm := 300; (* test run: small format, no glue *)
GlueOn := FALSE;
CodeValid := TRUE;
ELSE
FormatMm := 0;
GlueOn := FALSE;
CodeValid := FALSE;
END_CASE;
END_PROGRAMTry 199, 200, 901 and 350. Look at the shape of the code: every branch writes every output. That is the same discipline as an IF/ELSE from chapter 5, and for the same reason. A branch that forgets GlueOn leaves it at whatever the previous size code set, and the first box of the next order comes out without glue. The branch table from that chapter works here unchanged, with the labels as rows:
| Label | FormatMm | GlueOn | CodeValid |
|---|---|---|---|
| 100..199 | 300 | TRUE | TRUE |
| 200..299 | 450 | TRUE | TRUE |
| 900, 901 | 300 | FALSE | TRUE |
| ELSE | 0 | FALSE | FALSE |
No empty cells, so no value can survive from one order into the next.
ELSE is the safe default
ELSE catches every value no label mentions. In an IF, ELSE is about the condition being false. In a CASE, ELSE is about the selector being something you did not plan for, and on a running plant that happens more often than you would think.
Where do unexpected selector values come from? An HMI numeric entry without limits. A recipe number read from a database or an MES over OPC UA. A step number that a colleague changed in one place and not another. A retained value from an older software version after a download. A three-position selector switch wired as two digital inputs, where a jammed contact closes both. Each of these will one day put a value into your selector that has no label.
What belongs in ELSE is a decision, made when you write the CASE. The usual answer has two parts: put the outputs in their safe state, and raise a flag so someone finds out why. A recipe selector that receives recipe 47 when only twelve exist should load no parameters, block the start and tell the operator. It should not carry on with last batch's parameters as if nothing had happened.
Recipe selection shows the pattern in full. A mixer runs three products; the operator picks one on the HMI.
PROGRAM Main
VAR
RecipeNo : INT := 1; (* input: recipe chosen on the HMI *)
MixSpeed : REAL; (* output: agitator speed in rpm *)
MixTimeS : INT; (* output: mixing time in seconds *)
WaterLitres : REAL; (* output: water dose in litres *)
RecipeOk : BOOL; (* output: permission to start a batch *)
END_VAR
CASE RecipeNo OF
1: (* standard white base *)
MixSpeed := 120.0;
MixTimeS := 300;
WaterLitres := 180.0;
RecipeOk := TRUE;
2: (* high-viscosity base *)
MixSpeed := 80.0;
MixTimeS := 480;
WaterLitres := 140.0;
RecipeOk := TRUE;
3: (* rinse cycle *)
MixSpeed := 200.0;
MixTimeS := 60;
WaterLitres := 250.0;
RecipeOk := TRUE;
ELSE
MixSpeed := 0.0;
MixTimeS := 0;
WaterLitres := 0.0;
RecipeOk := FALSE; (* blocks the start, raises "invalid recipe" on the HMI *)
END_CASE;
END_PROGRAMType 4 or 0 into RecipeNo. Everything drops to zero and RecipeOk goes FALSE. The batch sequence elsewhere in the program only starts when RecipeOk is TRUE, so a bad number cannot start a batch with leftover parameters. In a real project those parameters would come from a table rather than literals (an array of structures, which is where chapter 15 is heading), but the CASE that validates the number stays the same.
An ELSE that tells you what arrived
A flag that says "invalid recipe" sends someone looking. A flag that says "invalid recipe 47" tells them where to look. Copy the bad value into its own variable in the ELSE branch and show it in the alarm text:
ELSE
MixSpeed := 0.0;
MixTimeS := 0;
WaterLitres := 0.0;
RecipeOk := FALSE;
BadRecipeValue := RecipeNo; (* shown in the alarm: "Recipe 47 does not exist" *)
END_CASE;
A mixer controller is replaced after a failure, and the new CPU is loaded with the project but not with the retained data, so the current recipe number comes up as 0. A program with a good ELSE refuses to start and shows "recipe 0 does not exist"; the operator selects the recipe again and production carries on. I have also seen the other kind of ELSE, written as RecipeNo := 1; "so the machine always has a valid recipe". On a line that changes products, that is a batch of the wrong product made without anyone being asked. The ELSE should stop and ask, not guess.
A mode selector that stays readable
Almost every machine has a mode selector: a key switch or an HMI choice between Off, Manual and Automatic. The modes are exclusive, and each mode decides how all the outputs behave. That is exactly what CASE is shaped for.
PROGRAM Main
VAR
Mode : INT := 0; (* input: 0 = Off, 1 = Manual, 2 = Auto *)
JogButton : BOOL; (* input: jog push-button, held *)
AutoDemand : BOOL; (* input: downstream machine is ready for product *)
Motor : BOOL; (* output: conveyor motor contactor *)
AutoLamp : BOOL; (* output: white "Auto" lamp on the desk *)
ModeFault : BOOL; (* output: selector reports an impossible value *)
END_VAR
CASE Mode OF
0: (* Off *)
Motor := FALSE;
AutoLamp := FALSE;
ModeFault := FALSE;
1: (* Manual: run only while the jog button is held *)
Motor := JogButton;
AutoLamp := FALSE;
ModeFault := FALSE;
2: (* Auto: run when the downstream machine asks *)
Motor := AutoDemand;
AutoLamp := TRUE;
ModeFault := FALSE;
ELSE
Motor := FALSE;
AutoLamp := FALSE;
ModeFault := TRUE;
END_CASE;
END_PROGRAMSet Mode to 1 and use JogButton; set it to 2 and use AutoDemand. Switch on AutoDemand, then go back to Mode 1: the motor stops, because in manual it only listens to the jog button. Each mode is a self-contained block you can read without looking at the others. Try Mode 3 and the ELSE takes over.
The bare numbers 0, 1 and 2 are the weak spot. Six months from now, CASE Mode OF 2: means nothing to the engineer on a night call. The comments help, but the better fix is to give the values names. In CODESYS, TwinCAT 3 and TIA Portal you can declare integer constants and use them as labels:
VAR CONSTANT
MODE_OFF : INT := 0;
MODE_MANUAL : INT := 1;
MODE_AUTO : INT := 2;
END_VAR
CASE Mode OF
MODE_OFF:
Motor := FALSE;
MODE_MANUAL:
Motor := JogButton;
MODE_AUTO:
Motor := AutoDemand;
ELSE
Motor := FALSE;
ModeFault := TRUE;
END_CASE;
Strictly, the 2nd edition of IEC 61131-3 only allows integer literals and enumerated values as CASE labels; named constants came with later editions and vendor extensions. Every mainstream environment accepts them today, and so does the engine on these pages; the live examples above keep the literals only so you can see the numbers. When you write for a real controller, use the constants. And when you reach chapter 15, use an ENUM instead: TYPE E_Mode : (Off, Manual, Auto); END_TYPE. An enumerated selector cannot hold a value without a name, the online watch shows Auto instead of 2, and the compiler checks that you did not compare a mode with a recipe number.
Where the mode number comes from
On an HMI the mode is already a number. On a desk it is usually a three-position key switch, Hand-Off-Auto, with two contacts: one closes in Hand, the other in Auto, and in Off neither is closed. The program has to turn two BOOLs into one mode, and there are four combinations for three positions.
| Hand contact | Auto contact | Meaning |
|---|---|---|
| open | open | Off |
| closed | open | Hand (Manual) |
| open | closed | Auto |
| closed | closed | impossible: a welded contact, a short between the two wires, or a broken switch |
The fourth row is the reason to decode into a number and CASE on it, instead of writing IF HandContact THEN … ELSIF AutoContact THEN …. The ELSIF version gives the impossible combination to whichever contact is tested first, and the machine runs in that mode with a broken switch. Decoding into a number gives the impossible combination a value of its own, and the CASE's ELSE turns it into a mode fault:
Mode := BOOL_TO_INT(HandContact) + 2 * BOOL_TO_INT(AutoContact);
(* 0 = Off, 1 = Manual, 2 = Auto, 3 = both contacts closed *)
A switch caught between positions can also show both contacts open for a moment, which reads as Off. That is the safe reading, so it needs no special handling, but it is why the machine should never start on a mode change alone. The challenge at the end of this chapter has you build the whole thing.
Banding a REAL with CASE
A CASE on a temperature is tempting: "below 20 °C do this, 20 to 50 °C do that, above 50 °C reject". Standard ST will not take a REAL selector, and as you saw above, the one environment that does advises ranges. The portable approach is to turn the REAL into an integer band first, deliberately, and CASE on the band.
The conversion you choose decides where the band edges fall. IEC 61131-3 conversions from REAL to an integer type round to the nearest whole number. TRUNC cuts the fraction off instead. For bands 10 °C wide, that is the difference between edges at 20 °C and edges at 15 °C:
PROGRAM Main
VAR
Temp : REAL := 26.0; (* input: product temperature, deg C *)
BandTrunc : DINT; (* TRUNC: 20.0 .. 29.99 -> 2 *)
BandRound : INT; (* rounding: 15.0 .. 24.99 -> 2 *)
Action : INT; (* 1 = accept, 2 = send to chiller, 9 = reject *)
END_VAR
BandTrunc := TRUNC(Temp / 10.0);
BandRound := REAL_TO_INT(Temp / 10.0);
CASE BandTrunc OF
0, 1: Action := 1; (* 0 to 19.99 deg C: accept *)
2..4: Action := 2; (* 20 to 49.99 deg C: chill first *)
ELSE
Action := 9; (* 50 deg C and above: reject *)
END_CASE;
END_PROGRAMType 19.9, 20, 24.9 and 25 into Temp and watch the two band columns. BandTrunc changes at 20 and 30; BandRound changes at 15 and 25. Both are "temperature in tens", and they put the same product in different bands for half of every band. Write the conversion that matches the specification, and write the band edges in a comment as above, so that a reviewer can check them without doing division in their head.
Banding is the right tool when each band does something genuinely different and the bands are regular. When there are only two or three thresholds, or the thresholds are irregular (53 g, 63 g, 73 g), an ELSIF chain of comparisons says it more plainly, with each boundary written exactly once and its operator chosen as chapter 6 taught.
Why CASE beats a long ELSIF chain
For exclusive values of one selector, CASE is better than ELSIF in four concrete ways.
The structure is visible. There is one selector at the top and one value per label. An ELSIF chain makes the reader check every line to confirm that it really tests the same variable, and sooner or later someone slips ELSIF Recipe = 3 OR Override THEN into the middle.
Gaps and overlaps are easy to see. A list of labels reads like a table: 1, 2, 3, 10..19, ELSE. In a chain of comparisons, a missing >= or a duplicate value hides in plain sight. Duplicate labels are usually a compile error; duplicate ELSIF tests never are.
The selector is evaluated once. If it is an expression, such as CASE Recipe MOD 100 OF, it is computed once per scan instead of once per comparison.
Order stops mattering. Because labels cannot overlap, you can sort them by value without changing behaviour. In an ELSIF chain, order is priority, and reordering can change what the machine does, as the conveyor in chapter 5 showed.
Converting a chain during a review
Here is a chain of the kind that turns up in older projects, from a palletiser that picks a layer pattern from the product code:
IF ProductCode = 1 THEN
Pattern := 5;
ELSIF ProductCode = 2 OR ProductCode = 3 THEN
Pattern := 8;
ELSIF ProductCode >= 10 AND ProductCode < 20 THEN
Pattern := 6;
ELSIF ProductCode = 4 OR NightShift THEN
Pattern := 8;
ELSIF ProductCode >= 19 AND ProductCode <= 25 THEN
Pattern := 9;
END_IF;
Converting it to a CASE is mechanical: each equality becomes a value, each pair of bounds becomes a range, and the ELSE gets a decision. Two lines refuse to convert, and they are the two findings of the review. The fourth branch tests NightShift, which is not the selector at all, so a product code of 30 on night shift gets pattern 8 for no stated reason. And the last branch's range, 19 to 25, overlaps the 10 to 19 range above it, so code 19 has two owners; in the chain the earlier one wins, and pattern 9 is never used for code 19. Nothing assigns Pattern for an unknown code either, so the last pattern carries over.
PatternFault := FALSE; (* default first; only the ELSE sets it *)
CASE ProductCode OF
1: Pattern := 5;
2, 3, 4: Pattern := 8; (* 4 confirmed with the line manager *)
10..19: Pattern := 6;
20..25: Pattern := 9; (* boundary moved: 19 belongs to the 10..19 block *)
ELSE
Pattern := 0; (* no pattern: the palletiser will not start *)
PatternFault := TRUE;
END_CASE;
The CASE could not express either finding, so the conversion forced someone to go and ask what was meant. The night-shift rule, if it is real, becomes a separate, named condition outside the CASE where everyone can see it.
ELSIF is still the right tool when the conditions are not values of one selector: a fault bit, then a jog button, then an automatic request. It is also right for REAL thresholds. CASE selectors are integers or enumerations; you cannot write CASE Temperature OF 20.0..80.0: in standard ST, and you should not want to, because the boundaries of a REAL range deserve the care of chapter 6. A common pattern combines the two: a CASE on the machine mode, with short IF or ELSIF logic inside each branch.
When the selector holds a value nobody expected
Leave out the ELSE and a CASE with an unknown selector does nothing at all. No branch runs, no statement executes, and every output written inside the CASE keeps its last value. It is the missing-ELSE latch from chapter 5, with a bigger blast radius: instead of one output, an entire mode's worth of outputs freezes.
The program below runs the same selector into two CASE statements, one without ELSE and one with it.
PROGRAM Main
VAR
Mode : INT := 2; (* input: 0 = Off, 1 = Manual, 2 = Auto *)
PumpNoElse : BOOL;
PumpWithElse : BOOL;
ModeAlarm : BOOL;
END_VAR
CASE Mode OF
0: PumpNoElse := FALSE;
1: PumpNoElse := FALSE; (* manual: pump off unless jogged *)
2: PumpNoElse := TRUE; (* auto: pump running *)
END_CASE;
CASE Mode OF
0: PumpWithElse := FALSE;
ModeAlarm := FALSE;
1: PumpWithElse := FALSE;
ModeAlarm := FALSE;
2: PumpWithElse := TRUE;
ModeAlarm := FALSE;
ELSE
PumpWithElse := FALSE;
ModeAlarm := TRUE;
END_CASE;
END_PROGRAMBoth pumps run in Mode 2. Now type 7 into Mode, as a faulty HMI write or a corrupted value would. PumpWithElse stops and ModeAlarm comes on. PumpNoElse keeps running, in a mode that does not exist, and nothing anywhere tells you.
An ELSE is not a safety function either. A mode fault in the standard program stops the conveyor through the normal control path. Guard doors, emergency stops and the other protective functions on that conveyor belong to the safety relay or safety PLC, designed to ISO 13849-1, and they must work whatever value Mode holds.
Keeping a CASE reviewable
A CASE that starts as three tidy branches tends to grow. Some habits keep it readable after a few years of changes.
Keep branches short. If a branch passes about twenty lines, the CASE stops reading like a table, and nobody can see the labels any more. Move the body into a function or a function block (chapter 17 and chapter 18) and leave one call in the branch.
Nest with care. A CASE on the machine mode containing a CASE on the sequence step is a common and reasonable structure: Auto has steps, Manual does not. Two levels is the limit I would accept. If the inner CASE appears in several modes, it is a state machine on its own and wants its own routine.
Keep data out of the CASE when it is only data. The recipe example above writes four constants per branch. With twelve recipes that is forty-eight lines of numbers inside control logic, and every recipe change is a program change. When the branches differ only in the values they assign, the values belong in an array indexed by the recipe number, and the CASE shrinks to the validation. Chapter 30 covers recipes properly.
Give the labels names, as in the mode example, and sort them by value. A reviewer reading 0, 10, 20, 30, 90 can see at once that there is nothing between 30 and 90. A reviewer reading MODE_AUTO, MODE_OFF, MODE_MANUAL cannot.
Draw the branch table. It is as quick here as it was in chapter 5, and it finds the same bug: the output one label forgot.
From choosing to remembering
Use CASE for modes, recipes, codes and, very soon, the steps of a sequence in chapter 12. Keep ELSIF for priorities between different conditions and for thresholds on REALs. Whichever you use, the ELSE is where you decide what the machine does with a value nobody planned for. Make that decision when you write the CASE, with the process engineer if necessary, rather than at commissioning.
So far every decision has been made from what the inputs are now. Real machines also care about what just changed: the instant a button was pressed, the moment a part left a sensor. And some things must be remembered after the input that caused them has gone, like a motor that keeps running after the start button is released or an alarm that waits for acknowledgement. The next chapter is about edges, latches and the memory that makes them work.