I/O Binding¶
Address Examples: %IX0.0, %QX0.0, %IW0, %QW0¶
Use %IX0.0 when you need a concrete boolean input example, %QX0.0 for a
boolean output example, %IW0 for a word input example, and %QW0 for a word
output example.
Symbolic variables bind to physical I/O channels through direct addresses,
VAR_CONFIG, or io.toml.
At this point the problem is signal identity, not transport selection.
Keep one binding path authoritative for each signal so diagnostics and operator views describe the same process image.
Guide¶
This guide explains how to map hardware I/O to Structured Text variables using the Web UI
or io.toml and IEC addresses (%IX, %QX, %MX).
Tip: The Web UI supports driver selection, GPIO pin mapping, Modbus/TCP settings, and safe‑state outputs under I/O → I/O configuration (no manual file editing needed).
1) Addressing Basics¶
Use IEC-style addresses in ST:
VAR_GLOBAL
InSignal AT %IX0.0 : BOOL;
OutSignal AT %QX0.0 : BOOL;
END_VAR
%I= input,%Q= output,%M= memoryX= bit address (use for GPIO and discrete I/O)
Marker (%M) address variants:
%MX<byte>.<bit>(bit, BOOL), example:%MX0.7%MB<byte>(byte), example:%MB12%MW<byte>(word), example:%MW50%MD<byte>(double word), example:%MD200%ML<byte>(long word), example:%ML8%M*(wildcard, resolved byVAR_CONFIG)
Runtime cycle semantics for %M bindings:
- Cycle start:
%Mprocess image is read into bound variables. - Cycle end: bound variable values are written back to
%Mprocess image.
2) io.toml Structure (v1)¶
Single-driver form (legacy + still supported):
[io]
driver = "simulated"
params = {}
Multi-driver form (composed drivers, executed in order):
[io]
drivers = [
{ name = "modbus-tcp", params = { address = "192.168.0.10:502", unit_id = 1, input_start = 0, output_start = 0, timeout_ms = 500, on_error = "fault" } },
{ name = "mqtt", params = { broker = "192.168.0.20:1883", topic_in = "line/in", topic_out = "line/out", reconnect_ms = 500, keep_alive_s = 5, allow_insecure_remote = true } }
]
Rule:
- Use either
io.driver+io.paramsorio.drivers(do not mix both in one file).
Optional safe state outputs:
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
If io.toml is missing, the runtime uses system IO config:
- Linux/macOS:
/etc/trust/io.toml - Windows:
C:\\ProgramData\\truST\\io.toml
3) GPIO Example (Raspberry Pi)¶
[io]
driver = "gpio"
[io.params]
backend = "sysfs"
inputs = [
{ address = "%IX0.0", line = 17, debounce_ms = 5 }
]
outputs = [
{ address = "%QX0.0", line = 27, initial = false }
]
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
4) Loopback (Local Testing)¶
[io]
driver = "loopback"
params = {}
This copies outputs to inputs for local testing without hardware.
5) Modbus/TCP Example¶
[io]
driver = "modbus-tcp"
[io.params]
address = "192.168.0.10:502"
unit_id = 1
input_start = 0
output_start = 0
input_function = "read_input_registers"
output_function = "write_multiple_registers"
timeout_ms = 500
on_error = "fault"
input_function is optional and defaults to read_input_registers (FC04).
Supported explicit input functions are read_coils (FC01),
read_discrete_inputs (FC02), read_holding_registers (FC03), and
read_input_registers (FC04). output_function is optional and defaults to
write_multiple_registers (FC16). Supported explicit output functions are
write_single_coil (FC05), write_single_register (FC06),
write_multiple_coils (FC15), and write_multiple_registers (FC16). Coil
payloads use Modbus bit packing with bit 0 in the least-significant bit of the
first process-image byte.
For device maps that are not one contiguous raw block, add explicit point maps:
[[io.params.input_points]]
image_offset = 0
image_bit = 0
address = 10
function = "read_coils"
data_type = "bool"
[[io.params.input_points]]
image_offset = 2
address = 100
function = "read_holding_registers"
data_type = "u32"
scale = 0.1
offset = -40.0
byte_order = "big"
word_order = "little"
[[io.params.output_points]]
image_offset = 8
address = 200
function = "write_single_register"
data_type = "u16"
scale = 1.0
offset = 0.0
Point maps support bool, u16, i16, u32, i32, and f32. scale and
offset convert input raw values as engineering = raw * scale + offset; output
writes invert that formula. byte_order and word_order describe the Modbus
wire/register layout. Numeric process-image bytes remain little-endian so
%IW/%ID bindings read them consistently inside the runtime.
6) MQTT Example¶
[io]
driver = "mqtt"
[io.params]
broker = "192.168.0.20:1883"
topic_in = "line/in"
topic_out = "line/out"
reconnect_ms = 500
keep_alive_s = 5
allow_insecure_remote = true
Without a point map, topic_in payload bytes are copied directly into %I and
%Q bytes are published directly to topic_out. For typed point topics, add
explicit MQTT point maps:
MQTT can also resolve named program-instance variables into typed point maps at
runtime startup. This avoids adding a VAR_CONFIG address solely for MQTT:
[[io.params.mappings]]
tag = "MainInstance.Green"
topic = "traffic/north/green"
direction = "write"
Mapping direction is relative to the PLC. write means PLC to broker, while
read means broker to PLC. Output-only mappings do not subscribe to the raw
default trust/io/in topic. Tag mapping currently supports direct scalar
variables on configured program instances; use explicit input_points or
output_points for nested values, arrays, custom payload settings, or
Sparkplug metric metadata.
[[io.params.input_points]]
topic = "line/in/ready"
image_offset = 0
image_bit = 0
data_type = "bool"
payload_format = "json"
[[io.params.input_points]]
topic = "line/in/temperature"
image_offset = 2
data_type = "i16"
payload_format = "text"
scale = 0.1
offset = -40.0
[[io.params.output_points]]
topic = "line/out/speed"
image_offset = 4
data_type = "u16"
payload_format = "json"
scale = 0.5
offset = 0.0
MQTT point maps support bool, u16, i16, u32, i32, and f32.
payload_format defaults to text and may be text, json, binary_le, or
binary_be. Numeric process-image bytes remain little-endian; binary MQTT
payload endianness describes the MQTT payload only.
For Sparkplug B outbound node metrics, keep typed output_points and add a
Sparkplug profile:
[io.params.sparkplug]
enabled = true
namespace = "spBv1.0"
spec_version = "3.0.0"
group_id = "trust-line"
edge_node_id = "runtime-a"
[[io.params.output_points]]
topic = "legacy/out/speed"
metric_name = "drive/speed"
image_offset = 4
data_type = "u16"
payload_format = "json"
scale = 0.5
offset = 0.0
This profile publishes NBIRTH on connect, configures NDEATH as the MQTT last will, and publishes NDATA from typed output points. It is outbound node metrics only: Sparkplug commands, device-level DBIRTH/DDATA topics, aliases, templates, and store-and-forward are separate future slices.
7) Transport Gating Notes (Critical)¶
EtherCAT hardware transport (non-mock adapter):
- requires build feature
ethercat-wire - supported on unix targets only in this build
adapter = "mock"remains valid for deterministic local/CI validation
OPC UA wire server:
- requires build feature
opcua-wire - if
[runtime.opcua].enabled = truewithoutopcua-wire, runtime startup fails with a feature-disabled error - when enabled, configure either:
allow_anonymous = true(local commissioning only), orallow_anonymous = falseplus bothusernameandpassword
Communication examples with step-by-step commissioning flow:
examples/communication/modbus_tcp/README.mdexamples/communication/mqtt/README.mdexamples/communication/opcua/README.mdexamples/communication/ethercat/README.mdexamples/communication/ethercat_field_validated_es/README.mdexamples/communication/gpio/README.mdexamples/communication/multi_driver/README.md
8) Validate + Inspect¶
EtherCAT backend details (module chain profile, diagnostics, and hardware setup):
docs/guides/ETHERCAT_BACKEND_V1.md.
Validate a project folder:
trust-runtime validate --project <project-folder>
Read current I/O snapshot:
trust-runtime ctl --project <project-folder> io-read
Write output (for testing):
trust-runtime ctl --project <project-folder> io-write %QX0.0 TRUE