Skip to main content

Structured Text (VirtualPLC)

The CppModel library includes VirtualPLC, an interpreter for IEC 61131-3 Structured Text. PLC code is tested the same way as C code: the plant model is written in C, C++, Python or JavaScript, and the controller under test is your .st files. The simulation loads them once. Then, at each step, it sets the PLC's inputs, runs the tasks that are due and reads the PLC's outputs. No PLC runtime or other tool is needed, only the CppModel library.

A tank and its fill controller​

The controller under test opens the valve when the level falls below 20 % and closes it above 80 %:

TYPE Mode : (Idle, Filling, Full); END_TYPE

VAR_GLOBAL
Level AT %IW0 : REAL;
Valve AT %QX0.0 : BOOL;
END_VAR

PROGRAM Tank
VAR_OUTPUT
state : Mode;
END_VAR
IF Level < 20.0 THEN
state := Filling;
ELSIF Level > 80.0 THEN
state := Full;
END_IF
Valve := state = Filling;
END_PROGRAM

The simulation in C is the tank model. It connects the model to the PLC's I/O:

#include <cppmodel/CModel.h>
#include <cppmodel/VirtualPlc.h>

static VirtualPlc_ts *plc;
static double level = 50.0;

CMODEL_CYCLIC()
{
if (plc == NULL)
plc = VirtualPlc_open(TANK_ST); /* loads and starts; NULL after an error */
VirtualPlc_set(plc, "Level", level); /* the PLC's inputs */
VirtualPlc_cycle(plc, currentTime_ms); /* the tasks due at this time */
int valve = VirtualPlc_get(plc, "Valve") != 0; /* its outputs */
level += (valve ? 0.8 : 0.0) - 0.3; /* the plant */
CppModel_setOutputF64(self, "Level", level);
CppModel_setOutputU8(self, "Valve", (uint8_t)valve);
}

CMODEL_SIMULATE("Tank (ST)", 60000, 100)

Build it like any C simulation. Only a C compiler is needed:

find_package(cppmodel REQUIRED)

add_executable(tank tank.c)
target_compile_definitions(tank PRIVATE TANK_ST="${CMAKE_CURRENT_SOURCE_DIR}/Tank.st")
target_link_libraries(tank PRIVATE cppmodel::cppmodel)
cppmodel_deploy_runtime(tank)

A relative .st path is opened from the working directory of the run. Passing the absolute path from CMake, as above, lets the executable run from any folder. The SDK ships this example as a complete project in share/cppmodel/virtualplc-example/ (see Installation).

C++, Python and Node.js​

In C++, cppmodel::VirtualPlc from cppmodel/VirtualPlc.hpp does the same with Set(), Cycle() and Get(), and throws cppmodel::Error when something fails:

cppmodel::VirtualPlc plc{{"Tank.st"}};          // loads and starts
// ...in RunCyclic:
plc.Set("Level", level); // the PLC's inputs
plc.Cycle(simulationTime_ms); // the tasks due
bool valve = plc.Get("Valve") != 0; // its outputs

The Python and Node.js packages have VirtualPlc too.

The C API​

FunctionWhat it does
VirtualPlc_open(file)Loads one .st file and starts the PLC. Returns NULL after an error.
VirtualPlc_create()An empty PLC: load the sources, then start it.
VirtualPlc_load(plc, file)Parses a .st file. Load every file before starting.
VirtualPlc_loadSource(plc, source, name)Parses source text; name is used in error messages.
VirtualPlc_start(plc)Creates the program instances and globals with their initial values. Called again, it restarts the PLC from them.
VirtualPlc_cycle(plc, time_ms)Runs every task due at time_ms.
VirtualPlc_get(plc, path), VirtualPlc_set(plc, path, value)Reads or writes a variable as a number.
VirtualPlc_getString(plc, path), VirtualPlc_setString(plc, path, value)Reads or writes a variable as text.
VirtualPlc_has(plc, path)Whether the path names a variable.
VirtualPlc_variableCount(plc), VirtualPlc_variable(plc, index, ...)The PLC's inputs, outputs, locals and globals.
VirtualPlc_lastError()The message of the last error.
VirtualPlc_destroy(plc)Frees the PLC.

A failed call prints a message to stderr. Functions that return int return nonzero after one.

Variables​

Variables are addressed by path, and names are case-insensitive, as in Structured Text:

PathNames
"Main.Motor"variable Motor of program instance Main
"Pump"global Pump
"Main.Levels[2]"an array element
"Main.Axis.Position"a struct member
  • As a number: BOOL is 0 or 1, TIME is milliseconds and an enum is its value. Integers wrap at their type's width when set.
  • As text: any scalar (TRUE, 42, 1.5, T#1s500ms). A STRING can only be read and written as text.
  • Only scalars can be read or written, not a whole array or struct.
  • A global AT %I... is listed as an input and AT %Q... as an output, like the VAR_INPUT and VAR_OUTPUT of a program.

Tasks and cycle timing​

Tasks come from a CONFIGURATION in the sources:

CONFIGURATION Cell
TASK Fast (INTERVAL := T#10ms, PRIORITY := 1);
PROGRAM Main WITH Fast : MotorStarter;
END_CONFIGURATION
  • With a configuration, its PROGRAM entries are the instances, named as declared there (Main above, so its variables are Main.<name>).
  • Without a configuration, each PROGRAM gets an instance with its own name that runs on every cycle.
  • A program without a task runs on every cycle, after the configured tasks.
  • Each cycle runs the tasks due at the time given, lowest PRIORITY value first.
  • A task with an INTERVAL first runs at its first cycle, then every INTERVAL from then on. It runs at most once per cycle.
note

The PLC runs only when the simulation calls cycle, with the simulation time it passes. To run a task at its configured interval, use a simulation step no longer than the shortest INTERVAL.

Supported Structured Text​

  • Program organization units: PROGRAM, FUNCTION_BLOCK with METHODs, FUNCTION, CONFIGURATION/RESOURCE/TASK, and TYPE (enums, structs and aliases).
  • Variables: VAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_GLOBAL, VAR_EXTERNAL and VAR_TEMP, with initial values and AT %I/%Q locations.
  • Data types: BOOL, the signed and unsigned integers and bit strings (wrapping at their width), REAL, LREAL, TIME, LTIME, STRING, WSTRING, multi-dimensional arrays, enums, structs and aliases, and typed literals such as T#1s500ms. Assignments convert implicitly, as CODESYS does.
  • Statements: IF, CASE, FOR, WHILE, REPEAT, EXIT, CONTINUE and RETURN.
  • Standard function blocks: timers (TON, TOF, TP and their L forms), edge triggers (R_TRIG, F_TRIG), bistables (SR, RS) and counters (CTU, CTD, CTUD).
  • Standard functions: numeric, arithmetic, comparison, selection, bit shift, string and type conversion functions.

Not supported: pointers and references, inheritance (EXTENDS, INTERFACE, PROPERTY, SUPER), bit access (x.3), the date and time-of-day types, CHAR and NAMESPACE. The SDK header VirtualPlc.hpp lists exactly what is supported.

Errors​

A syntax error in the sources, or a fault while cycling, such as a division by zero or an index out of range, is reported with the .st file and line. In C, VirtualPlc_open() returns NULL and the other functions return nonzero; in C++, Python and Node.js an error is thrown.

Recorded runs​

CppModel.BinaryFingerprint identifies the simulation executable, not the .st files it loads, so a change to the Structured Text alone doesn't change it. Compare CppModel.GitHash instead: its -dirty suffix shows uncommitted changes to tracked files (see What a run records).