Control Flow
Control flow statements determine which code executes and in what order. TSharkRex provides
conditional branching (IF, CASE) and loops (WHILE,
FOR, DO-WHILE).
These constructs follow IEC 61131-3 structured text conventions
with some TSharkRex-specific extensions.
IF / THEN / ELSE / ELSIF / END_IF
The IF statement is the most fundamental control structure. It evaluates a
boolean expression and executes the corresponding branch.
Simple IF
Execute a block only when a condition is true:
// Syntax:
// IF <bool_expression> THEN
// ... statements ...
// END_IF;
VAR_SIGNAL
SIGNAL_TANDNING : BOOL;
END_VAR;
VAR_OUTPUT
DRL : OUTPUT;
END_VAR;
// Turn on daytime running lights when ignition is on
IF SIGNAL_TANDNING THEN
DRL(VALUE := 500, PERIOD := 1000); // 50% brightness
END_IF;
IF / ELSE
Choose between two branches:
// Syntax:
// IF <bool_expression> THEN
// ... true branch ...
// ELSE
// ... false branch ...
// END_IF;
VAR_SIGNAL
SIGNAL_TANDNING : BOOL;
END_VAR;
VAR_OUTPUT
DRL : OUTPUT;
END_VAR;
IF SIGNAL_TANDNING THEN
DRL(VALUE := 500, PERIOD := 1000); // 50% brightness
ELSE
DRL(VALUE := 0, PERIOD := 1000); // Off when ignition is off
END_IF;
IF / ELSIF / ELSE
Chain multiple conditions. Conditions are tested top-to-bottom; the first one that evaluates to TRUE wins, and the rest are skipped:
// Syntax:
// IF <condition1> THEN
// ... executed if condition1 is TRUE ...
// ELSIF <condition2> THEN
// ... executed if condition1 is FALSE and condition2 is TRUE ...
// ELSIF <condition3> THEN
// ... executed if condition1 and condition2 are FALSE and condition3 is TRUE ...
// ELSE
// ... executed if all conditions are FALSE ...
// END_IF;
Practical Examples
VAR
batteryVoltage : INT; // millivolts
chargeState : BYTE;
END_VAR;
// Classify battery voltage into charge states
IF batteryVoltage > 14000 THEN
chargeState := 3; // Charging
ELSIF batteryVoltage > 12400 THEN
chargeState := 2; // Normal
ELSIF batteryVoltage > 11800 THEN
chargeState := 1; // Low
ELSE
chargeState := 0; // Critical
END_IF;
VAR_SIGNAL
SIGNAL_HELLJUS : BOOL;
SIGNAL_HALVLJUS : BOOL;
SIGNAL_DIMLJUS : BOOL;
END_VAR;
VAR_OUTPUT
FRONT_LIGHT : OUTPUT;
END_VAR;
// Priority-based light control
IF SIGNAL_HELLJUS THEN
FRONT_LIGHT(VALUE := 1000, PERIOD := 1000); // High beam: full brightness
ELSIF SIGNAL_HALVLJUS THEN
FRONT_LIGHT(VALUE := 800, PERIOD := 1000); // Low beam: 80%
ELSIF SIGNAL_DIMLJUS THEN
FRONT_LIGHT(VALUE := 400, PERIOD := 1000); // Fog light: 40%
ELSE
FRONT_LIGHT(VALUE := 0, PERIOD := 1000); // All off
END_IF;
Compound Conditions
Use AND, OR, and NOT to build complex conditions.
Use parentheses to make the logic clear:
VAR_SIGNAL
SIGNAL_TANDNING : BOOL;
END_VAR;
VAR
speed : INT;
doorLocked : BOOL;
securityMode : BOOL;
engineTemp : INT;
batteryVoltage : INT;
warningActive : BOOL;
END_VAR;
// Only activate if ignition is on AND vehicle is stationary AND doors are locked
IF SIGNAL_TANDNING AND (speed = 0) AND doorLocked THEN
securityMode := TRUE;
END_IF;
// Activate warning if EITHER temperature is too high OR voltage is too low
IF (engineTemp > 110) OR (batteryVoltage < 11000) THEN
warningActive := TRUE;
END_IF;
Using Bit Access in Conditions
Bit access expressions return BOOL and can be used directly as conditions:
VAR_SIGNAL
SIGNAL_HALVLJUS : BOOL;
END_VAR;
VAR
CANRECV : CAN_RX;
RECVDATA : ARRAY[0..7] OF BYTE;
bitCheck : BOOL;
END_VAR;
// Receive CAN frame 0x320
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 5, DATA := RECVDATA);
// Check individual bits from CAN data
IF CANRECV.AVAILABLE > 0 THEN
IF RECVDATA[3].5 THEN
SIGNAL_HALVLJUS := TRUE;
END_IF;
// Combine bit checks
IF RECVDATA[0].0 AND NOT RECVDATA[0].1 THEN
bitCheck := TRUE; // Bit 0 is set, bit 1 is clear
END_IF;
END_IF;
CASE / OF / END_CASE
The CASE statement selects one of several branches based on the value of a
selector expression. It is cleaner than a long chain of ELSIF when you are
matching against discrete values.
// Syntax:
// CASE <variable> OF
// 0: ... statements for value 0 ...
// 1: ... statements for value 1 ...
// 2: ... statements for value 2 ...
// ELSE
// ... default (no match) ...
// END_CASE;
The selector expression can be BOOL, BYTE, INT, or
DINT. Each case label is a single constant value.
Basic Example
VAR
mode : BYTE := 0;
outputLevel : BYTE;
END_VAR;
CASE mode OF
0:
outputLevel := 0; // Off
1:
outputLevel := 25; // Low
2:
outputLevel := 50; // Medium
3:
outputLevel := 100; // High
ELSE
outputLevel := 0; // Unknown mode, default to off
END_CASE;
State Machine Pattern
The most powerful use of CASE is implementing state machines. This is the
standard pattern for multi-step CAN communication protocols like UDS diagnostics:
VAR
state : BYTE := 0;
responseTimer : TON;
retryCount : BYTE := 0;
startDiagnostics : BOOL;
responseReceived : BOOL;
resetRequested : BOOL;
CANSEND : CAN_TX;
SENDDATA : ARRAY[0..7] OF BYTE;
END_VAR;
VAR_CONSTANT
STATE_IDLE : BYTE := 0;
STATE_SEND_REQUEST : BYTE := 1;
STATE_WAIT_RESPONSE : BYTE := 2;
STATE_PROCESS : BYTE := 3;
STATE_ERROR : BYTE := 10;
END_VAR;
CASE state OF
STATE_IDLE:
// Wait for trigger
IF startDiagnostics THEN
state := STATE_SEND_REQUEST;
retryCount := 0;
END_IF;
STATE_SEND_REQUEST:
// Send UDS request
SENDDATA[0] := 0x03;
SENDDATA[1] := 0x22; // ReadDataByIdentifier
SENDDATA[2] := 0xF1;
SENDDATA[3] := 0x90; // VIN
SENDDATA[4] := 0x00;
SENDDATA[5] := 0x00;
SENDDATA[6] := 0x00;
SENDDATA[7] := 0x00;
CANSEND(ENABLE := TRUE, ID := 0x7E0, EXT := FALSE,
DATALENGTH := 8, DATA := SENDDATA);
responseTimer(IN := FALSE); // Reset timer
state := STATE_WAIT_RESPONSE;
STATE_WAIT_RESPONSE:
// Wait for response with timeout
responseTimer(IN := TRUE, PT := T#2s);
IF responseReceived THEN
state := STATE_PROCESS;
ELSIF responseTimer.Q THEN
// Timeout
retryCount := retryCount + 1;
IF retryCount >= 3 THEN
state := STATE_ERROR;
ELSE
state := STATE_SEND_REQUEST; // Retry
END_IF;
END_IF;
STATE_PROCESS:
// Process the response data
state := STATE_IDLE;
STATE_ERROR:
// Handle error
IF resetRequested THEN
state := STATE_IDLE;
END_IF;
END_CASE;
Multiple Values per Case
TSharkRex requires one value per case label. To handle multiple values with the same logic, use separate case labels for each value:
VAR
errorCode : BYTE;
statusLed : BYTE;
END_VAR;
CASE errorCode OF
0:
// No error
statusLed := 0;
1:
// Minor errors: yellow warning
statusLed := 1;
2:
statusLed := 1;
3:
statusLed := 1;
10:
// Communication errors: red warning
statusLed := 2;
11:
statusLed := 2;
12:
statusLed := 2;
ELSE
// Unknown error code
statusLed := 3;
END_CASE;
1, 2, 3: is not valid). Each value must have
its own case label.
WHILE / DO / END_WHILE
The WHILE loop repeats a block of code as long as its condition remains
TRUE. The condition is checked before each iteration, so the body
may execute zero times if the condition is initially false.
// Syntax:
// WHILE <bool_expression> DO
// ... repeated while expression is TRUE ...
// END_WHILE;
Processing a CAN Message Queue
The most common use of WHILE in TSharkRex is draining the CAN receive
buffer. The hardware may accumulate multiple CAN frames between scan cycles, and you
need to process all of them:
VAR
CANRECV : CAN_RX;
RECVDATA : ARRAY[0..7] OF BYTE;
MAXCNT : BYTE;
END_VAR;
VAR_SIGNAL
SIGNAL_TANDNING : BOOL;
SIGNAL_HALVLJUS : BOOL;
SIGNAL_HELLJUS : BOOL;
END_VAR;
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 10, DATA := RECVDATA);
MAXCNT := 0;
WHILE CANRECV.AVAILABLE > 0 AND MAXCNT < 10 DO
SIGNAL_TANDNING := RECVDATA[3].0;
SIGNAL_HALVLJUS := RECVDATA[3].5;
SIGNAL_HELLJUS := RECVDATA[3].6;
MAXCNT := MAXCNT + 1;
// Pop next message from queue
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 10, DATA := RECVDATA);
END_WHILE;
FALSE. In the example above, MAXCNT serves as a safety
limit: even if CANRECV.AVAILABLE never reaches zero (due to a flood of
messages), the loop will exit after 10 iterations, preventing the scan cycle from stalling.
Search Pattern
VAR
buffer : ARRAY[0..63] OF BYTE;
idx : BYTE := 0;
found : BOOL := FALSE;
searchValue : BYTE := 0xAA;
END_VAR;
// Search for a specific byte value in a buffer
idx := 0;
found := FALSE;
WHILE idx < 64 AND NOT found DO
IF buffer[idx] = searchValue THEN
found := TRUE;
ELSE
idx := idx + 1;
END_IF;
END_WHILE;
// After the loop:
// - found = TRUE and idx = position of the match, OR
// - found = FALSE and idx = 64 (not found)
FOR / TO / DO / END_FOR
The FOR loop iterates a counter variable from a start value to an end value
(inclusive). It is the cleanest way to process arrays and fixed-count iterations.
// Syntax:
// FOR counter := start TO end DO
// ... body executes for each value of counter ...
// END_FOR;
Basic Examples
VAR
myArray : ARRAY[0..7] OF BYTE;
counter : BYTE;
sum : DINT := 0;
END_VAR;
// Fill an array with 0xFF
FOR counter := 0 TO 7 DO
myArray[counter] := 0xFF;
END_FOR;
// Sum all elements
sum := 0;
FOR counter := 0 TO 7 DO
sum := sum + Z_EXT(myArray[counter], DINT);
END_FOR;
Inline Variable Declaration
TSharkRex allows you to declare the loop variable directly in the FOR
statement. This keeps the variable scoped to the loop and avoids cluttering your
VAR section:
VAR
buffer : ARRAY[0..7] OF BYTE;
END_VAR;
// Inline declaration: the variable 'i' is declared right in the FOR statement
FOR i : BYTE := 0 TO 7 DO
buffer[i] := 0x00;
END_FOR;
VAR
TIMERS : ARRAY[0..3] OF TON;
timerActive : ARRAY[0..3] OF BOOL;
END_VAR;
// Call each timer function block
FOR i : BYTE := 0 TO 3 DO
TIMERS[i](IN := timerActive[i], PT := T#1s);
END_FOR;
Nested FOR Loops
VAR
matrix : ARRAY[0..3, 0..7] OF BYTE;
row : BYTE;
col : BYTE;
END_VAR;
// Initialize a 2D array (4 rows x 8 columns)
FOR row := 0 TO 3 DO
FOR col := 0 TO 7 DO
matrix[row, col] := 0x00;
END_FOR;
END_FOR;
Copying CAN Data
VAR
savedFrame : ARRAY[0..7] OF BYTE;
RECVDATA : ARRAY[0..7] OF BYTE;
CANRECV : CAN_RX;
END_VAR;
// Receive CAN frame and save its data
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 5, DATA := RECVDATA);
IF CANRECV.AVAILABLE > 0 THEN
FOR i : BYTE := 0 TO 7 DO
savedFrame[i] := RECVDATA[i];
END_FOR;
END_IF;
DO-WHILE
The DO-WHILE loop executes its body at least once before checking
the condition. Use it when you need to perform an action before deciding whether to repeat.
// Syntax:
// DO
// ... executed at least once ...
// WHILE <bool_expression> END_WHILE;
Example
VAR
idx : BYTE := 0;
checksum : BYTE := 0;
data : ARRAY[0..7] OF BYTE := [0x03, 0x22, 0x45, 0x55, 0x00, 0x00, 0x00, 0x00];
result : BYTE := 0;
END_VAR;
// Calculate XOR checksum of first 4 bytes using DO-WHILE
// XOR is simulated as: (a BOR b) BAND BNOT(a BAND b)
checksum := 0;
idx := 0;
DO
result := (checksum BOR data[idx]) BAND BNOT(checksum BAND data[idx]);
checksum := result;
idx := idx + 1;
WHILE idx < 4 END_WHILE;
VAR
idx : BYTE := 0;
checksum : BYTE := 0;
data : ARRAY[0..7] OF BYTE;
END_VAR;
// Calculate XOR checksum of a data block (always processes at least one byte)
// XOR is simulated as: (a BOR b) BAND BNOT(a BAND b)
checksum := 0;
idx := 0;
DO
checksum := (checksum BOR data[idx]) BAND BNOT(checksum BAND data[idx]);
idx := idx + 1;
WHILE idx < 8 END_WHILE;
Breaking Out of a Loop
The TSharkRex compiler does not support the EXIT keyword.
To break out of a loop early, use a boolean flag variable in the loop
condition. This is a clean and reliable pattern:
VAR
buffer : ARRAY[0..63] OF BYTE;
foundIndex : INT := -1;
found : BOOL := FALSE;
END_VAR;
// Find the first zero byte in the buffer using a flag variable
found := FALSE;
foundIndex := -1;
FOR i : BYTE := 0 TO 63 DO
IF buffer[i] = 0x00 AND NOT found THEN
foundIndex := Z_EXT(i, INT);
found := TRUE; // Stop processing further iterations
END_IF;
END_FOR;
// foundIndex is now the position of the first 0x00, or -1 if not found
FOR loop will still iterate through all values,
but the AND NOT found guard ensures the body logic only executes for the
first match. For WHILE loops, include the flag directly in the loop condition
to stop iteration entirely.
VAR
CANRECV : CAN_RX;
RECVDATA : ARRAY[0..7] OF BYTE;
targetFound : BOOL := FALSE;
maxIter : BYTE := 0;
END_VAR;
// Set up CAN receiver for ECU response address
CANRECV(ENABLE := TRUE, ID := 0x7E8, EXT := FALSE,
MSG_COUNT := 10, DATA := RECVDATA);
// Process CAN messages until we find our target
targetFound := FALSE;
maxIter := 0;
WHILE CANRECV.AVAILABLE > 0 AND maxIter < 50 AND NOT targetFound DO
// Check if this is the diagnostic response we want
IF RECVDATA[1] = 0x62 THEN
targetFound := TRUE;
END_IF;
maxIter := maxIter + 1;
// Pop next message from queue
CANRECV(ENABLE := TRUE, ID := 0x7E8, EXT := FALSE,
MSG_COUNT := 10, DATA := RECVDATA);
END_WHILE;
Note on Functions
TSharkRex supports FUNCTION definitions with input parameters and a return
value. Note that RETURN (early exit from a function) is not supported
by the compiler. Structure your function logic so that the return value is assigned at
the end, using conditional branches instead of early returns.
FUNCTION find_byte : INT
VAR_INPUT
buffer : ARRAY[0..63] OF BYTE;
target : BYTE;
END_VAR;
VAR
found : BOOL := FALSE;
END_VAR;
// Search for target byte, return its index or -1
find_byte := -1; // Default: not found
FOR i : BYTE := 0 TO 63 DO
IF buffer[i] = target AND NOT found THEN
find_byte := Z_EXT(i, INT);
found := TRUE;
END_IF;
END_FOR;
END_FUNCTION;
FUNCTION validate_frame : BOOL
VAR_INPUT
id : DINT;
dlc : BYTE;
END_VAR;
// Validate frame - assign result based on conditions
// (RETURN is not supported, so use conditional logic)
IF id = 0 THEN
validate_frame := FALSE;
ELSIF dlc > 8 THEN
validate_frame := FALSE;
ELSE
validate_frame := TRUE;
END_IF;
END_FUNCTION;
Nesting Control Structures
All control flow statements can be nested inside each other. Here is a realistic example that combines several constructs to implement a CAN library parser:
VAR
CANRECV : CAN_RX;
RECVDATA : ARRAY[0..7] OF BYTE;
msgCount : BYTE;
END_VAR;
VAR_SIGNAL
SIGNAL_HELLJUS : BOOL;
SIGNAL_HALVLJUS : BOOL;
SIGNAL_TANDNING : BOOL;
END_VAR;
// Set up CAN receiver
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 20, DATA := RECVDATA);
// Process up to 20 CAN messages per scan cycle
msgCount := 0;
WHILE CANRECV.AVAILABLE > 0 AND msgCount < 20 DO
// Lighting status frame
SIGNAL_TANDNING := RECVDATA[3].0;
SIGNAL_HALVLJUS := RECVDATA[3].5;
SIGNAL_HELLJUS := RECVDATA[3].6;
msgCount := msgCount + 1;
// Pop next message
CANRECV(ENABLE := TRUE, ID := 0x320, EXT := FALSE,
MSG_COUNT := 20, DATA := RECVDATA);
END_WHILE;
Summary
| Statement | Purpose | Key Syntax |
|---|---|---|
IF |
Conditional branching | IF ... THEN ... ELSIF ... ELSE ... END_IF; |
CASE |
Multi-way branching on a value | CASE x OF 0: ... 1: ... ELSE ... END_CASE; |
WHILE |
Pre-checked loop | WHILE cond DO ... END_WHILE; |
FOR |
Counted loop | FOR i := 0 TO n DO ... END_FOR; |
DO-WHILE |
Post-checked loop (runs at least once) | DO ... WHILE cond END_WHILE; |
| Flag variable | Break out of current loop | AND NOT found in condition |
- Always include a safety counter in
WHILEloops to prevent infinite loops. - Use
CASEfor state machines - it is the clearest pattern for multi-step protocols. - Prefer
FORwhen you know the iteration count at compile time. - Use a boolean flag in the loop condition to stop searching once you find what you need (
EXITis not supported by the compiler). - Structure functions with conditional branches instead of early
RETURN(not supported).