🔍
v1.3.8

Data Types

TSharkRex is a statically typed language - every variable must be declared with a specific type, and the compiler enforces type rules at compile time. This chapter covers all available data types, their sizes, ranges, and practical usage.

Integer Types

TSharkRex provides a range of integer types with different sizes and signedness, following IEC 61131-3 naming conventions:

Type Size Range Sign
BOOL 1 bit (8-bit storage) TRUE / FALSE (0 / 1) -
BYTE 8 bit 0 .. 255 unsigned
SINT 8 bit -128 .. 127 signed
INT 16 bit -32,768 .. 32,767 signed
UINT 16 bit 0 .. 65,535 unsigned
DINT 32 bit -2,147,483,648 .. 2,147,483,647 signed
UDINT 32 bit 0 .. 4,294,967,295 unsigned
WORD 16 bit 0 .. 65,535 unsigned (alias for UINT)
DWORD 32 bit 0 .. 4,294,967,295 unsigned (alias for UDINT)
Note

WORD is an alias for UINT and DWORD is an alias for UDINT. They are interchangeable - use whichever is clearer in your context. WORD/DWORD are common in IEC 61131-3 and low-level CAN data manipulation.

BOOL

The BOOL type represents a logical true/false value. Use it for flags, conditions, and signal states:

VAR
    ar_aktiv : BOOL := FALSE;
    forsta_start : BOOL := TRUE;
END_VAR;

VAR_SIGNAL
    SIGNAL_HELLJUS : BOOL;    // On/off signal from vehicle
END_VAR;

IF SIGNAL_HELLJUS THEN
    ar_aktiv := TRUE;
END_IF;
Although BOOL is logically a single bit, it occupies 8 bits (1 byte) of storage in memory. This is because the ARM Cortex-M4 architecture addresses memory in bytes. You do not need to worry about this in practice, but it explains why arrays of BOOL use more memory than you might expect.

BYTE

BYTE is an 8-bit unsigned integer. It is commonly used for raw CAN data, bitmask operations, and byte-level manipulation:

VAR
    can_byte : BYTE := 0x00;    // Hex literal
    bitmask : BYTE := 0x0F;     // Lower nibble mask
    resultat : BYTE;
END_VAR;

resultat := can_byte BAND bitmask;  // Bitwise AND

SINT

SINT (Short Integer) is an 8-bit signed integer. Use it when you need a small signed value, for example temperature offsets or small delta values:

VAR
    temp_offset : SINT := -10;   // Range: -128 to 127
END_VAR;

INT

INT is a 16-bit signed integer. It is the natural choice for signal values like speed, RPM, and temperature that fit within the range of -32,768 to 32,767:

VAR_SIGNAL
    SIGNAL_HASTIGHET : INT;      // Speed: 0..250 km/h fits easily
    SIGNAL_TEMPERATUR : INT;     // Temperature: -40..80 °C
    SIGNAL_VARVTAL : INT;        // RPM: 0..8000 (fits in INT)
END_VAR;

VAR
    max_hastighet : INT := 0;
END_VAR;

// Track maximum speed seen
IF SIGNAL_HASTIGHET > max_hastighet THEN
    max_hastighet := SIGNAL_HASTIGHET;
END_IF;

UINT

UINT is a 16-bit unsigned integer. Use it when you know the value is never negative and you need the extra positive range (up to 65,535):

VAR
    raknare : UINT := 0;        // Counter that never goes negative
    can_id : UINT := 0x0320;    // CAN identifier (11-bit fits in UINT)
END_VAR;

raknare := raknare + 1;

DINT

DINT (Double Integer) is a 32-bit signed integer. It is the most commonly used integer type for general-purpose values in TSharkRex programs:

VAR
    total_distance : DINT := 0;    // Large accumulator
    tidsstampel : DINT := 0;       // Timestamp in ms
    can_data_32 : DINT;            // 32-bit CAN signal value
