🔍
v1.3.8

Type Conversions

TSharkRex is a strongly typed language - you cannot freely mix data types in expressions without telling the compiler how to interpret the conversion. This chapter covers all available conversion functions, from standard named conversions to low-level bit manipulation, float conversions, and the implicit conversions the compiler handles automatically.

Standard Conversions

Standard conversion functions follow the naming pattern SOURCE_TO_TARGET(value). They convert a value from one integer or boolean type to another:

Function Conversion Example
BYTE_TO_BOOL(x) BYTE → BOOL flag := BYTE_TO_BOOL(value);
INT_TO_BOOL(x) INT → BOOL flag := INT_TO_BOOL(count);
DINT_TO_BOOL(x) DINT → BOOL flag := DINT_TO_BOOL(result);
INT_TO_BYTE(x) INT → BYTE b := INT_TO_BYTE(value);
UDINT_TO_INT(x) UDINT → INT i := UDINT_TO_INT(time);
SINT_TO_DINT(x) SINT → DINT d := SINT_TO_DINT(small);

Practical Examples

Boolean to integer for output control

VAR
    doorOpen : BOOL;
    outputValue : INT;
    canData : ARRAY[0..7] OF BYTE;
END_VAR;

// BOOL is implicitly 0 or 1 - multiply for PWM range
outputValue := doorOpen * 1000;

// Or use explicit conversion when needed in expressions
outputValue := INT_TO_BOOL(canData[3]) * 500;

Narrowing a larger type to BYTE

VAR
    fullValue : INT := 1234;
    lowByte : BYTE;
END_VAR;

// Extract low byte (loses upper bits)
lowByte := INT_TO_BYTE(fullValue);
// lowByte = 210 (1234 AND 0xFF = 210)

Widening SINT to DINT for arithmetic

VAR
    temperature : SINT := -20;   // Signed byte: -128 to 127
    tempInMillideg : DINT;
END_VAR;

// Widen to DINT before multiplication to avoid overflow
tempInMillideg := SINT_TO_DINT(temperature) * 1000;
// tempInMillideg = -20000
Note: When converting to BOOL, any non-zero value becomes TRUE and zero becomes FALSE. When converting from BOOL, TRUE becomes 1 and FALSE becomes 0.

Low-Level Conversions

Low-level conversion functions give you explicit control over how bits are interpreted when changing type size. These are essential when working with CAN data where byte ordering and sign handling matter:

Function Description Example
TRUNC(val, type) Truncate to a smaller type - keeps only the low bits b := TRUNC(dint_val, BYTE);
S_EXT(val, type) Sign-extend to a larger type - preserves the sign bit d := S_EXT(sint_val, DINT);
Z_EXT(val, type) Zero-extend to a larger type - fills upper bits with zeros d := Z_EXT(byte_val, DINT);

TRUNC - Truncation

TRUNC discards the upper bits, keeping only the bits that fit in the target type. This is equivalent to a bitwise AND with the target type’s mask:

VAR
    bigValue : DINT := 0xAABBCCDD;
    lowWord : INT;
    lowByte : BYTE;
END_VAR;

lowWord := TRUNC(bigValue, INT);    // 0xCCDD
lowByte := TRUNC(bigValue, BYTE);   // 0xDD

S_EXT - Sign Extension

S_EXT widens a signed value while preserving its sign. The sign bit (most significant bit) is copied into all the new upper bits:

VAR
    signed_byte : SINT := -50;       // 0xCE in binary
    wide_result : DINT;
END_VAR;

wide_result := S_EXT(signed_byte, DINT);
// wide_result = -50 (0xFFFFFFCE - sign bit extended)

This is critical when decoding signed CAN data. Many vehicle signals transmit signed values as single bytes. Without sign extension, a negative value like -50 (stored as 0xCE) would be interpreted as 206:

VAR
    canData : ARRAY[0..7] OF BYTE;
    temperatureDINT : DINT;
    temperatureWrong : DINT;
END_VAR;
// CAN byte representing temperature: 0xCE = -50°C (signed)
temperatureDINT := S_EXT(canData[2], DINT);
// Correct: temperatureDINT = -50

// WRONG: without S_EXT, zero-extension gives +206
temperatureWrong := Z_EXT(canData[2], DINT);
// Wrong: temperatureWrong = 206

Z_EXT - Zero Extension

Z_EXT widens an unsigned value by filling the upper bits with zeros. Use this for unsigned data such as CAN IDs, counters, and positive-only measurements:

VAR
    canByte : BYTE := 0xFF;
    fullValue : DINT;
END_VAR;

fullValue := Z_EXT(canByte, DINT);
// fullValue = 255 (0x000000FF - upper bits zeroed)
Warning: Choosing the wrong extension type is a common source of bugs. Use S_EXT for signed data (temperatures, angles, offsets) and Z_EXT for unsigned data (counters, IDs, percentages). Getting this wrong will produce wildly incorrect values for negative numbers.

Float Conversions

TSharkRex supports IEEE 754 floating-point arithmetic via the REAL type. Use these functions to convert between integer and floating-point representations:

Function Description
FP_TO_SINT(val, type) Float → signed integer (truncates toward zero)
FP_TO_UINT(val, type) Float → unsigned integer (truncates toward zero)
SINT_TO_FP(val, type) Signed integer → float
UINT_TO_FP(val, type) Unsigned integer → float
FP_TRUNC(val, type) Float truncation - LREAL → REAL (double to single precision)
FP_EXT(val, type) Float extension - REAL → LREAL (single to double precision)

Float Precision Conversion

To convert between single-precision (REAL) and double-precision (LREAL):

