🔍
v1.3.8

System Functions

TSharkRex provides a small set of low-level system functions for time measurement, memory operations, debugging, and instance identification. These functions operate below the normal recipe abstraction level and are rarely needed in typical recipes. However, when you need precise timing, raw memory manipulation, or instance-aware function blocks, these tools are indispensable.

Note: Most recipes will never need the functions in this chapter. The most commonly used one is GET_SYSTEM_TIME for measuring elapsed time. The memory functions (MEMSET, MEMCPY) and debug output (printf) are for advanced use cases only.

GET_SYSTEM_TIME

Returns the number of milliseconds since the system booted, as a UDINT (unsigned 32-bit integer). This is a free-running counter that wraps around after approximately 49.7 days.

FUNCTION GET_SYSTEM_TIME() : UDINT;

Basic Usage

VAR
    now : UDINT;
END_VAR;

now := GET_SYSTEM_TIME();
// now = milliseconds since boot (e.g. 123456)

Measuring Elapsed Time

The most common use case is measuring how much time has passed since an event. Capture the time when the event occurs, then subtract on subsequent cycles:

VAR
    startTime : UDINT;
    elapsed : UDINT;
    timerRunning : BOOL := FALSE;
    ignitionOn : BOOL;
END_VAR;

// Start measuring when ignition turns on
IF ignitionOn AND NOT timerRunning THEN
    startTime := GET_SYSTEM_TIME();
    timerRunning := TRUE;
END_IF;

// Calculate elapsed time each cycle
IF timerRunning THEN
    elapsed := GET_SYSTEM_TIME() - startTime;

    // Do something after 5 seconds
    IF elapsed > 5000 THEN
        timerRunning := FALSE;
        // 5 seconds have passed since ignition on
    END_IF;
END_IF;
Tip: For most timing needs, the built-in timer function blocks (TON, TOF, TP - see Timers) are simpler and less error-prone. Use GET_SYSTEM_TIME only when you need precise measurements that the timer blocks cannot provide, such as measuring the duration of a CAN request/response cycle or implementing custom timeout logic.

Rate Limiting with GET_SYSTEM_TIME

Use system time to limit how often an action is performed, independent of the scan cycle time:

VAR
    lastSendTime : UDINT := 0;
    currentTime : UDINT;
END_VAR;

currentTime := GET_SYSTEM_TIME();

// Send CAN message at most once every 200ms
IF (currentTime - lastSendTime) > 200 THEN
    lastSendTime := currentTime;
    // Send the CAN message here
END_IF;

Signal Debouncing

Debounce a noisy input signal by requiring it to be stable for a minimum duration:

VAR
    rawSignal : BOOL;
    debouncedSignal : BOOL := FALSE;
    lastChangeTime : UDINT;
    lastRawState : BOOL := FALSE;
END_VAR;

// Detect raw signal changes
IF rawSignal <> lastRawState THEN
    lastChangeTime := GET_SYSTEM_TIME();
    lastRawState := rawSignal;
END_IF;

// Accept new state only if stable for 50ms
IF (GET_SYSTEM_TIME() - lastChangeTime) > 50 THEN
    debouncedSignal := rawSignal;
END_IF;
Warning: GET_SYSTEM_TIME returns a UDINT that wraps around after ~49.7 days. The subtraction pattern GET_SYSTEM_TIME() - startTime handles wraparound correctly due to unsigned arithmetic, so you do not need special wraparound logic for intervals shorter than 49 days.

MEMSET

Fills a block of memory with a specified byte value. Commonly used to zero out buffers or initialize arrays to a known state.

FUNCTION MEMSET(dest : POINTER TO BYTE, value : DINT, size : DINT) : DINT;
Parameter Type Description
dest POINTER TO BYTE Starting address of the memory block to fill
value DINT Byte value to write (only the low 8 bits are used)
size DINT Number of bytes to fill

Examples

Zero out a CAN data buffer

VAR
    canBuffer : ARRAY[0..7] OF BYTE;
END_VAR;

// Clear all 8 bytes to zero
MEMSET(ADR(canBuffer[0]), 0, 8);

Initialize an array to a specific value

VAR
    statusArray : ARRAY[0..15] OF BYTE;
END_VAR;

// Fill all 16 bytes with 0xFF (all bits set)
MEMSET(ADR(statusArray[0]), 0xFF, 16);
Warning: MEMSET operates on raw memory with no bounds checking. Writing beyond the allocated buffer size will corrupt adjacent memory, leading to unpredictable behavior. Always ensure size does not exceed the actual buffer length.

MEMCPY

Copies a block of memory from a source address to a destination address. Used for transferring CAN frame data, duplicating buffers, or assembling multi-byte values.

FUNCTION MEMCPY(dest : POINTER TO BYTE, src : POINTER TO BYTE, size : DINT) : DINT;
Parameter Type Description
dest POINTER TO BYTE Destination address
src POINTER TO BYTE Source address
size DINT Number of bytes to copy

Examples

Copy CAN data to a processing buffer

VAR
    rxData : ARRAY[0..7] OF BYTE;    // Received CAN data
    workBuffer : ARRAY[0..7] OF BYTE; // Working copy
END_VAR;

// Copy all 8 bytes from received data to working buffer
MEMCPY(ADR(workBuffer[0]), ADR(rxData[0]), 8);

Copy a subset of CAN data

VAR
    canFrame : ARRAY[0..7] OF BYTE;
    payload : ARRAY[0..3] OF BYTE;