END_VAR;
Memory Matters

XBB devices have limited RAM. A typical production recipe uses 40–80% of available RAM, so choosing the right data type is important. Use the smallest type that fits your data:

Every unnecessary DINT where a BYTE or INT would suffice wastes 2–3 bytes of RAM. In a recipe with hundreds of variables, this adds up quickly.

UDINT

UDINT is a 32-bit unsigned integer. It is used internally for the TIME type and is useful for large counters and addresses:

VAR
    cykliskt_raknare : UDINT := 0;    // Can count to ~4.3 billion
    bitfalt : UDINT := 0xDEADBEEF;    // 32-bit bitmask
END_VAR;

Floating-Point Types

TSharkRex supports IEEE 754 floating-point numbers for calculations that require decimal precision:

Type Size Description
REAL 32 bit Single-precision floating point
LREAL 64 bit Double-precision floating point

REAL

REAL provides approximately 7 significant decimal digits. Use it for physical calculations, scaling factors, and sensor values that include fractional parts:

VAR
    spaning : REAL;                    // Battery voltage
    skalfaktor : REAL;                 // Scaling factor
    temperatur_c : REAL;
END_VAR;

VAR_SIGNAL
    SIGNAL_TEMP_RAW : INT;            // Raw sensor value
END_VAR;

// Initialize REAL values in code (not in VAR declarations)
spaning := 12.8;
skalfaktor := 0.1;

// Convert raw sensor reading to Celsius
// Example formula: temp = raw * 0.1 - 40.0
temperatur_c := SINT_TO_FP(SIGNAL_TEMP_RAW, REAL) * skalfaktor - 40.0;
REAL and LREAL variables cannot have initial values in VAR declarations. Declare them without an initializer and assign values in the code body.

LREAL

LREAL provides approximately 15 significant decimal digits. Use it only when REAL does not provide sufficient precision:

VAR
    precision_varde : LREAL;
    ackumulator : LREAL;
END_VAR;

// LREAL literals use the 'd' suffix
precision_varde := 3.141592653589793d;
ackumulator := 0.0d;
Floating-point operations on the ARM Cortex-M4 hardware FPU are limited to single precision (REAL). While LREAL is supported by the compiler, double-precision operations are emulated in software and are significantly slower. Prefer REAL unless you genuinely need the extra precision.

Special Types

In addition to the numeric primitives, TSharkRex provides several special-purpose types:

Type Description
TIME Duration in milliseconds (stored as UDINT internally)
STRING Text string (not yet supported by compiler)
POINTER TO X Pointer to a value of type X
ARRAY[a..b] OF X Fixed-size array of type X

TIME

The TIME type represents a duration in milliseconds. Internally it is stored as a UDINT (32-bit unsigned integer), giving a maximum duration of approximately 49.7 days. TIME is used extensively with timers and delay function blocks:

VAR
    fordrojning : TIME := T#500ms;     // 500 milliseconds
    liten_paus : TIME := T#100ms;      // 100 milliseconds
    lang_timeout : TIME := T#30s;      // 30 seconds
    en_minut : TIME := T#1m;           // 1 minute
END_VAR;

Time literals use the T# prefix followed by a value and unit:

Literal Meaning Milliseconds
T#100ms 100 milliseconds 100
T#1s 1 second 1,000
T#1500ms 1.5 seconds 1,500
T#5s 5 seconds 5,000
T#1m 1 minute 60,000

A practical example using TIME with a timer function block:

VAR
    timer1 : TON;                      // Timer On-Delay
    lampan_pa : BOOL := FALSE;
END_VAR;

VAR_SIGNAL
    SIGNAL_HELLJUS : BOOL;
END_VAR;

