When writing PLC programs in TwinCAT 3, many developers default to using IF
statements for branching logic. However, TwinCAT 3 offers a variety of other branching techniques that can improve readability, efficiency, and control flow. In this post, we will explore different ways to structure your program beyond simple IF
conditions. Note that we will not cover AND_THEN
and OR_ELSE
here—check out the part on Full Evaluation in the blog article Understanding Expression Evaluation in TwinCAT for more details on those operators.
S= Assignment
If the operand of the set assignment switches to TRUE
, the assignment causes the variable to the left of the operator to be assigned a TRUE
. The variable is set.
Syntax
<variable> S=
[<variable2> S=
] [<variableN> S=
] <expression>;
The <variable> must be one of the boolean types BIT
or BOOL
. The <expression> must return a BOOL
.
The parts in square brackets are optional.
Example
(* simple assignment *)
foo S= bar;
(* equal expression with an if statement*)
IF (bar) THEN
foo := TRUE;
END_IF
(* Multi assingment *)
foo S= baz S= bar;
(* equal expression with an if statement*)
IF (bar) THEN
foo := TRUE;
baz := TRUE;
END_IF
(* assigment with complex expression *)
foo S= bar XOR baz;
(* equal expression with an if statement*)
IF (bar XOR baz) THEN
foo := TRUE;
END_IF
R= Assignment
If the operand of the reset assignment switches to TRUE
, the assignment causes the variable to the left of the operator to be assigned a FALSE
. The variable is unset.
Syntax
<variable> R=
[<variable2> R=
] [<variableN> R=
] <expression>;
The <variable> must be one of the boolean types BIT
or BOOL
. The <expression> must return a BOOL
.
Example
(* simple assignment *)
foo R= bar;
(* equal expression with an if statement*)
IF (bar) THEN
foo := FALSE;
END_IF
(* Multi assingment *)
foo R= baz R= bar;
(* equal expression with an if statement*)
IF (bar) THEN
foo := FALSE;
baz := FALSE;
END_IF
(* assigment with complex expression *)
foo R= bar XOR baz;
(* equal expression with an if statement*)
IF (bar XOR baz) THEN
foo := FALSE;
END_IF
S= and R= Assignment Combined
Example
foo S= bar R= baz;
(* equal expression with an if statement*)
IF (baz) THEN
foo := TRUE;
bar := FALSE;
END_IF
foo R= bar S= baz;
(* equal expression with an if statement*)
IF (baz) THEN
foo := FALSE;
bar := TRUE;
END_IF
(* assigment with complex expression *)
foo S= bar R= baz OR NOT fooBar
(* equal expression with an if statement*)
IF baz OR NOT fooBar) THEN
foo := TRUE;
bar := FALSE;
END_IF
IF Statement
The IF
statement is used to test a condition and to execute the subsequent instructions if the condition is met. A condition is encoded as a expression that returns a boolean value. If the expression returns TRUE
, the condition is met and the associated statements after THEN
are executed. If the expression returns FALSE
, the following optional conditions marked ELSIF
are evaluated. If an ELSIF
condition returns TRUE
, the instructions after the associated THEN
are executed. If all conditions are FALSE
, the statements after the optional ELSE
are executed.
So at most one branch of the IF
statement is executed. The ELSIF
branches and the ELSE
branch are optional. The multiplicity of ELSIF
is 0..n and the multiplicity of ELSE
is 0..1. The scope of the IF
is closed by an END_IF
.
Syntax
IF <condition> THEN
<statements>
[ELSIF <condition> THEN
<statements>]
[ELSE
<statements>]
END_IF
The parts in square brackets are optional.
Example
IF (THIS^.foo AND THIS^.bar) THEN
THIS^.doFooBar();
ELSIF (THIS^.foo) THEN
THIS^.doFoo();
ELSIF (THIS^.bar) THEN
THIS^.doBar();
ELSE
THIS^.doBaz();
END_IF
CASE Statement
The CASE
instruction is used to group multiple conditional instructions with the same conditional variable in a construct. The conditional variable must be an integer type: BYTE
, SINT
, USINT
, WORD
, INT
, UINT
, DWORD
, DINT
, UDINT
, LWORD
, LINT
, ULINT
, __XWORD
, __XINT
, __UXINT
, or PVOID
. It works with PVOID
but not with a non-dereferenced POITER TO
.
The conditions are values. These values must be constant integer values in the range from -2^63 to 2^64-1 that can be literals, enumerations, as CONSTANT
declared variables or as constant defined types. Each value has the multiplicity 0..1. It is possible to use ranges as value, e.g. 2 .. 4:
, or more than one value per case, e.g. 1, 2, 3, 5, 8, 13:
. Each value can exist only once, no matter if it is hidden in a range.
The value expression is finished with a colon. It is required that every value case has at least one statement. Here, a semicolon counts as a statement, e.g. 1 .. 3:;
. A value has to be compatible with the data type of the conditional variable, e.g. if a (8-Bit singed integer) is used and there is a literal then the literal will be interpreted as -1. Be careful if you change the type of conditional variable. For the default case is the keyword ELSE
, the instruction(s) in the default case will be executed if no other value matches, it does not matter where the ELSE
keyword is. ELSE
has a multiplicity of 0..1, too. Finally, it is possible to create an empty CASE
statement because every value is optional.
Syntax
CASE <varibale> OF
[
[<value1>:
[<instructionA>];
[<instructionN>;]
]
[<value2>:
[<instructionB>];
[<instructionN>;]
]
[<value3, value4, value5>:
[<instructionC>];
[<instructionN>;]
]
[<value6 .. value10>:
[<instructionD>];
[<instructionN>;]
]
[<valueN>:
[<instructionE>];
[<instructionN>;]
]
[ELSE
[<instructionF>];
[<instructionN>;]
]
]
END_CASE
The parts in square brackets are optional.
Processing scheme of a CASE instruction:
- If the variable <variable> has the value <value1>, <instructionA> to <instructionN> is executed.
- If you want to execute the same instruction for a value range of the <variable>, write the <startValue> and the <endValue> separated by
..
. - If you want to execute the same instruction for multiple values (but no range) of the variable, write these values separated by commas.
- If the variable <variable> has none of the specified values, the else <instructionF> is executed.
Example
CASE foo OF
-1:; (* do nothing *)
0:
initDos(force := FALSE);
foo := waitForSomthingToDo();
1:
foo := do1() AND_THEN do2();
FOO_BAR:
foo := doFooBar();
Foocolate.BAZ:
foo := doFooBaz();
ELSE
initDos(force := TRUE);
foo := 0;
END_CASE
(* just an empty *)
CASE bar OF
END_CASE
JMP Statement
The JMP
instruction is used to perform an (un)conditional jump to a line, which is marked by a label. It does not matter where the label is: above or below the JMP
instruction, but it must be in the same scope (Editor Window e.g. METHOD
, PROGRAM
, FUNCTION
, etc.) as the JMP
instruction. The label is a freely selectable, unique identifier, which you place at the beginning of a line.
Identifier rules:
- It must be no keyword.
- It must start with an underscore or a letter a to z or a letter A to Z.
- It can contain the digits 0 to 9, but it cannot start with a digit.
- It should not use double or more underscores in row, because double underscores are for internal use by Beckhoff. It will compile, but it is a bad practice.
As RegEx, it would look like this /(?<=\\\\s)[a-zA-Z_]([a-zA-Z0-9]*|(?<![_])[_]?)*/
.
The JMP
statement is per default unconditional, but it is possible to add an expression in round braces ()
between JMP
and label. The condition must return a boolean value. If the condition is TRUE
, then the jump to the label is performed.
Syntax
JMP[(<condition>)] <label>;
<label>: [<instructions>]
The parts in square brackets are optional.
Example
bar1:
(*conditional jump*)
JMP (foo) foo1;
(*unconditional jump*)
IF (bar) then
JMP bar1;
END_IF
foo1:
RETURN Statement
The RETURN
instruction is used to exit a scope (Editor Window, e.g. METHOD
, PROGRAM
, FUNCTION
, etc.). You can make this dependent on a condition. The RETURN
statement is per default unconditional, but it is possible to add an expression in round braces ()
after the RETRUN
keyword. The condition must return a boolean value. If the condition is TRUE
, then the RETURN
performs a jump to the end of the current scope.
Syntax
RETURN[(<condition>)];
The parts in square brackets are optional.
Example
(* conditional RETRUN *)
RETURN(foo OR bar);
(*equal example with an unconditional RETURN*)
IF (foo OR bar) THEN
RETURN;
END_IF
SEL Operator
The SEL
operator functions similarly to a ternary conditional operator. It evaluates a boolean expression and returns the result of one of two expressions, depending on whether the boolean expression evaluates to TRUE
or FALSE
. Although it looks like a function, it behaves differently. In TwinCAT, if the boolean expression evaluates to TRUE
, the expression preceding in0 is not computed, and if it evaluates to FALSE
, the expression preceding in1
is not computed. The precedence is still like a function call. However, this behavior is specific to ST/ExST; in graphical languages like ladder logic, both in0
and in1
are computed regardless of the boolean result.
Ensure that all three positions use expressions of the same type, especially when dealing with user-defined data types. The compiler checks for type consistency and will issue compilation errors if the types do not match. Specifically, assigning instances of a function block to interface variables is not supported. Additionally, REF=
assignments are not allowed, and only S=
, R=
, and :=
assignments are supported.
Syntax
<target> [<Assignment-Operator>] SEL(<Boolean-Expression>,<in0>, <in1>);
The parts in square brackets are optional.
Example
(*
if the pointer to injectedFoo valid then will set the interface fooness to the returned interface injectedFoo^.fooness
otherwise it will set it to the return of the defaultFoo.fooness
*)
fooness := SEL(injectedFoo <> 0, defaultFoo.fooness, injectedFoo^.fooness);
(*equal example with if-statment*)
IF (injectedFoo <> 0) THEN
fooness := injectedFoo^.fooness;
ELSE
fooness := defaultFoo.fooness;
END_IF
(* Here are two exmaples with the S= and R= assignments *)
foo S= SEL(bar, baz, TRUE);
bar R= SEL(foo, FALSE, TRUE);
(*equal examples with if-statments*)
IF (bar) THEN
foo := TRUE;
ELSIF (baz) THEN
foo := TRUE;
END_IF
IF (foo) THEN
bar := FALSE;
END_IF
(* Here is an example without assignment *)
IF (bar XOR SEL(foo, baz, FALSE)) THEN
;
END_IF
(*equal example with if-statment*)
IF (bar XOR (NOT foo AND baz)) THEN
;
END_IF
MUX Operator
The MUX
operator works similarly to a CASE
statement. It evaluates an integer expression and selects the nth statement based on the integer value from in0
to inN
. While it appears to function like a typical operation, it behaves differently by computing only the selected statement. If no value matches, it defaults to selecting the last statement.
Ensure that all positions (from in0
, to inN
and the target) use expressions of the same type, particularly when dealing with user-defined data types. The compiler enforces type consistency and will issue compilation errors if types do not match. Notably, assigning instances of a function block to interface variables is not supported. Furthermore, REF=
assignments are not allowed; only S=
, R=
, and :=
assignments are supported.
Permitted data types for the integer expression include:
ANY_BIT
:BYTE
,WORD
,DWORD
,LWORD
, and__XWORD
ANY_INT
:SINT
,USINT
,INT
,UINT
,DINT
,LINT
,ULINT
,UDINT
,__XINT
, and__UXINT
Syntax
<target> [<Assignment-Operator>] MUX(<integer-Expression>,<in0>, <in1>[, … <inN>]);
Mux
requires at least 3 parameters:
- Integer expression as selector
in0
as first selectablein1
to have a default case
Example
(*
Example with := assignment bar is an interafce IFoocolate and the methods
return differnt IFoocolate implementations
*)
bar := MUX(
typeSelect,
broom.getFoocolate(),
broom.getFoocolate(),
froom.getFoocolate(),
braam.getFoocolate(),
default.getFoocolate()
);
(* Equal example but with an CASE statement *)
CASE typeSelect OF
0,1:
bar := broom.getFoocolate();
3:
bar := froom.getFoocolate();
4:
bar := braam.getFoocolate();
ELSE
bar := default.getFoocolate();
END_CASE
(* Example to use it in an IF-Statement *)
IF (MUX(selector, foo, baz, fooBar, fooBarBaz)) THEN
bar := default.getFoocolate();
END_IF
(* Equal example but without MUX *)
CASE selector OF
0:
IF (foo) THEN
bar := default.getFoocolate();
END_IF
1:
IF (baz) THEN
bar := default.getFoocolate();
END_IF
2:
IF (fooBar) THEN
bar := default.getFoocolate();
END_IF
ELSE
IF (fooBarBaz) THEN
bar := default.getFoocolate();
END_IF
END_CASE
(* Equal example without MUX but less efficient*)
IF ((
((selector = 0) AND foo)
) OR_ELSE (
((selector = 1) AND baz)
) OR_ELSE (
((selector = 2) AND fooBar)
) OR_ELSE fooBarBaz
) THEN
bar := default.getFoocolate();
END_IF
(* Example with S= R= assignments *)
foo S= baz R= (MUX(selector, boo, baa, moo, maa) <> 42);
(* Equal example without mux *)
CASE selector OF
0:
foo S= baz R= (boo <> 42);
1:
foo S= baz R= (baa <> 42);
2:
foo S= baz R= (moo <> 42);
ELSE
foo S= baz R= (maa <> 42);
END_CASE
(* statemachine with mux *)
step := MUX(
step,
THIS^.receiveCarrier(step),
THIS^.moveIndexUnitUp(step),
THIS^.releaseForProcess(step),
THIS^.waitForProcessDone(step),
THIS^.sendCarrierToNext(step),
THIS^.executeErrorState(step)
);
(* equal statemachine with case-statment *)
CASE step OF
0:
step := THIS^.receiveCarrier(step);
1:
step := THIS^.moveIndexUnitUp(step);
2:
step := THIS^.releaseForProcess(step);
3:
step := THIS^.waitForProcessDone(step);
4:
step := THIS^.sendCarrierToNext(step);
ELSE
step := THIS^.executeErrorState(step);
END_CASE