Structured Text (ST)
The panel executes logic written in IEC 61131-3 Structured Text. The editor compiles the source into compact bytecode at your desk; the pack carries the bytecode and the panel runs it in the PLC cycle — uploading a new program never reflashes the firmware, exactly like uploading new screens.
The FBD blocks and the ST program run in the same task, in this order: FBD first, then ST. A tag the FBD wrote in this cycle is already visible to the ST body (and the other way round next cycle) — the same "written this cycle reads live" rule S7 programmers expect.
The subset
POUs: PROGRAM (one per project), FUNCTION_BLOCK (your own
blocks with instance state), FUNCTION (stateless, returns through
its own name).
Sections: VAR, VAR_INPUT, VAR_OUTPUT, VAR_TEMP … END_VAR.
A name that is not declared in the POU resolves against the project's
tag lists (VAR/RVAR) — tags behave as globals, case-insensitively.
Types: BOOL, INT/DINT (32-bit), REAL (float), TIME
(milliseconds), STRING(N) (bounded text, 1..250 bytes, default 32).
INT widens to REAL silently; REAL never narrows silently —
write TRUNC(x) or REAL_TO_INT(x). INT and TIME convert freely
(a TIME is milliseconds).
Statements: assignment :=, IF/ELSIF/ELSE, CASE (values,
lists, ranges 4..6), FOR/TO/BY, WHILE, REPEAT/UNTIL, EXIT,
RETURN. FB invocation with named parameters:
t1(IN := start, PT := T#500ms);
lamp := t1.Q;
Literals: TRUE/FALSE, 123, 16#FF, 2#1010, 1.5,
T#1m30s500ms, typed INT#5. Comments (* ... *) and // to eol.
Time: NOW() returns the cycle's monotonic milliseconds. All four
comparisons work on TIME (IF t1.ET > T#500ms THEN ...), and they are
unsigned — a machine that is never switched off passes 2^31 ms
(24.8 days), where a signed compare would call its uptime negative.
Never count cycles: the cycle period may jitter, milliseconds do not.
A duration is written T#… or TIME#… with segments in d h m s ms,
most significant first, and underscores as separators — T#1s500ms,
TIME#1h_30m, T#25h (the leading segment may overflow its unit).
A TIME tag is the same value with a face: the panel shows and the
keypad edits it in the tag's unit (s, ms, min), so an operator can
set a machine's delay on the screen and the program takes it straight as
t1(PT := delay). See Tagy.
Standard functions: ABS MIN MAX LIMIT SEL TRUNC, the
conversions above, and the string set below.
Your own function block
The classic on-delay written from scratch — if you can read this, you can write ctrl32 blocks:
FUNCTION_BLOCK MY_TON
VAR_INPUT IN : BOOL; PT : TIME; END_VAR
VAR_OUTPUT Q : BOOL; ET : TIME; END_VAR
VAR running : BOOL; t0 : TIME; END_VAR
IF IN AND NOT running THEN t0 := NOW(); running := TRUE; END_IF
IF NOT IN THEN running := FALSE; Q := FALSE; ET := T#0s; END_IF
IF running THEN
ET := NOW() - t0;
IF ET >= PT THEN Q := TRUE; ET := PT; END_IF
END_IF
END_FUNCTION_BLOCK
Instances are allocated at compile time — the editor knows the RAM consumption before the upload and rejects a program over the limits (32 kB bytecode, 8 kB instance data) at your desk, not on the machine.
What the compiler refuses (on purpose)
- recursion and call chains deeper than 8 — checked at compile time
- two declarations differing only in case — identifiers are case-insensitive per the norm
REALinto an integer withoutTRUNC/REAL_TO_INT- pointers, OOP, 64-bit types,
WSTRING— not in this version; the message names the reason, never a cryptic parse error
Runtime safety
A broken program (division by zero, an endless loop) FAULTS: it stops with a diagnostic entry and the panel keeps running — HMI, WEBview, communication and the other logic are untouched. The fault stays until a new pack or a restart.
Strings — bounded STRING(N)
A string variable declares its capacity: s : STRING(32);. The
buffer lives in the statically proven instance area — no heap, ever.
Assignments and CONCAT clamp to the destination capacity instead
of overflowing; comparing with = / <> compares content. The parser
toolkit of the norm is in:
cmd := CONCAT('SET:', INT_TO_STRING(setp));
at := FIND(cmd, ':'); (* 1-based, 0 = not found *)
verb := LEFT(cmd, at - 1);
num := MID(cmd, LEN(cmd) - at, at + 1); (* MID(IN, L, P) *)
val := STRING_TO_INT(num);
LEN LEFT RIGHT MID FIND CONCAT STRING_TO_INT INT_TO_STRING — plus
literals with the norm's $ escapes ($$, $', $L, $N, $R,
$T, $P). This is the barcode-reader / SMS-command motif; the
ingress blocks that feed real reader and modem text into these
buffers arrive with the modem service.
A FUNCTION may take STRING(N) parameters and return a STRING
(the return capacity is the norm default, 32).
Messages (string literals in SMS_SEND)
The literal in SMS_SEND('text', n) is stored once in the pack
(localizable) and the running cycle handles only its number — same
queue and hook as the FBD block.
Receiving SMS commands (SMS_RECV)
SMS_RECV(from, text) pops one received SMS into two STRING
variables and returns BOOL — call it each cycle and parse:
IF SMS_RECV(from, txt) THEN
IF LEFT(txt, 4) = 'SET:' THEN
setp := STRING_TO_INT(RIGHT(txt, LEN(txt) - 4));
END_IF
END_IF
Only texts from the project's configured numbers ever reach the program — everything else is dropped (and counted in diagnostics) before the logic sees it. An SMS is a command: the program decides what it may change. Without a modem (PC simulation) the call simply returns FALSE.
The serial line port (SERIAL_RECV / SERIAL_SEND)
A project may claim one spare UART as a universal line port
(serial: — pins, baud, terminator, max line length). Barcode
readers, scales, label printers — anything that talks in
terminator-ended ASCII lines:
IF SERIAL_RECV(line) THEN (* one framed line per call *)
pieces := STRING_TO_INT(line);
SERIAL_SEND(CONCAT('ACK ', line)); (* terminator appended *)
END_IF
SERIAL_RECV wants a STRING variable; SERIAL_SEND takes any
STRING expression and returns FALSE while the previous line is
still leaving (one line in flight — the cycle never blocks on the
UART). Overlong lines and queue overflows are dropped and counted,
never truncated into plausible-looking garbage.
One budget note: the panel has three UART controllers — console, RS485 and the modem/GPS already claim them all. The line port runs where one is free; a project combining RS485, a modem AND a line port does not fit on the S3 generation (the diagnostics say so plainly).
Notes the norm-lawyers will want
REPEAT body UNTIL condition END_REPEAT— no semicolon after the condition (the norm's syntax; CoDeSys tolerates one, we hint it)- comments do not nest in this version
AND/ORevaluate both sides (no short-circuit) — keep side effects out of conditions