// Delay turning on lights by 2 seconds after high-beam activates
timer1(IN := SIGNAL_HELLJUS, PT := T#2s);
lampan_pa := timer1.Q;                // TRUE after 2 seconds
Since TIME is stored as UDINT internally, you can perform arithmetic on time values. For example, T#1s + T#500ms results in T#1500ms. You can also compare time values with <, >, =, etc.

STRING

Not Yet Supported

The STRING data type is not currently supported by the TSharkRex compiler. String literals (e.g., "Hello") can only be used in specific contexts such as description labels in VAR_OUTPUT declarations (e.g., HELLJUS ["Helljus"] : OUTPUT;). You cannot declare STRING variables or perform string operations in recipes.

POINTER TO

The POINTER TO type creates a pointer (reference) to a value of the specified type. Pointers are an advanced feature used primarily in function block implementations and library code:

VAR
    varde : DINT := 42;
    pekare : POINTER TO DINT;
END_VAR;

pekare := @varde;        // Point to 'varde'
// pekare^ accesses the value (42)
Pointers are a powerful but potentially dangerous feature. Dereferencing an invalid pointer will cause undefined behavior on the ARM hardware. Use pointers only when necessary, and prefer passing values directly when possible.

ARRAY

The ARRAY type declares a fixed-size, zero-overhead collection of elements of the same type. Arrays are essential for working with multi-byte CAN data and lookup tables:

VAR
    can_data : ARRAY[0..7] OF BYTE;       // 8-byte CAN frame data
    hastigheter : ARRAY[0..9] OF INT;     // 10 speed samples
    flaggor : ARRAY[0..31] OF BOOL;       // 32 boolean flags
END_VAR;

VAR_SIGNAL
    SIGNAL_HASTIGHET : INT;
END_VAR;

// Access elements by index
can_data[0] := 0xFF;
can_data[1] := 0xA0;

// Use in calculations
hastigheter[0] := SIGNAL_HASTIGHET;

// Loop through array (FOR declares loop variable inline)
FOR i : BYTE := 0 TO 7 DO
    can_data[i] := 0x00;    // Clear all bytes
END_FOR;

Array indices are zero-based (unless you declare a different range). The array bounds are checked at compile time when using literal indices, but runtime bounds checking is not performed for performance reasons.

Accessing an array out of bounds (e.g., can_data[8] on an ARRAY[0..7]) causes undefined behavior. The compiler will catch constant out-of-bounds indices, but it cannot check variable indices at compile time. Always ensure your loop bounds and calculated indices are valid.

You can also declare arrays with custom index ranges:

VAR
    tabell : ARRAY[0..4] OF DINT;     // 5 elements, indices 0 through 4
END_VAR;

tabell[0] := 100;
tabell[2] := 300;
tabell[4] := 500;

Type Conversions

TSharkRex does not perform implicit type conversions (with very few exceptions). When you need to convert between types, you must be explicit about it. The general rules are:

Widening Conversions (Safe)

Converting from a smaller type to a larger type of the same signedness is always safe - no data is lost:

VAR
    liten : BYTE := 200;
    stor : DINT;
END_VAR;

stor := liten;     // Safe: BYTE (200) fits in DINT

Narrowing Conversions (Potentially Lossy)

Converting from a larger type to a smaller type may lose information. The value is truncated to fit:

VAR
    stor_varde : DINT := 100000;
    litet_varde : INT;
END_VAR;

litet_varde := stor_varde;  // WARNING: 100000 does not fit in INT
                              // Result is truncated/wrapped
When converting from a larger type to a smaller type, the value is silently truncated. The compiler may or may not warn about this. Always verify that your values fit within the target type’s range to avoid unexpected behavior.

Float/Integer Conversion

Converting between floating-point and integer types requires care:

VAR
    hastighet_float : REAL;
    hastighet_int : INT;
    avrundad : REAL;
END_VAR;

// REAL cannot have initial values in VAR - assign in code
hastighet_float := 85.7;

// Float to integer: use FP_TO_SINT to convert (truncates fractional part)
hastighet_int := FP_TO_SINT(hastighet_float, INT);    // Result: 85 (not 86)

// Integer to float: use SINT_TO_FP to convert
avrundad := SINT_TO_FP(hastighet_int, REAL);           // Result: 85.0

Choosing the Right Type

Here is a practical guide for common scenarios:

Scenario Recommended Type Why
On/off signal (high-beam, etc.) BOOL Logical true/false
Raw CAN byte BYTE Matches CAN frame byte size
Speed, RPM, temperature INT Typical signal range fits
General-purpose counter/value DINT Wide range, native word size
CAN ID (11-bit or 29-bit) UDINT Unsigned, 29-bit extended IDs need 32 bits
Sensor scaling / physical units REAL Fractional precision needed
Timer delays TIME Semantic clarity, works with timer FBs
CAN frame data buffer ARRAY[0..7] OF BYTE Standard 8-byte CAN frame

Practical Examples

The following recipe demonstrates multiple data types working together:

(*
   Recipe: Datatypes Demo
   Shows various data types in a realistic scenario
*)

VAR_SIGNAL
    SIGNAL_HELLJUS : BOOL;         // High-beam status
    SIGNAL_HASTIGHET : INT;        // Speed in km/h
    SIGNAL_MOTORTEMP : INT;        // Engine temp raw value
END_VAR;

VAR_OUTPUT
    EXTRALJUS : OUTPUT;            // Auxiliary lights
    TEMP_VARNING : OUTPUT;         // Temperature warning
END_VAR;

VAR
    // Counters and state
    cykel_raknare : UDINT := 0;          // Scan cycle counter
    helljus_tid : TIME := T#0ms;         // Time with high-beam
    overtemp : BOOL := FALSE;

    // Scaling
    temp_celsius : REAL;
    temp_skalfaktor : REAL;
    temp_offset : REAL;

    // Thresholds
    max_temp : REAL;
END_VAR;

VAR_CONSTANT
    HASTIGHETSGRANS : INT := 160;
    PERIOD_BLINK : INT := 400;
END_VAR;

// Initialize REAL values (cannot be set in VAR declarations)
temp_skalfaktor := 0.75;
temp_offset := -48.0;
max_temp := 105.0;

// Count scan cycles
cykel_raknare := cykel_raknare + 1;

// Convert raw temperature to Celsius
temp_celsius := SINT_TO_FP(SIGNAL_MOTORTEMP, REAL) * temp_skalfaktor + temp_offset;

// Check overtemperature
IF temp_celsius > max_temp THEN
    overtemp := TRUE;
END_IF;

// Calculate output values using intermediate variables
VAR
    temp_varning_varde : INT;
    extraljus_varde : INT;
END_VAR;

IF overtemp THEN
    temp_varning_varde := 1000;
ELSE
    temp_varning_varde := 0;
END_IF;

IF SIGNAL_HELLJUS AND (SIGNAL_HASTIGHET < HASTIGHETSGRANS) THEN
    extraljus_varde := 1000;
ELSE
    extraljus_varde := 0;
END_IF;

// Outputs ALWAYS called every cycle
TEMP_VARNING(VALUE := temp_varning_varde, PERIOD := PERIOD_BLINK);
EXTRALJUS(VALUE := extraljus_varde, PERIOD := 1000);

User-Defined Types (TYPE / STRUCT)

TSharkRex supports user-defined structured types using TYPE and STRUCT. This lets you group related fields into a single named type:

TYPE SensorData : STRUCT
    value : INT;
    status : BYTE;
    timestamp : UDINT;
END_STRUCT
END_TYPE;

VAR
    sensor1 : SensorData;
    sensor2 : SensorData;
END_VAR;

sensor1.value := 1234;
sensor1.status := 0x01;
sensor2.timestamp := 0;
Note

User-defined types are primarily used in library development for organizing complex data. Most recipes do not need custom types - built-in types and arrays cover typical use cases.