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.
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;
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;
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);
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);
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 |
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;
{$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;
{$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