Documentation

20 items

Scope

dokumentacia
Scope

Online 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

  1. 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.
  2. 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.

  3. 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.

Tags

Forcing values

dokumentacia
Forcing values

Everything 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 n in 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

  1. Pick the tag from the list (the filter takes a substring).
  2. Set the value. A BOOL gets a checkbox; a number gets a field; a TIME tag is forced in milliseconds.
  3. Leave the hold at 0 to keep it until you release it, or enter seconds for a timeout the panel enforces itself.
  4. 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.

Tags
Structured Text on ctrl32

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_TEMPEND_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
  • REAL into an integer without TRUNC/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/OR evaluate both sides (no short-circuit) — keep side effects out of conditions
Tags

Ladder (LD)

dokumentacia
Ladder (LD) on ctrl32

The 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.

Tags
FBD: composite blocks, axes and drive control

Machine 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 ApplicationAdd 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_i550 from libraries/) 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 $PORT marks an interface port and ~wire an 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 running
  • MC_STOP — controlled stop along the decel ramp
  • MC_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.

Tags

Troubleshooting

dokumentacia
Troubleshooting

A 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.exe must stay next to visu_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.

Tags

Online monitoring

dokumentacia
Online monitoring

An 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.

Tags
Performance and limits

Every 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.

Tags
Widgets

Schema 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
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
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
Function blocks (logic)

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

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

Tags

dokumentacia
Tags

A 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). With unit: s the widget reads "2.5 s", the operator types 2.5 on the keypad and 2500 is stored,
  • min/max on the input widget are in the same unit, so the operator is bounded by the numbers they can see,
  • in ST it is a TIME, so t1(PT := delay) and delay := T#1s500ms need 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.

Tags