END_VAR;

// Copy bytes 4-7 from CAN frame into a separate payload buffer
MEMCPY(ADR(payload[0]), ADR(canFrame[4]), 4);

Building a CAN transmit frame

VAR
    txData : ARRAY[0..7] OF BYTE;
    headerBytes : ARRAY[0..2] OF BYTE;
END_VAR;

// Clear the frame
MEMSET(ADR(txData[0]), 0, 8);

// Copy 3-byte header into the start of the frame
MEMCPY(ADR(txData[0]), ADR(headerBytes[0]), 3);
Note: MEMCPY does not handle overlapping source and destination regions. If the buffers overlap, the results are undefined. For most recipe use cases (separate CAN buffers), this is not a concern.

printf (Debug Only)

Outputs formatted debug text. This function is only available in test and development mode - it has no effect in production firmware running on actual hardware.

FUNCTION printf(format : POINTER TO BYTE, ...) : DINT;

Usage

VAR_SIGNAL
    HW : HARDWARE;
END_VAR;

VAR
    canData : ARRAY[0..7] OF BYTE;
END_VAR;

HW();
// Print a simple message
printf("Recipe started\n");

// Print a variable value
printf("Voltage: %d mV\n", HW.SUPPLY_VOLTAGE);

// Print hex values (useful for CAN debugging)
printf("CAN data: %02X %02X %02X\n", canData[0], canData[1], canData[2]);

Common Format Specifiers

Specifier Output Example
%d Signed decimal integer printf("%d", -42);-42
%u Unsigned decimal integer printf("%u", 255);255
%X Uppercase hexadecimal printf("%X", 255);FF
%02X Hex, zero-padded to 2 digits printf("%02X", 10);0A
\n Newline Line break in output
Warning: printf is not available in production firmware. Do not rely on it for any runtime behavior. It is strictly a development and debugging aid. Calls to printf in production recipes are silently ignored.

{$INSTANCE_ID}

The {$INSTANCE_ID} compiler directive returns a unique integer identifier for the current function block instance. This is a compile-time feature - each instance of a function block receives a different ID automatically.

VAR
    id : DINT;
END_VAR;

id := {$INSTANCE_ID};

Purpose

{$INSTANCE_ID} is primarily used inside library function blocks that need to distinguish between their own instances. For example, if a function block registers itself with a system service, it needs a unique identifier to receive callbacks for the correct instance:

FUNCTION_BLOCK MY_READER
VAR
    myId : DINT;
    initialized : BOOL := FALSE;
END_VAR;

IF NOT initialized THEN
    myId := {$INSTANCE_ID};
    // Register this specific instance with the system
    registerCallback(myId);
    initialized := TRUE;
END_IF;
Note: This example defines a function block and uses {$INSTANCE_ID} and registerCallback which are only available in library context. It cannot compile standalone.

When the recipe declares multiple instances of MY_READER, each one gets a different {$INSTANCE_ID} value:

VAR
    reader1 : MY_READER;   // {$INSTANCE_ID} = 1 (for example)
    reader2 : MY_READER;   // {$INSTANCE_ID} = 2
    reader3 : MY_READER;   // {$INSTANCE_ID} = 3
END_VAR;
Note: {$INSTANCE_ID} is a compile-time directive, not a runtime function. The IDs are assigned sequentially by the compiler and are stable across compilations of the same source code. You will typically only encounter this in library code - most recipe authors never need to use it directly.

Practical Patterns

CAN Response Timeout

Use GET_SYSTEM_TIME to implement a timeout for CAN request/response cycles:

VAR
    requestSentTime : UDINT;
    waitingForResponse : BOOL := FALSE;
    responseTimeout : BOOL := FALSE;
    shouldSendRequest : BOOL;
    responseReceived : BOOL;
END_VAR;

// Send a CAN request
IF shouldSendRequest AND NOT waitingForResponse THEN
    // ... send the CAN frame ...
    requestSentTime := GET_SYSTEM_TIME();
    waitingForResponse := TRUE;
    responseTimeout := FALSE;
END_IF;

// Check for timeout (500ms)
IF waitingForResponse THEN
    IF responseReceived THEN
        waitingForResponse := FALSE;
        // Process the response
    ELSIF (GET_SYSTEM_TIME() - requestSentTime) > 500 THEN
        waitingForResponse := FALSE;
        responseTimeout := TRUE;
        // Handle timeout - retry or report error
    END_IF;
END_IF;

Periodic Action

Execute an action at a fixed interval, regardless of scan cycle time:

VAR
    lastActionTime : UDINT := 0;
    INTERVAL : UDINT := 1000;   // 1 second
END_VAR;

IF (GET_SYSTEM_TIME() - lastActionTime) >= INTERVAL THEN
    lastActionTime := GET_SYSTEM_TIME();

    // Perform periodic action every 1 second
    // e.g., send a status CAN message
END_IF;

Buffer Initialization

Use MEMSET to prepare CAN transmit buffers before populating specific bytes:

VAR
    txData : ARRAY[0..7] OF BYTE;
END_VAR;

// Start with a clean buffer
MEMSET(ADR(txData[0]), 0, 8);

// Set only the bytes you need
txData[0] := 0x02;      // Service ID
txData[1] := 0x10;      // Sub-function
txData[2] := 0x01;      // Parameter

// Remaining bytes are guaranteed to be 0x00