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 friends — 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.
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. Write the lower value first. 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.
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.
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, not an afterthought. 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 quietly keep running last batch's parameters.
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.
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.
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. One selector at the top, 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 THENinto 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 —
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.
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.
From choosing to remembering
CASE gives you a clean way to pick one behaviour from many: one selector, labels as values, lists and ranges, and an ELSE that turns an impossible value into a safe state and an alarm instead of a frozen machine. Use it for modes, recipes, codes and — very soon — the steps of a sequence in chapter 12. Keep ELSIF for priorities between different conditions.
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.