Beyond IF-Statements: Exploring Branch Techniques in TwinCAT 3

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.

noun-information-6735183-FFFFFF_edited

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
noun-information-6735183-FFFFFF_edited
All assignments refer to the operand at the end of the code line.

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
noun-information-6735183-FFFFFF_edited
All assignments refer to the <expression> at the end of the code line.

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
noun-information-6735183-FFFFFF_edited
All assignments refer to the <expression> at the end of the code line.

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

noun-information-6735183-FFFFFF_edited

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

noun-information-6735183-FFFFFF_edited

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>]

noun-information-6735183-FFFFFF_edited

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>)];

noun-information-6735183-FFFFFF_edited

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>);

noun-information-6735183-FFFFFF_edited

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 selectable
  • in1 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