Outputs & PWM
Outputs are how your recipe controls the physical world. Each OUTPUT declared
in VAR_OUTPUT maps to a PWM channel on the connected PowerUnit. Through PWM
(Pulse Width Modulation), you control LED brightness, relay switching, and analog signal
levels. Outputs also serve as the data link between the recipe and the XBB mobile app -
the app displays each output’s current value, name, and status.
OUTPUT - PWM Output
The OUTPUT function block generates a PWM signal. It is also aliased as
PWM100, PWM1000, INFO_ONOFF,
INFO_VALUE, and INFO_GRAPH - the aliases control how the
output is displayed in the app but the underlying behavior is identical.
Parameters
| Parameter | Type | Direction | Description |
|---|---|---|---|
VALUE |
INT | Input | PWM on-time in microseconds (0 to PERIOD) |
PERIOD |
INT | Input | PWM period in microseconds |
USED |
BYTE | Output | TRUE if this output is connected to a physical PowerUnit channel |
ID |
BYTE | Output | Auto-assigned sequential ID (used internally) |
Basic Example
VAR_OUTPUT
HELLJUS ["Helljus"] : OUTPUT;
HALVLJUS ["Halvljus"] : OUTPUT;
END_VAR;
// Full brightness (100% duty cycle at 1kHz)
HELLJUS(VALUE := 1000, PERIOD := 1000);
// Half brightness (50% duty cycle at 1kHz)
HALVLJUS(VALUE := 500, PERIOD := 1000);
Calling OUTPUT Without PERIOD
If you call OUTPUT without specifying PERIOD, the firmware
automatically converts any non-zero VALUE to 100% on.
Specifically, the device normalizes the value to 1000 and sets PERIOD to 1000 internally.
VAR_OUTPUT
EXTRALJUS ["Extraljus"] : OUTPUT;
END_VAR;
// These are all equivalent - 100% on:
EXTRALJUS(VALUE := 1); // No PERIOD: firmware converts to full on
EXTRALJUS(VALUE := 2); // No PERIOD: any non-zero = full on
EXTRALJUS(VALUE := TRUE); // No PERIOD: TRUE (=1) = full on
EXTRALJUS(VALUE := 1000, PERIOD := 1000); // Explicit: 100% duty cycle
// This turns off the output:
EXTRALJUS(VALUE := 0); // Zero = off (with or without PERIOD)
Without PERIOD, you lose all PWM control. The output is either fully on
(any VALUE > 0) or off (VALUE = 0). If you need dimming, variable brightness,
or specific PWM frequencies, you must specify PERIOD.
PWM Timing Explained
The PERIOD parameter sets the PWM period in microseconds.
The VALUE parameter sets the on-time within each period. The PWM frequency
and duty cycle are calculated as:
Frequency (Hz) = 1,000,000 / PERIOD
Duty cycle (%) = VALUE / PERIOD * 100
Period and Frequency Examples
| PERIOD (us) | Frequency | Notes |
|---|---|---|
| 1000 | 1 kHz | Standard for LED dimming - no visible flicker |
| 5000 | 200 Hz | Lower frequency, suitable for relays and motors |
| 10000 | 100 Hz | Minimum for flicker-free LED control |
| 20000 | 50 Hz | Servo control (standard RC servo signal) |
Common PWM Settings
| Use Case | PERIOD | VALUE | Duty Cycle | Frequency |
|---|---|---|---|---|
| Full on | 1000 | 1000 | 100% | 1 kHz |
| Half brightness | 1000 | 500 | 50% | 1 kHz |
| Position light (5%) | 1000 | 50 | 5% | 1 kHz |
| Dim indicator (1%) | 1000 | 10 | 1% | 1 kHz |
| Off | 1000 | 0 | 0% | 1 kHz |
VALUE equals PERIOD, the output is
continuously on (100% duty cycle). When VALUE is 0, the output is continuously
off. Values between 0 and PERIOD produce a proportional PWM signal.
Boolean Signals to OUTPUT
Boolean signals (BOOL) have the value 1 (TRUE) or 0
(FALSE). To drive an output fully on or fully off from a boolean, multiply by the period:
VAR
SIGNAL_TANDNING : BOOL;
END_VAR;
VAR_OUTPUT
IGNITION ["Tandning"] : OUTPUT;
END_VAR;
// Boolean * PERIOD: TRUE = full on (1000), FALSE = off (0)
IGNITION(VALUE := SIGNAL_TANDNING * 1000, PERIOD := 1000);
This technique works because TRUE * 1000 = 1000 (100% duty) and
FALSE * 1000 = 0 (0% duty). It is more compact than an
IF/ELSE block:
VAR
SIGNAL_TANDNING : BOOL;
END_VAR;
VAR_OUTPUT
IGNITION ["Tandning"] : OUTPUT;
END_VAR;
// Equivalent but more verbose - avoid this
IF SIGNAL_TANDNING THEN
IGNITION(VALUE := 1000, PERIOD := 1000);
ELSE
IGNITION(VALUE := 0, PERIOD := 1000);
END_IF;
Common Recipe Outputs
Most recipes include a set of standard system outputs that provide status information to the app and to other function blocks. Here is the typical set:
VAR_OUTPUT
// System status outputs
SYSTEM_ACTIVE ["System Active"] : OUTPUT;
CAN_CONTROLLER ["CAN Controller"] : OUTPUT;
VOLTAGE ["Voltage"] : INFO_VALUE;
LOW_BATTERY ["Low Battery"] : INFO_ONOFF;
ANALOG_IN0 ["Analog Input"] : INFO_VALUE;
// Signal outputs (vehicle-specific)
TANDNING ["Tandning"] : OUTPUT;
HELLJUS ["Helljus"] : OUTPUT;
HALVLJUS ["Halvljus"] : OUTPUT;
BLINKERS_V ["Blinkers Vanster"] : OUTPUT;
BLINKERS_H ["Blinkers Hoger"] : OUTPUT;
BAKLJUS ["Bakljus"] : OUTPUT;
END_VAR;
VAR_SIGNAL
HW : HARDWARE;
END_VAR;
VAR
SIGNAL_TANDNING : BOOL;
SIGNAL_HELLJUS : BOOL;
SIGNAL_HALVLJUS : BOOL;
SIGNAL_BLINKERS_V : BOOL;
SIGNAL_BLINKERS_H : BOOL;
SIGNAL_BAKLJUS : BOOL;
canReady : BOOL;
END_VAR;
HW();
// System outputs
SYSTEM_ACTIVE(VALUE := 1000, PERIOD := 1000); // Always on
CAN_CONTROLLER(VALUE := canReady * 1000, PERIOD := 1000); // CAN status
VOLTAGE(VALUE := TRUNC(HW.SUPPLY_VOLTAGE, INT), PERIOD := 1000); // Battery voltage
LOW_BATTERY(VALUE := (HW.SUPPLY_VOLTAGE < 11000) * 1000, PERIOD := 1000); // Warning < 11.0V
ANALOG_IN0(VALUE := TRUNC(HW.ANALOG_IN0, INT), PERIOD := 1000); // Analog input
// Vehicle signal outputs
TANDNING(VALUE := SIGNAL_TANDNING * 1000, PERIOD := 1000);
HELLJUS(VALUE := SIGNAL_HELLJUS * 1000, PERIOD := 1000);
HALVLJUS(VALUE := SIGNAL_HALVLJUS * 1000, PERIOD := 1000);
BLINKERS_V(VALUE := SIGNAL_BLINKERS_V * 1000, PERIOD := 1000);
BLINKERS_H(VALUE := SIGNAL_BLINKERS_H * 1000, PERIOD := 1000);
BAKLJUS(VALUE := SIGNAL_BAKLJUS * 1000, PERIOD := 1000);
App Display Limitations
App version 1.6.13+: OUTPUT and INFO_VALUE display values as INT (0–65535), so larger values are shown correctly.
Older app versions (< 1.6.13): values were displayed as BYTE (0–255).
If your users may have older app versions, keep display values in the 0–255 range
or use SETTING_VALUE for guaranteed full-range display.
VAR
currentBrightness : INT;
END_VAR;
VAR_OUTPUT
// This will show correctly (0-255 range)
BRIGHTNESS ["Ljusstyrka"] : INFO_VALUE;
// This drives a physical output (PWM) - app shows 0-255
LED_OUT ["LED"] : OUTPUT;
END_VAR;
// INFO_VALUE for display: shows the actual number
BRIGHTNESS(VALUE := currentBrightness, PERIOD := 255);
// OUTPUT for PWM: controls hardware, app shows 0-255
LED_OUT(VALUE := currentBrightness * 4, PERIOD := 1000);
Checking if an Output is Connected
The USED output tells you whether this output channel is physically connected
to a PowerUnit. This is useful for conditional logic - you can skip expensive CAN
requests for signals that have no physical output connected:
VAR_SIGNAL
HW : HARDWARE;
END_VAR;
VAR_OUTPUT
HELLJUS ["Helljus"] : OUTPUT;
END_VAR;
VAR
ENABLE_HELLJUS : BOOL := FALSE;
END_VAR;
HW();
IF HELLJUS.USED OR HW.HOST_CONNECTED THEN
// Only read the high beam DID if:
// - The output is physically connected to a PowerUnit channel, OR
// - The app is connected (user wants to see the value)
ENABLE_HELLJUS := TRUE;
END_IF;
HELLJUS(VALUE := ENABLE_HELLJUS * 1000, PERIOD := 1000);
USED is an optimization. On simple recipes with
few outputs, you can skip this check and always read all signals. But on complex recipes
with many DID requests, gating reads behind USED reduces CAN bus load and
improves performance.
Output Types Summary
All output types use the same underlying OUTPUT function block. The type name
controls how the output appears in the XBB mobile app:
| Type | App Display | Typical Use |
|---|---|---|
OUTPUT |
Standard PWM indicator with on/off and percentage | Physical outputs (LEDs, relays, motors) |
PWM100 |
PWM at 100 Hz | Lower-frequency PWM applications |
PWM1000 |
PWM at 1000 Hz | Standard LED dimming frequency |
INFO_ONOFF |
On/Off toggle indicator | Status flags (ignition, door, etc.) |
INFO_VALUE |
Numeric value display | Sensor readings (voltage, temperature, speed) |
INFO_GRAPH |
Live graph/chart | Trending data (RPM over time, voltage curve) |
Type Selection Examples
VAR_SIGNAL
HW : HARDWARE;
END_VAR;
VAR
brightness : INT;
SIGNAL_TANDNING : BOOL;
rpmValue : INT;
END_VAR;
VAR_OUTPUT
// Physical PWM output for LED strip
LED_STRIP ["LED Strip"] : OUTPUT;
// Show ignition status as on/off toggle
IGN_STATUS ["Tandning"] : INFO_ONOFF;
// Show battery voltage as a number
BATT_VOLTAGE ["Batteri (V)"] : INFO_VALUE;
// Show RPM as a live graph
ENGINE_RPM ["Varvtal"] : INFO_GRAPH;
END_VAR;
HW();
// Physical output: PWM dimming
LED_STRIP(VALUE := brightness, PERIOD := 1000);
// Info on/off: boolean * period
IGN_STATUS(VALUE := SIGNAL_TANDNING * 1000, PERIOD := 1000);
// Info value: direct value
BATT_VOLTAGE(VALUE := TRUNC(HW.SUPPLY_VOLTAGE, INT), PERIOD := 1000);
// Info graph: direct value (app draws the graph over time)
ENGINE_RPM(VALUE := rpmValue, PERIOD := 10000);
Dimming and Fade Patterns
Variable Brightness from CAN Signal
Map a CAN byte value (0–255) to PWM duty cycle:
VAR
dimLevel : BYTE; // From CAN data (0-255)
END_VAR;
VAR_OUTPUT
INTERIOR_LIGHT ["Innerbelysning"] : OUTPUT;
END_VAR;
// Scale 0-255 to 0-1000 for PWM
INTERIOR_LIGHT(VALUE := dimLevel * 4, PERIOD := 1000);
Two-Level Output (Position + Full)
A common pattern for vehicle lights: dim for position/parking, full for active:
VAR
SIGNAL_BAKLJUS : BOOL; // Tail light signal
SIGNAL_BROMS : BOOL; // Brake signal
outputValue : INT;
END_VAR;
VAR_OUTPUT
TAIL_LIGHT ["Bakljus"] : OUTPUT;
END_VAR;
// Brake = 100%, tail light = 20%, off = 0%
IF SIGNAL_BROMS THEN
outputValue := 1000; // Full brightness
ELSIF SIGNAL_BAKLJUS THEN
outputValue := 200; // 20% for position light
ELSE
outputValue := 0;
END_IF;
TAIL_LIGHT(VALUE := outputValue, PERIOD := 1000);
VALUE by a small step each scan
cycle. Combined with the scan time (typically 1–10ms), this produces smooth
transitions visible to the eye.