Documentation
20 items
Scope
dokumentaciaOnline monitoring samples ten times a second. That is right for a human reading a value and useless for anything short: a pulse one cycle long falls between two samples by construction, and the fault you are chasing is usually exactly that.
The scope is the answer. The panel records up to 8 values every cycle into a ring buffer and stops on a trigger; the editor arms it and draws what came back.
Open it from Online → Scope.
What it records
| Signals | 8 |
| Samples | 256 rows — 2.56 s at a 10 ms cycle |
| Sampling | every cycle, in the panel, never on the PC |
| Cost while recording | 6 µs per cycle measured on a classic ESP32 at 240 MHz — 0.06 % of a 10 ms period |
| Cost while disarmed | one comparison — the sampler is not entered at all |
The buffer is fixed at 8 × 256 on purpose. A buffer whose size depends on the project is a buffer whose cost nobody can predict, and this one has to be free when it is off.
Using it
- Pick the signals. The list offers tags and, when the editor and the
panel run the same program, the wires between blocks and block members
(
t1.ET,c1.CV) as well. Up to eight; the counter above the list says how many are left. -
Set the trigger.
Mode Fires when now immediately — fill the buffer once and stop rising edge the source crosses the level upwards falling edge … downwards level the source is at or above the level A BOOL source needs no level and is not asked for one.
-
Choose how much history to keep. The pre slider says how much of the ring is kept from before the trigger. This is the point of the whole feature: what happened in the 300 ms before the fault is the part nobody can arm for by hand.
The number is exact and it is a promise: with 25 % of a 256-row ring the trigger row is row 64, with 64 rows of history behind it — not 63, and never zero because the event happened early. The panel holds the trigger off until that history exists. 4. Arm. The panel records; the editor shows waiting for the trigger, then recording after it, then complete — at which point the rows are pulled automatically and drawn.
The signal list, the trigger and the pre-trigger setting are remembered between sessions. They are stored by name, so they are re-resolved against whatever program is loaded now; a signal that no longer exists is dropped and the page says so. They live in the editor's preferences, not in the project — which signals you happen to be looking at is not a property of the machine.
A fault that happens once an hour
Tick repeat and the scope arms itself again after every capture. The re-arm waits until the rows have been read, so a capture is never overwritten before it has been seen. The counter under the checkbox says how many captures this session has produced.
The panel also keeps its recording across an editor restart or a pulled cable: on reconnecting, the editor asks whether one is running and picks it up instead of showing "off" over a live trace.
Reading the picture
- One lane per signal, stacked like a logic analyser. Eight curves on one axis is a picture nobody can read.
- Time is measured from the trigger. Zero is where it fired, everything to the left of it is the history you asked to keep.
- The milliseconds come from the panel's own cycle period, not from a number typed here — a trace labelled with the wrong period is a measurement that lies quietly.
- A BOOL is drawn as a square wave with a filled body. A number gets its own auto-scaled lane, with the range printed under its name; a fixed scale would hide exactly the small movement you opened the scope for.
- Move the mouse over the plot and every lane prints its value at that instant, next to its name. The tooltip adds the row and the time relative to the trigger.
- Wheel zooms, drag pans.
Measuring between two points
Hovering reads one instant; the question an electrician actually asks is how long from this edge to that one.
- Left click plants cursor A, right click plants B. No modifier keys — a person holding a probe in the other hand has one hand for the mouse.
- The span between them is shaded, and the corner reads
A -120 ms B +80 ms d 200 ms (5.00 Hz). The frequency is the same measurement asked the other way round, which is what you want when the two cursors sit on consecutive pulses. - While A is planted, the hover tooltip also gives the time from A and, for numbers, the difference in value — a ramp's slope without doing arithmetic on paper.
- Clear A/B removes them; a new capture clears them by itself, because a cursor belongs to the recording it was placed in.
Getting it out
Export CSV writes one row per sample: the time in milliseconds first
(relative to the trigger, exactly as the picture reads), then one
column per signal, and a T in the last column on the trigger row. That is the file you send a customer, or open in a spreadsheet next
to a measurement from something else.
When it will not arm
| The page says | What it means |
|---|---|
| not online — the scope records IN the panel | there is no connection; this is not a PC-side recorder |
| the panel runs a different program | the same rule as everywhere: values travel by index, and under a different program they would be values under the wrong names |
| the Arm button is grey | no signals picked yet, or the editor has no licence — talking to a panel is the paid part |
If a recording comes back empty, the trigger never fired — check the source and the level, or use now to see what the signals are doing at all.
An edge means a transition
Arming a rising edge on a signal that is already above the level does not fire immediately: the trace waits for the signal to go below and come back. Otherwise every arming would trigger on its own first sample and hand back a recording with no history — which is what it used to do before this was measured on a bench.
Forcing values
dokumentaciaEverything else in the editor shows a machine. This one changes it: a valve opens because somebody typed a number on a laptop. Forcing is a commissioning aid — for checking wiring, exercising an output before the sensor exists, or proving an interlock — and it is not a way to run a machine.
Open it from Online → Force.
What it does
A forced tag is held by the panel, not by the editor. The value is written into the tag on both sides of the program: before the logic runs, so the logic reads it, and after, so an output the logic just wrote is overridden. That is the standard scan model — anything else would make forcing an input work and forcing an output quietly not.
The program cannot win against a force. That is the point, and it is why the rules below matter.
How long it lasts
| Released when you release it | yes |
| Released when the editor closes or the cable is pulled | no |
| Released by a power cycle | yes — the table is RAM |
| Optional timeout | yes, in seconds; 0 means hold until released |
Closing the editor deliberately does not release a force: a technician who forces a valve open, walks around the machine and comes back expects to find it open. A power cycle does clear it, because a panel that came back from a power failure still holding somebody's value would be a trap — and unlike an S7, this panel has no dedicated lamp to warn about it.
A forced tag that is retentive keeps its last value
Forcing is gone after a restart, but if the tag itself is retentive, the value it was left at survives — that is the tag's property, not the force's. The program owns it again from the first cycle.
Nothing is silent
- Every set, release and expiry goes into the panel's diagnostic buffer, so the machine's own log carries what was held and for how long.
- The editor's status bar shows
FORCED nin a colour that belongs to nothing else, on every page, and clicking it opens this window. - The list in the window is what the panel reports, never what this editor asked for. The two differ the moment a force expires or somebody else releases one, and the machine is the party telling the truth.
Using it
- Pick the tag from the list (the filter takes a substring).
- Set the value. A BOOL gets a checkbox; a number gets a field; a TIME tag is forced in milliseconds.
- Leave the hold at
0to keep it until you release it, or enter seconds for a timeout the panel enforces itself. - Force this value.
Release one with the button next to it, or all of them with Release all — which is never disabled while anything is held.
Limits and refusals
| Forced tags at once | 8 |
| Longest timeout | 1 hour |
| Requires | ONLINE, the same program in editor and panel, and a licence |
Eight is deliberate. Somebody who needs to force nine things at once is not commissioning any more, they are rewriting the program — and that belongs in the editor, not in a live machine.
This is not a safety function
Forcing bypasses your program's logic, including its interlocks. It does not bypass hard-wired safety — emergency stop, doors and light curtains are wired, not programmed — and nothing here changes the rule that safety is never routed through the panel.
Structured Text (ST)
dokumentaciaThe 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
Ladder (LD)
dokumentaciaThe ladder editor draws the logic the way an electrician reads a schematic: rungs between two power rails, contacts in series and in parallel, the coil at the right. Underneath there is no separate "ladder runtime" — every rung compiles into the same function blocks the FBD editor produces and the panel already executes. Uploading a ladder program is uploading data, never firmware.
Elements (v1)
| Symbol | Meaning |
|---|---|
-[ ]- |
NO contact — conducts while the tag is TRUE |
-[/]- |
NC contact — conducts while the tag is FALSE |
-[P]- / -[N]- |
rising / falling edge (one cycle) |
-( )- |
coil — the rung result lands in the tag |
-(S)- / -(R)- |
set / reset latch |
| block in the rung | TON, TOF, TP, CTU, CTD — the rung drives IN, Q carries on |
Blocks show their second output at the right stub: a counter its CV (how many it has counted), a timer its ET (how long it has been running). Bind it to a tag, or just read it — during online monitoring the value comes straight from the machine.
Series contacts AND together; a parallel branch ORs. The classic start/stop seal-in is exactly:
|--[ start ]--+--[/ stop ]-----------------( run )--|
|--[ run ]--+
Working with rungs
- + Rung adds a rung; click a contact or the coil to change its tag (with a filter), its kind, insert contacts before/after, add a parallel branch, drop a timer/counter into the rung, or move the rung up/down. Everything is undoable.
- A
(S)and(R)pair on the same tag becomes ONE flip-flop; the pair's order decides the priority (set-first = set wins). - Two plain coils on one tag are refused at the desk — the last writer would silently win on the panel. Use a branch or S/R.
- Every contact and coil shows its variable name and, underneath,
the tag's description as a
//comment(edited right in the element popup; the comments checkbox hides them when the rungs should pack tighter). Descriptions travel with the project. - In test mode the rungs show the live power flow: conducting wires and symbols turn green, the coil fills when its tag is TRUE. This is the same test PLC the FBD scheme uses. Clicking a contact toggles its tag.
Ladder and FBD
A program belongs to ONE editor. Ladder-owned programs carry the ladder icon in the tree and open in the LD editor; opening them in FBD is read-only (the blocks are regenerated from the rungs, edits there would be lost). An FBD network that fits the ladder subset can be viewed as rungs; anything richer — analog paths, multiple consumers of one output, PID — stays in FBD or moves to ST.
Limits
32 rungs per program, 8 parallel branches per fork, one coil column. The real ceiling is the shared block budget (64 blocks) — the Resources panel shows the consumption as you build.
Function block diagram (FBD)
dokumentaciaMachine control in ctrl32 is assembled from blocks in an FBD diagram —
the logic is data in the pack, not code in the firmware. Besides the
fixed block library (TON, PID, … — see widgets.md) there are:
Composite block (an FB built from blocks)
A composite is a POU with an interface (input/output ports) and its own internal FBD network. In a diagram it is used like an ordinary block; a double-click opens it and shows how it is put together. At compile time it is flattened into primitives — the panel executes a flat list, with no recursion and no extra interpreter.
- Creating one: right-click on Application → Add Composite FB; the ports are edited with the Ports… button in the diagram (max 4 inputs, 2 outputs). Changing the interface bumps the block version.
- Supplied blocks (for example
DRIVE_i550fromlibraries/) are read-only — you can look inside, but not change it. Save as own creates an editable copy in the project. - In YAML: the
composites:section; inside the body$PORTmarks an interface port and~wirean internal wire (every use of a composite gets its own wires — instances do not share state). - Limits: nesting depth 8, recursion forbidden, at most 64 blocks AFTER flattening — all of it enforced by the compiler, not the panel.
Axis (AXIS) and drives
An axis is a drive controlled by motion blocks. It is added by right-clicking its bus node in the device tree (CANopen master → CiA402, SPI → POWERSTEP01 or TMC5160 — the node decides the backend) and carries the backend and its parameters:
| Backend | Bus | Status |
|---|---|---|
| CiA402 (Lenze i550) | CANopen | tested on the bench |
| POWERSTEP01 | SPI | interface ready, driver ships with the hardware |
| TMC5160 / TMC5130 | SPI | driver written from the datasheet, mock-tested — verify on the bench |
Blocks bound to an axis (they have an axis selector in the top bar of the diagram):
MC_POWER— switch the axis on (Status = axis in operation)MC_MOVEVELOCITY— velocity; it also accepts a change of the setpoint while runningMC_STOP— controlled stop along the decel rampMC_RESET,MC_READACTVEL,MC_READSTATUS- positioning
MC_MOVEABS/MOVEREL/HOME/READACTPOS(for the SPI stepper backends POWERSTEP01 / TMC5160) CO_CIA402— the core of the state machine (a building block for composites)CO_SDOWRITE— writes a parameter at startup (it is translated into commissioning; an SDO is never executed inside the cycle)CO_RAMPTIME— ACC/DEC ramp times in seconds for the commanded speed; they are written to the drive only while the axis is stopped
The supplied DRIVE_i550 block
┌────────────────┐
command ────┤ Enable Running ├──── running
setpoint ───┤ Speed ActualS.├──── speed
direction ──┤ Reverse │
└───────┬────────┘
axis1 (CANopen node …)
Inside: MC_POWER + MC_MOVEVELOCITY + MC_READACTVEL. STOP = dropping
Enable → the drive coasts down along its own decel ramp and disables
itself. Read the diagnostics (current, torque, DC link, frequency …)
with CANopen data tags and gain scaling (for example ‰ → A); no
blocks are needed for that.
Verified i550 objects: the actual output frequency is 0x2DDD (0.1 Hz); 0x2D87 is the ramped setpoint (at standstill it holds the setpoint).
Safety
The motor only starts from a deliberate operator command (a button on the display / in WEBview). Stopping over CANopen is not a safety function — the drive's STO terminals and the limit switches must be hard-wired.
Troubleshooting
dokumentaciaA list of the actual messages the system prints and what is really behind
them. When a build fails, the editor opens a window with the whole
output — the text can be selected with the mouse and copied with a button,
and the same text is in build\last_build_error.txt.
Downloading a program
"the ST compiler (visu_stc) was not found"
The ST compiler is missing. Since ladder and FBD compile to bytecode, every project with logic needs it.
- Installed editor:
visu_stc.exemust stay next tovisu_editor.exe. If you moved files, put it back or reinstall. - Repository clone: either use the binary from
dist\, or build the compiler:cmake --build build --target visu_stc.
"the ST compiler … is schema v16, this builder is v17"
The three installed binaries do not come from one build — an update
replaced some and not others. The editor, the pack builder and visu_stc
carry the schema version they were built for and check each other, because
a mismatch used to surface as nonsense further down ("TON has no member
'ET'"). Reinstall ctrl32 so all three come from the same release.
The same message about the pack builder means packbuild\ next to the
editor is older than the editor itself.
"tag 'NOT' has the name of an ST keyword"
A tag becomes an identifier in the generated program, so a name like
NOT, VAR, REAL or TIME means something else there. Rename the tag —
the message says which one.
"EQ 'b30': its parameters have no ST form yet"
A block the editor can draw but the generator cannot yet translate. The
message always names both the type and the name of the block. Report it
to bugs@ctrl32.com — it is usually a small fix.
"logic cannot compile to bytecode: … did not compile"
The generated program failed to compile — that is our fault, not your project's. The editor prints the whole generated source with line numbers; send that output.
"project has N blocks, the limit is 512"
A real limit. See Performance and limits; usually the tag table runs out first.
The panel
The panel refused the pack ("invalid pack")
The pack failed verification — most often the target board does not match (the project is for another panel), or the runtime is older than the pack format.
Important: the old pack stays and keeps running. A download cannot disable the panel. The fix: check the board in the project, or flash the runtime.
"!! N FB RECORDS – this pack predates the one-engine build"
The panel holds a pack from a very old editor. Its logic does not run — the panel says so on the console and in the diagnostic buffer. Rebuild the project with the current editor and download it again.
The panel answers but seq does not move
The connection is fine; the PLC cycle has stopped. Look at the diagnostic buffer (Debug → diagnostics) — the reason is usually there (a VM fault, a cycle overrun).
Online monitoring
The plug icon is red
The panel runs a different program than the editor. Values are deliberately not shown — they travel by index, and under a different program they would appear as real numbers under the wrong names. The editor offers to download straight away.
The icon is amber and nothing happens
The port cannot be opened: another program is holding it (a terminal, another editor), the panel is restarting, or the cable is unplugged. It retries once a second, so plugging the cable back in is enough.
The port list is empty, or nothing is selected
No port is preselected on purpose. Pick one in the toolbar or in the transfer dialog — the download and flash buttons stay disabled until you do.
I see tags but no values on the wires between blocks
The symbols could not be verified — the editor uses them only when the compiled program matches the one in the panel byte for byte. It writes the state into the status bar; the usual cause is that the panel holds a different build.
ONLINE and Deploy are locked
The licence is missing. Editing and simulation work without one; talking to a real panel is the paid part.
The editor
A new project will not take a rung
This was a bug and it is fixed: the + Rung button now creates a BOOL tag itself and says which one. If it still does not work, you have an old binary.
A comment next to a variable disappeared
Also fixed. The comment is written at the end of the line after // and
read back; leaving the line without one deletes the comment — the text is
the whole truth for its list.
An opened project has an empty ladder
A fixed import bug (the project was being loaded through a pack without block records). Update the editor.
If a problem does not fit here, send the whole error output (the Copy
message button) to bugs@ctrl32.com — the bug icon at the top right of the
menu opens the mail directly.
Online monitoring
dokumentaciaAn editor connected to a running panel shows live values in the places you are looking at anyway: at the contacts in a rung, on block pins, in the variable list and in the screen preview. It is the same view as test mode, except the values are not computed by a simulation — they come from the machine.
Switching it on
Pick the COM port in the toolbar and click the plug icon. The colour is the state:
| Colour | Meaning |
|---|---|
| green | the panel answers and runs the same program |
| amber | looking for the port (cable unplugged, panel restarting) |
| red | the panel answers but runs a different program |
The tooltip over the icon carries three numbers worth a look: what the monitoring costs the panel (in µs), how old the last reading is, and the sample counter.
A frozen sample counter is not a broken connection
If the panel answers but seq does not move, the PLC cycle has
stopped. That is a diagnosis, not a timeout — and it saves an hour of
looking in the wrong place.
The same program, or nothing
Values travel from the panel by index, not by name. If the editor held a newer project than the panel, it would show real numbers under the wrong names — a lie that looks alive, which is worse than an empty field.
So both sides compare two numbers when connecting:
- the tag table fingerprint — the names and types of all tags in index order,
- the pack CRC — the exact identity of the program (a changed rung leaves the tags alone but moves the offsets of local variables).
When they do not match, the editor says "The panel runs a different program — downloading this one" and opens the download. Monitoring parks itself meanwhile (the upload needs the same port) and reconnects after the restart.
You can also read the fingerprint from the command line:
visu_editor.exe project.yaml --tag-hash
# TAGHASH 43943135 (215 tags)
On site that separates "the panel runs a different program" from "the cable is bad" without guessing.
What can be watched
| Kind | Where from | Note |
|---|---|---|
| tags | the panel's tag table | always works |
| wires between blocks | the VM's instance memory | names come from the compiled program |
block members (t1.ET, c1.CV) |
the VM's instance memory | the same |
| panel state | cycle, overruns, heap | including what the monitoring itself costs |
Wires and block members are not tags — since the move to bytecode they are words of instance memory (which is exactly why the cycle got 5× faster). The editor learns their names by building the project and comparing the compiled program byte for byte with what is in the pack. When they match, the offsets are provably the panel's; when they do not, it shows tags only and says so.
In a rung that means concrete things: for a counter how much it has
counted (CV), for a timer how long it has been running (ET, in ms
below a second and in seconds above it), and which PT or PV the block is
really working with — not the one drawn on it. When a parameter is fed
from a tag, that difference is exactly what you are looking for.
What it costs the cycle
The panel samples only what you are looking at, and only ten times a second — a faster eye would not read it anyway. Measured on a classic ESP32 (10 ms cycle, machine program):
| state | logic | whole task body |
|---|---|---|
| nobody watching | 299 µs | 287 µs |
| 8 tags | 299 µs | 288 µs |
| 64 tags (the maximum) | 299 µs | ~289 µs |
One watched value costs ~0.34 µs per sample, so 64 values ten times a second is 2.2 µs per cycle — 0.02 % of a ten-millisecond period. With nobody online the list is empty and even that is not executed.
Panel statistics stay live
Cycle time, overruns, heap and the bus states keep updating while you are online. The monitoring worker owns the serial port, so it asks the panel for them itself every ~2 s and everything that used to open the port for that — the status bar, the device page, the CAN and Modbus dots — reads from there. A reading older than 6 s is not shown at all: a frozen cycle time that keeps being displayed is worse than an empty field.
The serial console keeps working
The terminal tab reads through the monitoring session: every line the panel prints that is not a protocol answer lands in a buffer the terminal drains, and typing works too (the worker does the writing). The diagnostic pages — CAN dump, modem status, the diagnostic buffer — go the same way, so they answer while you are online instead of reporting "no reply".
Writing values (force)
The panel can hold a value against the program — for checking wiring or exercising an output before its sensor exists. It is a commissioning aid and it has its own page, because it is the one thing here that changes a machine rather than showing it: Forcing values.
The short version: a force is held by the panel until released, closing
the editor does not release it, a power cycle does, and the status bar
says FORCED n on every page while anything is held.
Buffer (scope)
A live view ten times a second shows a steady state and misses short events — a pulse one cycle long simply falls between samples. That is what the scope is for: up to 8 values sampled every cycle into a ring buffer in the panel, with a trigger and the history from before it.
It has its own page: Scope.
Recording switched off costs nothing
Not "almost nothing" — the cycle performs a single comparison and does not enter sampling at all. While recording, 8 values every cycle cost ~5 µs, or 0.05 % of the period.
Performance and limits
dokumentaciaEvery number on this page is measured, not estimated, and measured on the slowest board we ship — a classic ESP32 at 240 MHz with Ethernet, WEBview and a CAN master running alongside. A faster board (S3, P4) has more headroom, not less.
Declared limits
The editor refuses a project that exceeds them at your desk — not as a stuttering panel on a machine. They are also the numbers for the datasheet.
| Limit | Value |
|---|---|
| Tags | 512 |
| Alarms | 128 |
| Screens | 32 |
| Widgets per screen | 64 |
| LD/FBD blocks | 512 |
| Program (bytecode) | 32 kB |
| Instance data | 8 kB |
| Trends | 4 |
| WEBview clients | 2 |
| Axes (drives) | 4 |
| PLC tasks | 2 (fast + slow) |
How many blocks really fit
The program is N AND blocks over 150 tags — every block reads two tags and writes one, which is the worst realistic ratio:
| Blocks | Average cycle | Of a 10 ms period |
|---|---|---|
| 64 | 110 µs | 1.1 % |
| 100 | 153 µs | 1.5 % |
| 200 | 274 µs | 2.7 % |
| 400 | 515 µs | 5.2 % |
A Siemens LOGO! stops at 400 blocks. We run them on the weakest board we ship, with a cycle that is 95 % idle. The declared limit of 512 is not a performance boundary but a bytecode budget: a simple block costs ~10 B, so 512 blocks take 16 % of the 32 kB.
In practice the tag table (512) runs out sooner than the blocks do.
A real program, not a synthetic test
| Program | ST lines | Tags | Cycle |
|---|---|---|---|
| half a machine | 100 | 110 | 168 µs |
| a whole machine | 190 | 215 | 304 µs |
"A whole machine" means: 40 analog inputs scaled and alarmed, 30 motors with interlocks, a 40-step sequence and a positioning ramp. 3 % of a 10 ms cycle, zero overruns.
A realistic ST line costs ~1.6 µs, a ladder block ~1.3 µs.
What the cycle actually spends its time on
The interesting part: it is not the arithmetic, it is tag access. A synthetic loop over local variables runs at 0.145 µs per instruction; the same code touching tags is several times slower.
That is why the lock in the tag table was removed on 2026-08-16 — every value is one naturally aligned 32-bit word, so the hardware already guaranteed what the mutex was being paid for. The result, same packs, same board:
| before | after | |
|---|---|---|
| 400 blocks | 2 660 µs | 515 µs |
| a whole machine | 1 319 µs | 304 µs |
| 10 000 instructions (local variables) | 1 410 µs | 1 420 µs |
The last row is the control sample: code that does not touch tags did not move by a microsecond. Everything that moved, moved because of tags.
The degradation rule
When performance runs out, the order is fixed and written into the code itself:
logic > communication > graphics
Under overload, frames are dropped. The cycle is never extended. The display may stutter; machine control may not.
The diagnostics you can see
The panel measures min/average/max cycle time and an overrun counter and reports them to the editor. When someone writes heavy logic and the picture starts to stutter, they will see why — and the reports include what the monitoring itself costs.
The measurements are repeatable and the numbers are not from paper — the panel reads them itself (min/average/max cycle). The method and the test projects are available on request.
Screens and widgets
dokumentaciaSchema version: 17
Common properties (widget_base)
| Property | Type | Default |
|---|---|---|
id |
str | |
x |
i16 | |
y |
i16 | |
w |
i16 | |
h |
i16 | |
anchor_x |
WidgetAnchorX | start |
anchor_y |
WidgetAnchorX | start |
margin |
i16 | 0 |
style |
style_ref | |
visible_if |
tag_ref | |
enabled_if |
tag_ref | |
children |
list | |
animations |
list |
panel
| Property | Type | Default |
|---|---|---|
radius |
i16 | 0 |
list
| Property | Type | Default |
|---|---|---|
item_height |
i16 | |
inertia |
bool | True |
tabs
| Property | Type | Default |
|---|---|---|
active |
i16 | 0 |
popup
| Property | Type | Default |
|---|---|---|
radius |
i16 | 8 |
label
| Property | Type | Default |
|---|---|---|
text |
str | |
font |
font_ref | |
align |
LabelAlign | left |
value
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
font |
font_ref | |
show_unit |
bool | True |
warn_above |
f32 | |
warn_color |
color |
button
| Property | Type | Default |
|---|---|---|
text |
str | |
tag |
tag_ref | |
mode |
ButtonMode | |
value |
f32 | 1.0 |
icon |
image_ref | |
confirm |
bool | False |
confirm_text |
str |
switch
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
label_on |
str | ON |
label_off |
str | OFF |
bit |
i16 | -1 |
led
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
color_on |
color | |
color_off |
color | |
blink_hz |
f32 | 0.0 |
bit |
i16 | -1 |
slider
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
min |
f32 | 0.0 |
max |
f32 | 100.0 |
step |
f32 | 1.0 |
bar
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
min |
f32 | 0.0 |
max |
f32 | 100.0 |
orientation |
BarOrientation |
gauge
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
min |
f32 | 0.0 |
max |
f32 | 100.0 |
start_angle |
i16 | 135 |
sweep |
i16 | 270 |
cache_background |
bool | True |
image
| Property | Type | Default |
|---|---|---|
src |
image_ref | |
tint |
color |
nav
| Property | Type | Default |
|---|---|---|
text |
str | |
target |
str | |
icon |
image_ref |
icon
| Property | Type | Default |
|---|---|---|
src |
icon_ref | |
color |
color | |
tag |
tag_ref | |
src_active |
icon_ref | |
color_active |
color |
input
| Property | Type | Default |
|---|---|---|
tag |
tag_ref | |
keypad |
InputKeypad | |
min |
f32 | |
max |
f32 | |
title |
str | |
font |
font_ref |
trend
| Property | Type | Default |
|---|---|---|
series |
list | |
timespan_s |
i32 | 300 |
sample_ms |
i32 | 1000 |
autoscale |
bool | True |
min |
f32 | |
max |
f32 | |
grid |
bool | True |
show_legend |
bool | True |
scroll_mode |
bool | True |
alarm_table
| Property | Type | Default |
|---|---|---|
filter |
AlarmTableFilter | |
min_priority |
AlarmPriority | |
rows |
i16 | 8 |
show_time |
bool | True |
show_ack_button |
bool | True |
sort |
AlarmTableSort | newest_first |
A program = a list of block instances in execution order. A pin input is a tag or a constant, an output writes into a tag. Times are always in ms.
TON
On-delay timer — Q switches on once IN has been held continuously for PT ms.
| Pin | Direction | Type |
|---|---|---|
IN |
input | bool |
Q |
output | bool |
ET |
output | f32 |
PT (ms) |
parameter | f32, default 1000 |
TOF
Off-delay timer — Q stays on for another PT ms after IN drops.
| Pin | Direction | Type |
|---|---|---|
IN |
input | bool |
Q |
output | bool |
ET |
output | f32 |
PT (ms) |
parameter | f32, default 1000 |
TP
Pulse — a rising edge of IN fires Q for exactly PT ms (non-retriggerable).
| Pin | Direction | Type |
|---|---|---|
IN |
input | bool |
Q |
output | bool |
ET |
output | f32 |
PT (ms) |
parameter | f32, default 1000 |
CTU
Up counter — a rising edge of CU increments, R resets, Q = CV >= PV.
| Pin | Direction | Type |
|---|---|---|
CU |
input | bool |
R |
input | bool |
Q |
output | bool |
CV |
output | f32 |
PV |
parameter | f32, default 10 |
CTD
Down counter — a rising edge of CD decrements, LD loads CV = PV, Q = CV <= 0.
| Pin | Direction | Type |
|---|---|---|
CD |
input | bool |
LD |
input | bool |
Q |
output | bool |
CV |
output | f32 |
PV |
parameter | f32, default 10 |
R_TRIG
Rising edge — Q is TRUE for one cycle after CLK rises.
| Pin | Direction | Type |
|---|---|---|
CLK |
input | bool |
Q |
output | bool |
F_TRIG
Falling edge — Q is TRUE for one cycle after CLK drops.
| Pin | Direction | Type |
|---|---|---|
CLK |
input | bool |
Q |
output | bool |
SR
Flip-flop with SET priority.
| Pin | Direction | Type |
|---|---|---|
S1 |
input | bool |
R |
input | bool |
Q1 |
output | bool |
RS
Flip-flop with RESET priority.
| Pin | Direction | Type |
|---|---|---|
S |
input | bool |
R1 |
input | bool |
Q1 |
output | bool |
SCALE
Linear scaling of IN from IN_MIN..IN_MAX to OUT_MIN..OUT_MAX.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
IN_MIN |
parameter | f32, default 0 |
IN_MAX |
parameter | f32, default 100 |
OUT_MIN |
parameter | f32, default 0 |
OUT_MAX |
parameter | f32, default 100 |
LIMIT
Clamps IN into the MN..MX range.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
MN |
parameter | f32, default 0 |
MX |
parameter | f32, default 100 |
HYST
Comparator with hysteresis — Q switches on at IN >= ON, off at IN <= OFF.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
Q |
output | bool |
ON |
parameter | f32, default 100 |
OFF |
parameter | f32, default 90 |
RAMP
Rate limiter — OUT follows IN at most RATE units per second.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
RATE (1/s) |
parameter | f32, default 10 |
PT1
First-order filter — OUT approaches IN with time constant T ms.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
T (ms) |
parameter | f32, default 1000 |
BLINK
Blinker — while RUN, Q alternates T_ON ms on / T_OFF ms off.
| Pin | Direction | Type |
|---|---|---|
RUN |
input | bool |
Q |
output | bool |
T_ON (ms) |
parameter | f32, default 500 |
T_OFF (ms) |
parameter | f32, default 500 |
ADD
OUT = IN1 + IN2 + IN3 + IN4 (unconnected inputs are 0).
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
IN3 |
input | f32 |
IN4 |
input | f32 |
OUT |
output | f32 |
SUB
OUT = IN1 - IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
OUT |
output | f32 |
MUL
OUT = IN1 * IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
OUT |
output | f32 |
DIV
OUT = IN1 / IN2; division by zero yields 0 (no crash).
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
OUT |
output | f32 |
MIN
OUT = the smaller of IN1, IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
OUT |
output | f32 |
MAX
OUT = the larger of IN1, IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
OUT |
output | f32 |
ABS
OUT = |IN|.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
AND
Q = IN1 AND IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | bool |
IN2 |
input | bool |
Q |
output | bool |
OR
Q = IN1 OR IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | bool |
IN2 |
input | bool |
Q |
output | bool |
XOR
Q = IN1 XOR IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | bool |
IN2 |
input | bool |
Q |
output | bool |
NOT
Q = NOT IN.
| Pin | Direction | Type |
|---|---|---|
IN |
input | bool |
Q |
output | bool |
GT
Q = IN1 > IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
Q |
output | bool |
GE
Q = IN1 >= IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
Q |
output | bool |
LT
Q = IN1 < IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
Q |
output | bool |
LE
Q = IN1 <= IN2.
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
Q |
output | bool |
EQ
Q = |IN1 - IN2| <= EPS (comparison with tolerance).
| Pin | Direction | Type |
|---|---|---|
IN1 |
input | f32 |
IN2 |
input | f32 |
Q |
output | bool |
EPS |
parameter | f32, default 0 |
SEL
OUT = G ? IN1 : IN0 (binary selector as in IEC).
| Pin | Direction | Type |
|---|---|---|
G |
input | bool |
IN0 |
input | f32 |
IN1 |
input | f32 |
OUT |
output | f32 |
MOTOR
Start/stop with feedback: FB must confirm running within T_FB ms, otherwise FAULT (latched, command dropped). ACK acknowledges only at standstill. STOP overrides START.
| Pin | Direction | Type |
|---|---|---|
START |
input | bool |
STOP |
input | bool |
FB |
input | bool |
ACK |
input | bool |
CMD |
output | bool |
FAULT |
output | bool |
T_FB (ms) |
parameter | f32, default 3000 |
VALVE
Valve: OPEN opens, CLOSE has priority; FB (end position) must arrive within T_FB ms, otherwise a latched FAULT; ACK acknowledges.
| Pin | Direction | Type |
|---|---|---|
OPEN |
input | bool |
CLOSE |
input | bool |
FB |
input | bool |
ACK |
input | bool |
CMD |
output | bool |
FAULT |
output | bool |
T_FB (ms) |
parameter | f32, default 5000 |
PID
PID controller: OUT = KP·e + I + D within OUT_MIN..OUT_MAX (anti-windup — the integrator never runs beyond the limits). AUTO=FALSE: OUT follows MAN and the integrator tracks it — switching back to AUTO is bumpless. TI=0 disables the I term, TD=0 the D term (derivative from PV with a TD/8 filter, not from the error — a setpoint step does not kick the output). LIM signals saturation.
| Pin | Direction | Type |
|---|---|---|
SP |
input | f32 |
PV |
input | f32 |
MAN |
input | f32 |
AUTO |
input | bool |
OUT |
output | f32 |
LIM |
output | bool |
KP |
parameter | f32, default 1 |
TI (s) |
parameter | f32, default 60 |
TD (s) |
parameter | f32, default 0 |
OUT_MIN |
parameter | f32, default 0 |
OUT_MAX |
parameter | f32, default 100 |
MC_POWER
PLCopen: axis on/off. Status = axis ready and operational.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Status |
output | bool |
Error |
output | bool |
MC_MOVEVELOCITY
PLCopen: constant-velocity motion (i550). Ramps up to Velocity using the drive ramp; InVelocity when reached. Direction 0 = forward, 1 = reverse.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Velocity |
input | f32 |
InVelocity |
output | bool |
Busy |
output | bool |
Direction |
parameter | f32, default 0 |
MC_STOP
PLCopen: controlled stop along the decel ramp (Decel in seconds).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Done |
output | bool |
Busy |
output | bool |
Decel (s) |
parameter | f32, default 5 |
MC_RESET
PLCopen: axis fault acknowledge (fault reset).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Done |
output | bool |
Error |
output | bool |
MC_READACTVEL
PLCopen: reads the actual axis velocity.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Velocity |
output | f32 |
Valid |
output | bool |
MC_READSTATUS
PLCopen: axis state — Running, Error (fault).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Running |
output | bool |
Error |
output | bool |
MC_MOVEABS
PLCopen: move to an absolute position (POWERSTEP01 backend).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Position |
input | f32 |
Velocity |
input | f32 |
Done |
output | bool |
Busy |
output | bool |
Accel |
parameter | f32, default 100 |
Decel |
parameter | f32, default 100 |
MC_MOVEREL
PLCopen: move by a relative distance (POWERSTEP01 backend).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Distance |
input | f32 |
Velocity |
input | f32 |
Done |
output | bool |
Busy |
output | bool |
Accel |
parameter | f32, default 100 |
Decel |
parameter | f32, default 100 |
MC_HOME
PLCopen: homing run (POWERSTEP01 backend).
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Done |
output | bool |
Busy |
output | bool |
MC_READACTPOS
PLCopen: reads the actual axis position.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Position |
output | f32 |
Valid |
output | bool |
CO_CIA402
CANopen: the CiA402 state-machine core over an axis — building block for MC_* and the DRIVE_i550 composite. Ready = operation_enabled.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Ready |
output | bool |
Fault |
output | bool |
CO_SDOWRITE
CANopen: writes an object on the axis — at STARTUP only (packbuild compiles it into an SdoInitRec, never in the cycle). INDEX/SUB/SIZE parameterize the object.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
INDEX |
parameter | f32, default 0 |
SUB |
parameter | f32, default 0 |
VALUE |
parameter | f32, default 0 |
SIZE |
parameter | f32, default 1 |
CO_SDOREAD
CANopen: reads an object from the axis at startup. INDEX/SUB select the object.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
OUT |
output | f32 |
INDEX |
parameter | f32, default 0 |
SUB |
parameter | f32, default 0 |
CO_RAMPTIME
CiA402: ACC/DEC ramp times in seconds (0 -> target rpm). Written to the drive (0x6048/0x6049) ONLY when the axis is at standstill and only on change — the cycle never touches SDO while running. For the i550 with WEBview ACC/DEC inputs.
Bound to an axis (AXIS) — not to a tag.
| Pin | Direction | Type |
|---|---|---|
ACC |
input | f32 |
DEC |
input | f32 |
REF |
input | f32 |
IR_SEND
Sends an infrared code from the project's IR key table on the rising edge of Execute. KEY = table index. Done pulses while Execute holds after a successful transmit; Error = no IR transmitter configured or KEY out of range.
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Done |
output | bool |
Error |
output | bool |
KEY |
parameter | f32, default 0 |
SMS_SEND
Sends an SMS from the project's message table (gsm: messages) on the rising edge of Execute. MSG = message index (0-7 as numbered on the modem page), NUM = recipient: 0 = every configured number, 1-4 = one number (#1-#4 on the modem page). Done holds while Execute holds after the SMS was queued; Error = no modem service, an empty message/number or the service is busy (retry on the next edge).
| Pin | Direction | Type |
|---|---|---|
Execute |
input | bool |
Done |
output | bool |
Error |
output | bool |
MSG |
parameter | f32, default 0 |
NUM |
parameter | f32, default 0 |
GSM_INFO
Modem service state for the program: Ready = registered on the network (SMS possible), Signal = last +CSQ reading 0-31 (0 also while not measured yet — the modem polls it every 30 s). Without a configured modem both outputs stay 0. The v36 status tag stays the screen-facing path; this block is the logic-facing one (interlocks, retry gating).
| Pin | Direction | Type |
|---|---|---|
Enable |
input | bool |
Ready |
output | bool |
Signal |
output | f32 |
MOVE
OUT = IN — the IEC assignment as a block. In the LD editor it is the plain EN result box (commit a value while the rung conducts), in FBD a tag-to-tag copy.
| Pin | Direction | Type |
|---|---|---|
IN |
input | f32 |
OUT |
output | f32 |
Tags
dokumentaciaA tag is a variable. It is also the only place in the whole system where a value lives — a widget holds no value, only a tag index. That is why the screen, the logic, WEBview and Modbus all see the same number, with no way to drift apart.
The tag table is at the same time the PLC's process image (the equivalent of I/Q/M on an S7).
Creating a tag
In the project tree: Application → Tags. Every tag has:
| Property | What it is for |
|---|---|
| name | the only thing you refer to it by; in ST it is an identifier |
| type | BOOL, I16, U16, I32, U32, REAL (f32), STRING, TIME |
| access | ro (read only) / rw — for WEBview and the operator, not for the logic |
| source | where the value comes from (below) |
| gain / offset | conversion to a physical quantity: y = x · gain + offset |
| unit, decimals | how it is displayed |
| deadband | from what change on it counts as a change |
A tag name must not be an ST keyword
Names like NOT, AND, VAR, REAL, TIME or END_IF become
something else in the generated program. The editor refuses them at
build time and names the tag.
Where the value comes from
| Source | Meaning |
|---|---|
| internal | panel memory only; with persist it survives a restart |
| modbus | from a PLC, a drive or a meter — unit, function code, address |
| canopen | an object from a node's dictionary (index/subindex), over PDO or SDO |
| gpio | a local panel input/output, optionally inverted |
| periph | a chip pin on a local bus (expander, thermometer, LED) |
Word order for Modbus `f32`
A REAL travels over Modbus in two 16-bit registers, and the word
order differs from vendor to vendor. It is the most common source of
nonsense values. There is a word_order switch for it; when 23.5 °C
reads as 3.4·10³⁸, start here.
SDO never in the cycle
CANopen objects read over SDO are slow and do not belong in the cycle — they are for parameterization at startup or an operator's action. What belongs in the cycle is PDO.
TIME — a time the operator sets
A machine's delays belong to the operator, not to a rebuild. That is
what a TIME tag is for:
- { name: delay, datatype: time, unit: s, decimals: 1, init: 2500 }
- the table always holds milliseconds — the logic, the buses and Modbus see an ordinary integer,
- the panel shows and edits it in the tag's unit (
ms,s,min,h). Withunit: sthe widget reads "2.5 s", the operator types2.5on the keypad and 2500 is stored, min/maxon theinputwidget are in the same unit, so the operator is bounded by the numbers they can see,- in ST it is a
TIME, sot1(PT := delay)anddelay := T#1s500msneed no conversion at all.
This is exactly how you put a "preheat time" on the operator's screen: an
input bound to a TIME tag, and the same tag as the timer's PT.
Time comparisons are unsigned
A TIME runs past 2³¹ ms (24.8 days), which is ordinary on a machine
that is never switched off. Comparisons are therefore unsigned, and
IF t1.ET > T#500ms still holds after a month of running.
Deadband is not cosmetics
An analog input always has noise. Without a deadband the panel repaints the display on every flicker of the last bit, and sends that same flicker over the WebSocket to every connected browser.
Set the deadband larger than the noise and smaller than what you care about: for a pressure sampled at 0.01 bar resolution, 0.05 bar is sensible.
Retention (persist)
An internal tag with persist survives a power-down. One hard rule
applies:
Do not write retentive data on every change
Flash and EEPROM both have a finite number of writes. A tag that changes every cycle and is written on every change destroys the memory within months. Retentive is for what you need to remember across a power-down — a piece counter, a temperature setpoint, the current step — not for the instantaneous value of a measurement.
Why reading a tag is the most expensive thing in the cycle
Since 2026-08-16 the tag table is lock-free: a value is one naturally aligned 32-bit word and a tag's type never changes after registration, so a reader always gets either the old value or the new one — never half of each. The hardware guarantees it and no mutex is needed.
Before that, every access paid for a lock (~1.5 µs per take/give pair), which was 20–100× the memory access it guarded. The machine program went from 1.32 ms to 0.30 ms. Details in Performance and limits.
The practical consequence for you: keep intermediate results in POU local variables, not in tags. A tag is an interface — to the screen, to Modbus, to monitoring. Not scratch space.
A type wider than 32 bits does not fit the table
LREAL, LINT and a string stored inside the value are unsupported
precisely because they would break the lock-free read. Strings are
indices into the string table.
A snapshot, not an instant
The renderer and online monitoring both read a snapshot: every tag in it is fresh in itself, but it is not a consistent instant across all tags. It never was — a writer updates them tag by tag. For display that is right; when you need two values guaranteed to be from the same cycle, compute them in the logic and write the result.
Limit
512 tags. In practice this is the first thing to run out — before blocks, before bytecode. The editor refuses a project over the limit at build time.