Modbus TCP¶
Good fit vs bad fit¶
| Good fit | Bad fit |
|---|---|
| PLC-to-device register exchange | pub/sub event distribution |
| gateways, drives, legacy equipment | highly dynamic topic-style systems |
| explicit polling and deterministic offsets | loosely structured payloads |
First things to decide¶
- what server endpoint will the runtime talk to?
- what
unit_idis correct for the device or gateway? - where do inputs and outputs start in coil/register space?
- which Modbus functions should be used for the process image?
- should communication faults halt, warn, or degrade gracefully?
Success means the device endpoint, unit_id, function-code profile, coil or
register map, byte order, and fault behavior are written down before runtime
validation starts.
The default profile is conservative and backward compatible: FC04
read_input_registers for %I and FC16 write_multiple_registers for %Q.
When a device map requires a different shape, set input_function explicitly to
read_coils (FC01), read_discrete_inputs (FC02),
read_holding_registers (FC03), or read_input_registers (FC04), and set
output_function explicitly to write_single_coil (FC05),
write_single_register (FC06), write_multiple_coils (FC15), or
write_multiple_registers (FC16). Coil payloads use Modbus bit packing; register
payloads remain big-endian bytes.
Use input_points and output_points when the process image needs explicit
typed points instead of one contiguous raw block. Each point names the
process-image byte offset, optional bit, Modbus address, function, data type
(bool, u16, i16, u32, i32, or f32), optional scale/offset, and
optional Modbus byte_order/word_order. Scaling converts inputs as
engineering = raw * scale + offset; output writes invert that formula before
writing Modbus. Numeric point-map values are stored in the runtime process image
as little-endian bytes after scaling.
At runtime, Modbus TCP exchange is worker-backed. Scan-cycle reads copy the
latest worker snapshot or return the configured on_error policy result, and
scan-cycle writes hand off the latest desired output without waiting for a TCP
round trip. The Modbus worker owns connect/read/write latency, reconnect
backoff, and stale/degraded health reporting through the normal driver status
surface.
Example and commissioning guide¶
This example shows how to wire a minimal PLC project to io.driver = "modbus-tcp".
What you learn¶
- how
%IX/%QXbits map through Modbus register space - why
unit_id,input_start, andoutput_startmust be explicit - how timeout and
on_errorpolicy affect runtime behavior
Files in this folder¶
src/main.st: simple input-to-output logic (DO0 := DI0)src/config.st: task binding plusVAR_CONFIGmapping (P1.DI0/P1.DO0)io.toml: Modbus/TCP backend profileruntime.toml: runtime defaultstrust-lsp.toml: project settings
Step 1: Build the project¶
Why: prove ST compile path is valid before protocol troubleshooting.
cd examples/communication/modbus_tcp
trust-runtime build --project . --sources src
Step 2: Inspect io.toml¶
Why: every field controls a concrete transport or mapping decision.
[io]
driver = "modbus-tcp"
[io.params]
address = "127.0.0.1:1502"
unit_id = 1
input_start = 0
output_start = 0
timeout_ms = 500
on_error = "fault"
Field intent:
address: Modbus server endpoint.unit_id: target slave/unit address.input_start: first register offset read into%Iimage.output_start: first register offset written from%Qimage.timeout_ms: upper bound per exchange.on_error = "fault": fail closed during commissioning.
Step 3: Validate configuration¶
Why: catches schema/mode errors before runtime boot.
trust-runtime validate --project .
Step 4: Run with a local test server¶
Why: isolate mapping/timeout behavior before connecting to plant hardware.
- Start a Modbus test endpoint on
127.0.0.1:1502. - Start runtime:
trust-runtime run --project .
- In another terminal, inspect image:
trust-runtime ctl --project . io-read
Step 5: Harden for production¶
Why: lab defaults often become hidden failure points in production.
- replace loopback endpoint with real server address
- keep
on_error = "fault"until commissioning sign-off - verify safe state outputs match actuator-safe values
Common mistakes¶
- using wrong
unit_idfor gateway/slave topology - shifting
input_start/output_startby one register block - setting
on_error = "warn"too early in bring-up - validating config but not testing real timeout behavior
Common Modbus gotchas¶
- wrong
unit_idbehind a gateway - off-by-one mental model around coil/register blocks
- selecting a coil function for a register map, or a register function for a coil map
- applying Modbus byte/word order to the runtime process image instead of the wire/register layout
- byte/word order mismatches on non-trivial payloads
- accepting a “validate passed” result as proof of runtime connectivity