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
| Function | What 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:
| Path | Names |
|---|---|
"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 andAT %Q...as an output, like theVAR_INPUTandVAR_OUTPUTof 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
PROGRAMentries are the instances, named as declared there (Mainabove, so its variables areMain.<name>). - Without a configuration, each
PROGRAMgets 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
PRIORITYvalue first. - A task with an
INTERVALfirst runs at its first cycle, then everyINTERVALfrom then on. It runs at most once per cycle.
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_BLOCKwithMETHODs,FUNCTION,CONFIGURATION/RESOURCE/TASK, andTYPE(enums, structs and aliases). - Variables:
VAR,VAR_INPUT,VAR_OUTPUT,VAR_IN_OUT,VAR_GLOBAL,VAR_EXTERNALandVAR_TEMP, with initial values andAT %I/%Qlocations. - 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 asT#1s500ms. Assignments convert implicitly, as CODESYS does. - Statements:
IF,CASE,FOR,WHILE,REPEAT,EXIT,CONTINUEandRETURN. - Standard function blocks: timers (
TON,TOF,TPand theirLforms), 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).