Structured Text (IEC 61131-3) Target Documentation
VSCXML generates IEC 61131-3 Structured Text code from W3C SCXML state machine specifications. The output is a single .st file containing FUNCTION_BLOCK definitions suitable for CODESYS, TwinCAT (Beckhoff), and other IEC 61131-3 compliant PLC platforms.
Compliance Status
| Datamodel | Status | Tests | Notes |
|---|---|---|---|
| Null | Verified | 5/5 | Pure state logic with In() predicate (1 invoke test skipped) |
| Native-ST | Verified | 18/18 | Typed ST variables (INT, DINT, REAL, LREAL, BOOL, STRING) (2 tests skipped) |
| ECMAScript | Not supported | N/A | No JavaScript engine available on PLCs |
Verified: 2026-02-06 via CODESYS Control Win V3 simulation (V3.5 SP21 Patch 4)
Automated Testing
The ST target has an automated CODESYS test harness that:
- Generates
.stfiles from W3C test SCXML sources - Batches all tests into a single CODESYS project
- Runs compilation and simulation via CODESYS scripting API
- Reports pass/fail for each test
# Run null datamodel tests
python scxml-st/test/run_tests.py --datamodel null --verbose
# Run native-st tests (uses native-go test sources)
python scxml-st/test/run_tests.py --datamodel native-st --verbose
# Generate only (no CODESYS required)
python scxml-st/test/run_tests.py --datamodel null --generate-only --output-dir ./st-output
Requirements: CODESYS V3.5 (with simulation runtime) for full test execution. The --generate-only mode works without CODESYS and verifies that all test files generate valid ST code.
Requirements
- CODESYS V3.5 or later (recommended)
- TwinCAT 3 (Beckhoff) — compatible with CODESYS output
- Any IEC 61131-3 compliant PLC IDE that supports
METHODinsideFUNCTION_BLOCK
Code Generation
Generate Structured Text code using the CLI:
# Basic generation (null datamodel)
scxml-gen input.scxml -t st -o Output.st
# Code only (same as default — ST has no project scaffolding)
scxml-gen input.scxml -t st -o Output.st --code-only
# Bundle invoke children
scxml-gen parent.scxml -t st -o Parent.st --bundle-auto
# Batch processing
scxml-gen batch -i ./scxml -o ./st -t st
# Custom event queue depth (default: 16)
scxml-gen --config config.json --output-dir ./out
JSON Config Example
{
"scxml": "<?xml version='1.0'?><scxml name='TrafficLight' datamodel='native-st'>...</scxml>",
"target": "st",
"options": {
"className": "TrafficLight",
"eventQueueDepth": 32,
"plcPlatform": "codesys"
}
}
ST-Specific Options
| Option | Type | Default | Description |
|---|---|---|---|
plcPlatform |
string | codesys |
PLC platform variant: codesys, twincat, siemens, iec-strict |
eventQueueDepth |
integer | 16 |
Internal event ring buffer size (must be power of 2) |
className |
string | SCXML name |
Override the generated FUNCTION_BLOCK name |
codeOnly |
boolean | false |
Generate only code files (default for ST) |
bundleAuto |
boolean | false |
Auto-discover and bundle invoke children |
bundle |
string[] | [] |
Manually specify SCXML files to bundle |
PLC Platform Modes
The plcPlatform option controls vendor-specific syntax in the generated ST code:
| Mode | Pragmas | Access Modifiers | Use Case |
|---|---|---|---|
codesys (default) |
{attribute 'qualified_only'}, {attribute 'no_assign'} |
PUBLIC, PRIVATE on METHODs |
CODESYS, TwinCAT |
twincat |
Same as codesys | Same as codesys | TwinCAT (Beckhoff) |
siemens |
Same as codesys | Same as codesys | Siemens TIA Portal (future) |
iec-strict |
None (omitted) | None (omitted) | Portable IEC 61131-3, open-source validators |
iec-strict mode generates portable ST that conforms strictly to the IEC 61131-3 standard:
- No
{attribute ...}pragma directives (vendor-specific) - No
PUBLIC/PRIVATEaccess modifiers onMETHODdeclarations - No typed enumerations (uses plain
);instead of) DINT;) - No explicit enum values (sequential from 0 is the IEC default)
# Generate portable IEC 61131-3 ST
scxml-gen input.scxml -t st -o Output.st --plc-platform iec-strict
Generated Code Architecture
The ST generator produces a single .st file with the following structure:
┌─────────────────────────────────────────┐
│ TYPE E_{Name}_States : (...) │ State enum
│ TYPE E_{Name}_Events : (...) │ Event enum
├─────────────────────────────────────────┤
│ FUNCTION_BLOCK FB_{Name} │
│ ├── VAR │ Configuration bitset, queues, flags
│ ├── VAR_INPUT │ External event input
│ ├── VAR_OUTPUT │ Current state, isFinished
│ ├── (* Main body - cyclic execution *) │ Timer check, enqueue, process
│ ├── METHOD Start │ Initialize + enter initial state
│ ├── METHOD SendEvent │ Enqueue external event
│ ├── METHOD IsInState : BOOL │ BitSet check
│ ├── METHOD _Enter_S_{id} │ Per-state entry (one per state)
│ ├── METHOD _Exit_S_{id} │ Per-state exit (one per state)
│ ├── METHOD _Handle_S_{id} : BOOL │ Per-state event handler
│ ├── METHOD _Dispatch │ Route event to active states
│ ├── METHOD _CheckEventless │ Process automatic transitions
│ ├── METHOD _EnqueueInternal │ Ring buffer push
│ ├── METHOD _DequeueInternal : DINT │ Ring buffer pop
│ ├── METHOD _Stabilize │ Internal queue + eventless loop
│ └── END_FUNCTION_BLOCK │
└─────────────────────────────────────────┘
Usage
Basic Example (Null Datamodel)
SCXML input:
<scxml name="TrafficLight" datamodel="null" initial="red">
<state id="red">
<transition event="timer" target="green"/>
</state>
<state id="green">
<transition event="timer" target="yellow"/>
</state>
<state id="yellow">
<transition event="timer" target="red"/>
</state>
</scxml>
Usage in PLC program:
PROGRAM Main
VAR
fbTrafficLight : FB_TrafficLight;
bStarted : BOOL := FALSE;
END_VAR
IF NOT bStarted THEN
fbTrafficLight.Start();
bStarted := TRUE;
END_IF;
(* Send event via VAR_INPUT *)
fbTrafficLight.event := EVT_TIMER; (* Assign event ID *)
(* Call FB cyclically — processes events in FB body *)
fbTrafficLight();
(* Read current state from VAR_OUTPUT *)
(* fbTrafficLight.currentState = S_RED, S_GREEN, or S_YELLOW *)
Native-ST Datamodel
The native-st datamodel maps SCXML <data> elements to typed ST variables:
<scxml name="Counter" datamodel="native-st" initial="counting">
<datamodel>
<data id="counter" type="int" expr="0"/>
<data id="maxCount" type="int" expr="10"/>
<data id="temperature" type="double" expr="20.5"/>
<data id="active" type="boolean" expr="true"/>
</datamodel>
<state id="counting">
<transition event="increment" target="counting">
<assign location="counter" expr="counter + 1"/>
</transition>
<transition cond="counter >= maxCount" target="done"/>
</state>
<final id="done"/>
</scxml>
Type Mapping
SCXML type |
ST Type | Notes |
|---|---|---|
int |
INT |
16-bit signed integer |
long |
DINT |
32-bit signed integer |
float |
REAL |
32-bit floating point |
double |
LREAL |
64-bit floating point |
boolean |
BOOL |
Boolean value |
String |
STRING |
Variable-length string |
string |
STRING |
Variable-length string |
Parallel States
The ST generator supports parallel (orthogonal) regions using multi-bit DWORD configuration:
<scxml name="Machine" initial="operating">
<parallel id="operating">
<state id="motor" initial="stopped">
<state id="stopped">
<transition event="start" target="running"/>
</state>
<state id="running">
<transition event="stop" target="stopped"/>
</state>
</state>
<state id="heater" initial="off">
<state id="off">
<transition event="heat" target="on"/>
</state>
<state id="on">
<transition event="cool" target="off"/>
</state>
</state>
</parallel>
</scxml>
History States
Both shallow and deep history states are supported:
<state id="main" initial="s1">
<history id="h" type="shallow"/>
<state id="s1">
<transition event="next" target="s2"/>
</state>
<state id="s2">
<transition event="next" target="s1"/>
</state>
<transition event="interrupt" target="paused"/>
</state>
<state id="paused">
<transition event="resume" target="h"/> <!-- Restores last active child -->
</state>
Invoke (Child FUNCTION_BLOCKs)
The ST generator supports invoke via pre-compiled child FUNCTION_BLOCK instances:
# Bundle child machines
scxml-gen parent.scxml -t st -o Parent.st --bundle-auto
Each child SCXML becomes a separate FUNCTION_BLOCK in the same .st file. The parent FB declares static child instances in its VAR section and manages their lifecycle.
Design Decisions
State Tracking: DWORD BitSet
States are tracked using DWORD bit operations for O(1) lookups:
(* Enter state: set bit *)
_configuration := _configuration OR SHL(DWORD#1, stateIndex);
(* Exit state: clear bit *)
_configuration := _configuration AND NOT SHL(DWORD#1, stateIndex);
(* Check state active *)
IF (_configuration AND SHL(DWORD#1, stateIndex)) <> 0 THEN ...
For machines with more than 32 states, an ARRAY[0..N] OF DWORD is used automatically.
Event Representation
Events are represented as integer IDs (not strings) for efficient CASE-based dispatch:
TYPE E_MyMachine_Events : (
EVT_NONE := 0,
EVT_START := 1,
EVT_STOP := 2,
EVT_TIMER := 3
) DINT; END_TYPE
Event Queue
A fixed-size ring buffer handles internal events (<raise>) and delayed sends:
(* Ring buffer with configurable depth, default 16 *)
_iqBuf : ARRAY[0..15] OF DINT;
_iqHead : INT := 0;
_iqTail : INT := 0;
_iqCount : INT := 0;
Expression Transformation
Guard conditions and expressions are automatically transformed from ECMAScript-like syntax to ST:
| ECMAScript | Structured Text |
|---|---|
&& |
AND |
|| |
OR |
! |
NOT |
== |
= |
!= |
<> |
true |
TRUE |
false |
FALSE |
In('stateId') |
IsInState(stateIndex := N) |
Event Introspection
(* Returns DWORD bitmask — each bit = one event from the event enum *)
allEvents := fb.GetAllEvents();
enabledEvents := fb.GetEnabledEvents(); (* guard-aware *)
eventsForState := fb.GetEventsForState(S_IDLE);
enabledForState := fb.GetEnabledEventsForState(S_IDLE);
In ST, events are represented as bit positions in a DWORD matching the event enum indices. Use bitwise AND to test individual events.
Limitations
- No ECMAScript datamodel: PLCs have no JavaScript engine. Use
nullornative-stdatamodel. - No system variables:
_event,_sessionid,_nameare not available in ST target. - Early binding only: Late binding is not supported (all data initialized at FB creation).
- No dynamic invoke: Invoke children must be pre-compiled and bundled at generation time.
- Siemens SCL: The
siemensplatform variant is planned but not yet implemented (SCL lacksMETHODsupport, requiring a monolithic CASE architecture). - No runtime tracing: PLC debugging is done through the PLC IDE's online monitoring.
- Timer resolution: Delayed
<send>actions use pre-allocated TON function blocks. Timer resolution is limited to the PLC cycle time (typically 1–100 ms).
PLC Platform Variants
| Platform | CLI Value | Status | Notes |
|---|---|---|---|
| CODESYS | codesys |
Supported | Primary platform, uses METHODs |
| TwinCAT | twincat |
Supported | CODESYS-compatible |
| Siemens | siemens |
Planned | Requires monolithic CASE (no METHODs) |