Function Blocks
Function blocks are the backbone of TSharkRex programming. They are stateful, reusable components that encapsulate behavior which persists across scan cycles. Almost everything beyond simple calculations - timers, CAN communication, edge detection, output control, diagnostic requests - is implemented as a function block.
This chapter is one of the most important in this reference. Getting the function block syntax right is essential, and there is a critical difference between functions and function blocks that trips up many developers.
Function Block Definition
A function block is defined using the FUNCTION_BLOCK ... END_FUNCTION_BLOCK construct:
FUNCTION_BLOCK block_name
VAR
// ALL variables in ONE VAR block
// Inputs, outputs, and internal - all together
ENABLE : BOOL; // Input (set by caller)
PT : TIME; // Input (set by caller)
Q : BOOL; // Output (read by caller)
ET : TIME; // Output (read by caller)
internal : DINT; // Internal variable
END_VAR;
// Function block body
IF ENABLE THEN
Q := TRUE;
END_IF;
END_FUNCTION_BLOCK;
Notice the structure:
- A single
VAR ... END_VARblock contains all variables - inputs that callers set, outputs that callers read, and internal state variables. - There is no syntactic distinction between inputs, outputs, and internal variables. The convention is determined by usage and documentation.
- The function block body runs every time the block is called, with access to all variables from the
VARblock. - All variables persist between calls - this is what makes function blocks stateful.
CRITICAL: Function vs. Function Block
FUNCTION_BLOCK is fundamentally different from FUNCTION. They use different variable declaration syntax, and confusing the two will cause compiler errors or incorrect behavior.
Here is the core difference:
FUNCTIONusesVAR_INPUTfor parameters and returns a value via its name.FUNCTION_BLOCKuses a singleVARblock for everything - inputs, outputs, and internal state. While the compiler technically acceptsVAR_INPUTandVAR_OUTPUTinside function blocks, the convention (and all standard libraries) is to use a singleVARblock. Follow this pattern in your own code.
Side-by-Side Comparison
| Feature | FUNCTION | FUNCTION_BLOCK |
|---|---|---|
| Parameters | VAR_INPUT |
VAR (all in one block) |
| State | Stateless (reset each call) | Stateful (persists between calls) |
| Return value | Yes (assign to function name) | No (use output variables) |
| Instantiation | Called directly | Must create an instance |
| Use case | Pure calculations | Timers, triggers, CAN, I/O, diagnostics |
| Multiple copies | N/A (no state to isolate) | Each instance has independent state |
Common Mistake
This is wrong - do not use VAR_INPUT inside a function block:
// WRONG - will cause compiler errors!
FUNCTION_BLOCK MY_BLOCK
VAR_INPUT // ERROR: VAR_INPUT is for FUNCTION, not FUNCTION_BLOCK
ENABLE : BOOL;
END_VAR
VAR_OUTPUT // ERROR: VAR_OUTPUT is not valid in FUNCTION_BLOCK
Q : BOOL;
END_VAR
Q := ENABLE;
END_FUNCTION_BLOCK;
This is correct:
// CORRECT - single VAR block with all variables
FUNCTION_BLOCK MY_BLOCK
VAR
ENABLE : BOOL; // Input (set by caller)
Q : BOOL; // Output (read by caller)
END_VAR;
Q := ENABLE;
END_FUNCTION_BLOCK;
VAR_INPUT / VAR_OUTPUT in function blocks, be aware that TSharkRex differs here. In TSharkRex, function blocks use only VAR. This is a deliberate language design choice.
Instantiation and Usage
Unlike functions, which are called directly, function blocks must be instantiated. You create a named instance of a function block type in a VAR block, then call that instance:
VAR
TMR : TON; // Create an instance of the TON timer function block
start_condition : BOOL;
timerDone : BOOL;
END_VAR;
// Call the instance with named parameters
TMR(IN := start_condition, PT := T#1s);
// Access outputs through the instance
timerDone := TMR.Q;
// Reset the timer
TMR(IN := FALSE);
Each instance is an independent object with its own state. Creating the instance (in the VAR block) allocates the memory for all of the function block’s internal variables. Calling the instance (in the program body) executes the function block’s logic.
Named Parameters
Function blocks use named parameter syntax when called. Each parameter is specified as name := value:
VAR
CANSEND : CAN_TX;
SENDDATA : ARRAY[0..7] OF BYTE;
END_VAR;
CANSEND(ENABLE := TRUE, ID := 0x7DF, EXT := FALSE,
DATALENGTH := 8, DATA := SENDDATA);
You only need to provide parameters you want to change. Parameters not specified retain their previous values (since function blocks are stateful):
// First call - set all parameters
TMR(IN := TRUE, PT := T#5s);
// Later calls - only change what needs updating
TMR(IN := FALSE); // PT remains T#5s from previous call
CAN_TX or DID_EXT.
Accessing Outputs
After calling a function block instance, you access its output variables using dot notation:
VAR
TMR : TON;
timer_done : BOOL;
elapsed : TIME;
END_VAR;
TMR(IN := TRUE, PT := T#2s);
// Read outputs using dot notation
timer_done := TMR.Q; // Has the timer elapsed?
elapsed := TMR.ET; // How much time has passed?
Post-Assignment (=> Operator)
TSharkRex provides a shorthand for reading output values at the time of the function block call using the => (post-assignment) operator:
VAR
TMR : TON;
timer_done : BOOL;
elapsed : TIME;
END_VAR;
// Assign outputs in the same call
TMR(IN := TRUE, PT := T#1s, Q => timer_done, ET => elapsed);
// timer_done and elapsed now contain the latest output values
The => operator copies the output variable’s value into the specified variable after the function block executes. It is syntactic sugar for calling the block and then reading outputs with dot notation, but it keeps the code more compact.
The direction of the arrows indicates data flow:
:=- value flows into the function block (input)=>- value flows out of the function block (output)
Multiple Instances
One of the most powerful aspects of function blocks is that each instance maintains its own independent state. You can create as many instances as you need:
VAR
TMR_WAKE : TON; // Wake-up delay timer
TMR_SLEEP : TON; // Sleep timeout timer
TMR_POLL : TON; // Polling interval timer
condition1 : BOOL;
condition2 : BOOL;
END_VAR;
// Each timer runs independently with its own state
TMR_WAKE(IN := condition1, PT := T#5s);
TMR_SLEEP(IN := condition2, PT := T#30s);
TMR_POLL(IN := NOT TMR_POLL.Q, PT := T#100ms);
In this example, three TON timer instances run simultaneously. Each has its own IN, PT, Q, and ET variables that do not interfere with the others.
The last timer, TMR_POLL, demonstrates a common pattern: a self-resetting timer. When TMR_POLL.Q becomes TRUE (timer elapsed), the input IN becomes FALSE (because NOT TRUE = FALSE), which resets the timer. On the next cycle, Q is FALSE again, so IN becomes TRUE, restarting the timer. This creates a repeating pulse every 100ms.
Instance Naming Conventions
Use descriptive names for function block instances that indicate their purpose:
VAR
// Good - clear purpose
CAN_SKICKA_DID : CAN_TX;
CAN_TA_EMOT_SVAR : CAN_RX;
TMR_UPPSTART : TON;
TRIG_BLINKERS : R_TRIG;
// Avoid - ambiguous
TX1 : CAN_TX;
RX1 : CAN_RX;
T1 : TON;
END_VAR;
Stateful Behavior
The key feature of function blocks is that their variables persist between calls. This enables time-dependent and history-dependent behavior that is impossible with plain functions.
Consider a simple toggle function block:
FUNCTION_BLOCK TOGGLE
VAR
TRIG : BOOL; // Input: trigger signal
STATE : BOOL; // Output: current toggle state
prev_trig : BOOL; // Internal: previous trigger value
END_VAR;
// Detect rising edge of trigger
IF TRIG AND NOT prev_trig THEN
STATE := NOT STATE; // Toggle the state
END_IF;
prev_trig := TRIG; // Remember for next cycle
END_FUNCTION_BLOCK;
The prev_trig variable remembers the trigger value from the previous scan cycle, enabling edge detection. The STATE variable maintains the current toggle position. This kind of behavior is only possible because function block variables persist.
Example: Pulse Generator
Here is a complete custom function block that generates a repeating on/off pulse:
FUNCTION_BLOCK PULS_GENERATOR
VAR
ENABLE : BOOL; // Input: enable the pulse
ON_TID : TIME; // Input: on-duration
AV_TID : TIME; // Input: off-duration
UTGANG : BOOL; // Output: current pulse state
tmr : TON; // Internal: timer instance
state : BOOL; // Internal: current phase
END_VAR;
IF NOT ENABLE THEN
UTGANG := FALSE;
state := FALSE;
tmr(IN := FALSE);
ELSE
IF state THEN
// ON phase
tmr(IN := TRUE, PT := ON_TID);
UTGANG := TRUE;
IF tmr.Q THEN
state := FALSE;
tmr(IN := FALSE);
END_IF;
ELSE
// OFF phase
tmr(IN := TRUE, PT := AV_TID);
UTGANG := FALSE;
IF tmr.Q THEN
state := TRUE;
tmr(IN := FALSE);
END_IF;
END_IF;
END_IF;
END_FUNCTION_BLOCK;
Usage:
VAR
BLINK : PULS_GENERATOR;
LAMPA : OUTPUT;
END_VAR;
BLINK(ENABLE := TRUE, ON_TID := T#500ms, AV_TID := T#500ms);
IF BLINK.UTGANG THEN
LAMPA(VALUE := 1000);
END_IF;
tmr : TON). Function blocks can be nested - a function block can use instances of other function blocks as internal components.
Function Blocks Belong in Libraries
Like functions, function blocks should be defined in libraries, not in your main recipe code. This is especially important for function blocks because:
- They tend to be more complex and longer than functions.
- They are frequently reused across multiple recipes.
- The standard libraries provide the most commonly needed function blocks.
- Keeping them in libraries keeps your recipe code focused on application logic.
Built-in Function Blocks
TSharkRex provides a rich set of built-in function blocks through its standard libraries. These cover all the fundamental operations needed for automotive automation:
CAN Communication
| Block | Description | Chapter |
|---|---|---|
CAN_TX | Transmit a CAN frame | 14 |
CAN_RX | Receive a specific CAN frame by ID | 14 |
CAN_RX_ALL | Receive any CAN frame (promiscuous) | 14 |
CAN_MODE | Set CAN bus mode (normal, listen-only, etc.) | 14 |
CAN_FILTER | Configure CAN hardware filter | 14 |
CAN_MASK | Configure CAN hardware mask | 14 |
Timers
| Block | Description | Chapter |
|---|---|---|
TON | Timer On-delay - output turns on after a delay | 15 |
TOF | Timer Off-delay - output stays on after input drops | 15 |
Edge Detection
| Block | Description | Chapter |
|---|---|---|
R_TRIG | Rising edge trigger - pulse on FALSE-to-TRUE transition | 16 |
F_TRIG | Falling edge trigger - pulse on TRUE-to-FALSE transition | 16 |
Output Control
| Block | Description | Chapter |
|---|---|---|
OUTPUT | Control a physical output (0–1000 range) | 17 |
PWM100 | PWM output at 100 Hz | 17 |
PWM1000 | PWM output at 1000 Hz | 17 |
INFO_VALUE | Display a numeric value in the app | 17 |
INFO_ONOFF | Display an on/off status in the app | 17 |
Settings
| Block | Description | Chapter |
|---|---|---|
SETTING_* | User-configurable settings (various types) | 18 |
Hardware and Diagnostics
| Block | Description | Chapter |
|---|---|---|
HARDWARE | Access hardware features (LEDs, buttons, voltage) | 19 |
DID_EXT | UDS Diagnostic communication (Read Data By Identifier) | 22 |
Each of these function blocks is covered in detail in its respective chapter. The chapters that follow will progressively introduce them in context, starting with CAN communication.
Summary
| Aspect | Detail |
|---|---|
| Declaration | FUNCTION_BLOCK name ... END_FUNCTION_BLOCK |
| Variables | Single VAR ... END_VAR block (no VAR_INPUT/VAR_OUTPUT) |
| Instantiation | instance : BLOCK_TYPE; in a VAR block |
| Calling | instance(PARAM := value, ...); |
| Reading outputs | instance.OUTPUT or OUTPUT => variable |
| State | Stateful - all variables persist between calls |
| Return value | None - use output variables instead |
| Best practice | Define in libraries, instantiate in recipes |