VAR
    single : REAL;
    double : LREAL;
    back : REAL;
END_VAR;

single := 3.14;

// REAL -> LREAL (extend precision)
double := FP_EXT(single, LREAL);

// LREAL -> REAL (truncate precision, may lose accuracy)
back := FP_TRUNC(double, REAL);

Float Conversion Examples

Integer to float for precise division

VAR
    numerator : DINT := 7;
    denominator : DINT := 3;
    result : REAL;
    resultInt : DINT;
END_VAR;

// Convert to float for division, then back to integer
result := SINT_TO_FP(numerator, REAL) / SINT_TO_FP(denominator, REAL);
// result = 2.333...

resultInt := FP_TO_SINT(result, DINT);
// resultInt = 2 (truncated toward zero)

Scaling a CAN signal with float precision

VAR
    rawValue : INT;         // Raw CAN value (0-4095)
    scaledFloat : REAL;
    temperature : DINT;     // Final temperature in °C
END_VAR;

// Scale: temp = (raw * 0.1) - 40.0
scaledFloat := SINT_TO_FP(rawValue, REAL) * 0.1 - 40.0;
temperature := FP_TO_SINT(scaledFloat, DINT);

Unsigned to float

VAR
    rpmRaw : UDINT := 3200;
    rpmFloat : REAL;
END_VAR;

rpmFloat := UINT_TO_FP(rpmRaw, REAL);
// rpmFloat = 3200.0
Note: FP_TO_SINT and FP_TO_UINT truncate toward zero (not round). A value of 2.9 becomes 2, and -2.9 becomes -2. If you need rounding, add 0.5 before converting: FP_TO_SINT(val + 0.5, DINT).

Sign Conversions

These functions reinterpret a value’s sign without changing its bit pattern. The underlying bits stay the same - only the compiler’s interpretation changes:

Function Description
SINT_TO_UINT(val) Reinterpret signed as unsigned (same bit pattern)
UINT_TO_SINT(val) Reinterpret unsigned as signed (same bit pattern)

Sign Conversion Examples

VAR
    signed_val : DINT := -1;
    unsigned_val : UDINT;
END_VAR;

unsigned_val := SINT_TO_UINT(signed_val);
// unsigned_val = 4294967295 (0xFFFFFFFF - same bits, different meaning)
VAR
    unsigned_val : UDINT := 0xFFFFFFF0;
    signed_val : DINT;
END_VAR;

signed_val := UINT_TO_SINT(unsigned_val);
// signed_val = -16 (same bits, now interpreted as signed)

Sign conversions are primarily needed when interfacing with CAN data that stores signed values in unsigned byte arrays, or when passing values to functions that expect a different signedness:

// CAN data gives us a two-byte unsigned value
VAR
    canData : ARRAY[0..7] OF BYTE;
    rawUnsigned : UINT;
    steeringAngle : INT;   // Signed: negative = left, positive = right
END_VAR;

rawUnsigned := canData[0] * 256 + canData[1];
steeringAngle := UINT_TO_SINT(rawUnsigned);
// If rawUnsigned = 0xFF00, steeringAngle = -256 (hard left)

Implicit Conversions

TSharkRex automatically performs certain “safe” conversions where no information is lost. These implicit conversions happen transparently in expressions:

BOOL to Integer Types

BOOL values are implicitly converted to integer types in arithmetic expressions. TRUE becomes 1 and FALSE becomes 0:

VAR
    ignitionOn : BOOL := TRUE;
    outputPWM : INT;
END_VAR;

// BOOL * INT is implicitly converted - no cast needed
outputPWM := ignitionOn * 1000;
// outputPWM = 1000 (TRUE * 1000 = 1 * 1000)

This is the foundation of the idiomatic TSharkRex pattern for driving outputs from boolean signals (see Outputs & PWM).

Small to Large Integer Types

In mixed-type arithmetic, smaller integer types are implicitly widened to match the larger type:

VAR
    byteVal : BYTE := 200;
    intVal : INT := 1000;
    result : INT;
END_VAR;

result := byteVal + intVal;
// byteVal implicitly zero-extended to INT before addition
// result = 1200
Warning: Implicit conversions from larger to smaller types are not performed automatically. Assigning a DINT to a BYTE without an explicit conversion will produce a compiler error. Always use TRUNC or the appropriate TYPE_TO_TYPE function when narrowing:
VAR
    big : DINT := 1000;
    small : BYTE;
END_VAR;

// Compiler error: cannot implicitly narrow DINT to BYTE
// small := big;

// Correct: explicit truncation
small := TRUNC(big, BYTE);   // small = 232 (1000 AND 0xFF)

Conversion Quick Reference

Use this guide to choose the right conversion for your situation:

Situation Function Why
Unsigned byte → larger signed integer Z_EXT(val, DINT) Preserve unsigned value (no sign bit to extend)
Signed byte → larger signed integer S_EXT(val, DINT) Preserve negative values (extend sign bit)
Large integer → byte TRUNC(val, BYTE) Keep only the low 8 bits
Integer → float SINT_TO_FP(val, REAL) Enable precise arithmetic / division
Float → integer FP_TO_SINT(val, DINT) Convert result back (truncates toward zero)
Reinterpret sign UINT_TO_SINT(val) Same bits, different sign interpretation
Any value → BOOL DINT_TO_BOOL(val) Non-zero = TRUE, zero = FALSE
Tip: When in doubt about which conversion to use for CAN data, check the vehicle’s DBC file or signal specification. It will tell you whether a signal is signed or unsigned, which determines whether you need S_EXT or Z_EXT.