🔍
v1.3.8

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;
Note: Unlike some other languages, TSharkRex does not support comma-separated values in a single case label (e.g., 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;
Warning: Infinite loops. Always ensure the loop condition will eventually become 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.
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
Note: The 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
Best practices: