Structured Text (IEC 61131-3) Target Documentation

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:

  1. Generates .st files from W3C test SCXML sources
  2. Batches all tests into a single CODESYS project
  3. Runs compilation and simulation via CODESYS scripting API
  4. Reports pass/fail for each test
bash
# 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 METHOD inside FUNCTION_BLOCK

Code Generation

Generate Structured Text code using the CLI:

bash
# 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

json
{
  "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/PRIVATE access modifiers on METHOD declarations
  • No typed enumerations (uses plain ); instead of ) DINT;)
  • No explicit enum values (sequential from 0 is the IEC default)
bash
# 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:

xml
<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:

pascal
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:

xml
<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:

xml
<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:

xml
<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:

bash
# 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:

pascal
(* 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:

pascal
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:

pascal
(* 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

iec-st
(* 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

  1. No ECMAScript datamodel: PLCs have no JavaScript engine. Use null or native-st datamodel.
  2. No system variables: _event, _sessionid, _name are not available in ST target.
  3. Early binding only: Late binding is not supported (all data initialized at FB creation).
  4. No dynamic invoke: Invoke children must be pre-compiled and bundled at generation time.
  5. Siemens SCL: The siemens platform variant is planned but not yet implemented (SCL lacks METHOD support, requiring a monolithic CASE architecture).
  6. No runtime tracing: PLC debugging is done through the PLC IDE's online monitoring.
  7. 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)