ecmc_plugin_strucpp is a generic ecmc host plugin for running
loadable STruCpp logic libraries inside the normal ecmc realtime loop.
In this repo, ST means Structured Text as defined by IEC 61131-3,
the PLC programming-language standard. It does not mean EPICS sequencer
State Notation Language .st files.
The plugin is meant to run ST code like this:
PROGRAM MAIN
VAR
actual_position AT %IW0 : INT; // @ecmc ec.s${SLAVE_ID=14}.positionActual${CH_ID}
drive_control AT %QW0 : WORD; // @ecmc ec.s${SLAVE_ID=14}.driveControl${CH_ID}
velocity_setpoint AT %QW2 : INT; // @ecmc ec.s${SLAVE_ID=14}.velocitySetpoint${CH_ID}
cycle_counter AT %MW0 : INT; // @epics rec=Main-CycleCounterAct
END_VAR
cycle_counter := cycle_counter + 1;
drive_control := 16#0001;
IF actual_position < 0 THEN
velocity_setpoint := 1000;
ELSIF actual_position > 1000 THEN
velocity_setpoint := -1000;
END_IF;
END_PROGRAM
That example reads and writes named ecmc data items through %I/%Q, keeps
internal state in %M, and exposes one variable to EPICS with @epics.
If you want simple IOC-shell debug output from ST while bringing logic up,
the repo also ships a small helper in lib/ecmc_debug.st.
When you use the shared IOC build helper, this ST file is included by default:
VAR
dbgMoveDone : ECMC_DebugPrint;
END_VAR
dbgMoveDone(Execute := move_done,
Message := CONCAT('pos=', TO_STRING(actual_position)));
ECMC_DebugPrint prints one line on the rising edge of Execute, so it is
usable for ad-hoc tracing without flooding every cycle. The shared IOC build
helper also links the required C++ debug shim automatically. If you want to
disable the default ST helper include, set INCLUDE_DEBUG_ST := 0. Debug
printouts are disabled by default and only appear when ctrl.word bit 2 is
set. When enabled, the latest short debug message is also published on the
plugin's own asyn interface as Plg-ST0-DbgTxtAct. Timing measurements are
available on ctrl.word bit 1.
If you want an unconditional print whenever the call is executed, the same
library also provides ECMC_DebugPrintNow(Message := '...').
The shared IOC build helper also includes a small bundled control helper in
lib/ecmc_control.st:
VAR
pid : ECMC_PID;
END_VAR
pid(Setpoint := 12800.0,
Actual := actual_pos,
FF := vel_ff,
Kp := 1.0,
Ki := 0.01,
OutMin := -2000.0,
OutMax := 2000.0);
ECMC_PID is a simple ST PID controller with:
- feed-forward input
FFand gainKff - output limiting through
OutMin/OutMax - integrator limiting through
IMin/IMax - anti-windup when the output saturates
- optional derivative filtering through
DFilterTau - bumpless first enable cycle with derivative state reset
Defaults are chosen so the FB can be called with only the parameters you actually care about:
Enable := TRUEKp := 1.0Ki := 0.0Kd := 0.0Kff := 1.0DT := 1.0DFilterTau := 0.0
Output and integrator limits are disabled unless Max > Min. If you want to
disable the default control-helper include, set INCLUDE_CONTROL_ST := 0.
The shared IOC build helper also includes a bundled utility library in
lib/ecmc_utils.st. It complements the normal IEC
standard FBs, so TON, TOF, TP, R_TRIG, and similar blocks still come
from the standard ST library and are not redefined here.
The bundled ECMC_* utility additions are:
ECMC_DebounceBoolECMC_ApplyDeadbandECMC_ClampECMC_InWindowECMC_GetCycleTimeSECMC_RateLimiterECMC_FirstOrderFilterECMC_HysteresisBoolECMC_IntegratorECMC_EcMasterStatusECMC_EcSlaveStatusECMC_AxisGetTrajSourceECMC_AxisGetEncSourceECMC_AxisGetActualPosECMC_AxisGetSetpointPosECMC_AxisGetActualVelECMC_AxisGetSetpointVelECMC_AxisIsEnabledECMC_AxisIsBusyECMC_AxisHasErrorECMC_AxisGetErrorIdECMC_AxisSetTrajSourceECMC_AxisSetEncSourceECMC_AxisUseInternalTrajECMC_AxisUseExternalTrajECMC_AxisUseInternalEncECMC_AxisUseExternalEncECMC_AxisSetExternalSetpointPosECMC_AxisSetExternalEncoderPos
Example:
VAR
dt : LREAL;
filt : ECMC_FirstOrderFilter;
hyst : ECMC_HysteresisBool;
integ : ECMC_Integrator;
statusM : ECMC_EcMasterStatus;
statusS : ECMC_EcSlaveStatus;
axisPos : LREAL;
setErr : DINT;
END_VAR
dt := ECMC_GetCycleTimeS();
filt(Input := velocity_cmd, Tau := 0.02, DT := dt);
hyst(In := axis_load, Low := 20.0, High := 30.0);
integ(In := ctrl_err, K := 0.5, DT := dt, Min := -10.0, Max := 10.0);
statusM();
statusS(SlaveId := 14);
axisPos := ECMC_AxisGetActualPos(AxisId := 1);
setErr := ECMC_AxisUseExternalTraj(AxisId := 1);
setErr := ECMC_AxisSetExternalSetpointPos(AxisId := 1, Position := kin_pos_cmd);
ECMC_EcMasterStatus and ECMC_EcSlaveStatus expose generic EtherCAT state
without requiring you to map device-specific status words manually. For
terminal or drive-specific status information like DS402 status words, direct
@ecmc mapping is still the preferred path.
The axis getter and source helpers are intended for kinematics and
synchronization paths where ST needs to supervise axis state and, when
required, own the external trajectory or encoder values. The
...UseInternal... and ...UseExternal... helpers avoid raw source constants
in common ST code, while ECMC_AxisSetTrajSource and ECMC_AxisSetEncSource
remain available when you want the explicit numeric source value.
There is intentionally no separate ECMC_SlewToTarget, since
ECMC_RateLimiter already covers that use case. I also did not add a
dedicated pulse-stretch block because the standard IEC timer blocks TP and
TOF already cover that behavior well.
A compact helper overview is available in:
Detailed per-library reference pages are available in:
This is not limited to one flat PROGRAM. Normal IEC 61131-3
FUNCTION_BLOCKs, FUNCTIONs, and reusable helper code can be used as well.
The repo also ships a bundled motion library with PLCopen-style MC_* blocks
such as MC_Power, MC_MoveAbsolute, MC_MoveVelocity, and
MC_ReadActualPosition. These are used like normal ST function blocks with
instance state that is evaluated cyclically inside the main program, for
example:
VAR
axis : ECMC_AXIS_REF;
power : MC_Power;
moveAbs : MC_MoveAbsolute;
END_VAR
axis.AxisIndex := 1;
power(Axis := axis, Enable := enable_cmd);
moveAbs(Axis := axis, Execute := execute_cmd, Position := 1000.0, Velocity := 200.0);
The split is:
ecmc_plugin_strucppbuilds the generic host plugin loaded byecmc- your ST application
builds a separate shared library with generated
STruCppcode and a small wrapper that exposes a fixed C ABI
The host plugin uses the standard ecmc plugin interface. It does not add a
new plugin system. At runtime it:
- links the current ST program's
%I/%Q/%Maddresses to configuredecmcbuffers at realtime entry - runs one logic cycle in the normal
ecmcloop - executes a precompiled pointer copy plan for the configured binding mode
The copy work is compiled once at realtime entry into a direct pointer-based copy plan. The realtime loop only executes that plan.
The plugin is already usable for real IOC work, but it should still be treated as a relatively new integration layer. The current examples, build helpers, and validation tools cover the intended workflow well, but runtime coverage across different IOC setups and hardware combinations is still limited. It is a good idea to validate each application path on the target system before relying on it in routine operation.
For the fastest path to a working IOC, start with the PSI-style minimal example:
That example is intentionally small and shows the preferred default workflow:
- one ST source file:
src/main.st - direct
@ecmcmapping to EtherCAT items - optional
@epicsexport from the same ST file - default output files in
bin/:main.so,main.so.map,main.so.substitutions - a dedicated IOC-level substitutions file:
<IOC>_strucpp.subs - startup with a simple
require ecmc_plugin_strucpp ...
Typical flow:
- edit
src/main.st - run
make - run
ioc install --clean -V --ioc MINIMAL-STRUCPP-IOC - start the IOC with
MINIMAL-STRUCPP-IOC_startup.script
By default, the example root Makefile also generates a simple IOC-local
caQtDM panel from the exported @epics records:
make
caqtdm qt/MINIMAL-STRUCPP-IOC_strucpp.uiIf you do not want that panel, build with:
make GENERATE_QT=0If you want a slightly richer example with helper FBs, custom record naming, and a split source layout, use:
The shared IOC build helper includes lib/ecmc_control.st,
lib/ecmc_utils.st, and lib/ecmc_debug.st by default. Motion helpers are
available too, but are opt-in so they do not shadow any external MC_*
library you may already be using. Enable them with:
INCLUDE_MOTION_ST := 1When motion helpers are enabled, the shared build helper also adds
-L $(ECMC_PLUGIN_STRUCPP)/libs for strucpp and the ecmcMcApi.h include
path from $(ECMC)/devEcmcSup/motion for the C++ compile. Override ECMC in
your app Makefile if your checkout layout differs.
This repo now ships a reusable ST motion library:
Application repos can consume it directly with -L $(ECMC_PLUGIN_STRUCPP)/libs
instead of rebuilding a private copy of the motion library.
The current motion blocks follow the common PLCopen naming and calling pattern,
but they should be understood as PLCopen-style interfaces implemented on top
of ecmc, not as a formal or complete PLCopen compliance claim.
The first useful block set currently includes:
MC_PowerMC_ResetMC_MoveAbsoluteMC_MoveRelativeMC_MoveVelocityMC_HomeMC_HaltMC_ReadStatusMC_ReadActualPositionMC_ReadActualVelocity
The bundled library currently uses a simple integer-based axis reference:
TYPE ECMC_AXIS_REF :
STRUCT
AxisIndex : DINT;
END_STRUCT
END_TYPE
All MC_* blocks take Axis : ECMC_AXIS_REF and are intended to be called
cyclically like normal IEC 61131-3 function blocks.
Current ST interfaces:
MC_PowerInputs:Axis,EnableOutputs:Status,Valid,Busy,Error,ErrorID,ActiveMC_ResetInputs:Axis,ExecuteOutputs:Done,Busy,Error,ErrorID,ActiveMC_MoveAbsoluteInputs:Axis,Execute,Position,Velocity,Acceleration,DecelerationOutputs:Done,Busy,Active,CommandAborted,Error,ErrorIDMC_MoveRelativeInputs:Axis,Execute,Distance,Velocity,Acceleration,DecelerationOutputs:Done,Busy,Active,CommandAborted,Error,ErrorIDMC_MoveVelocityInputs:Axis,Execute,Velocity,Acceleration,DecelerationOutputs:InVelocity,Busy,Active,CommandAborted,Error,ErrorIDMC_HomeInputs:Axis,Execute,SeqId,HomePosition,VelocityTowardsCam,VelocityOffCam,Acceleration,DecelerationOutputs:Done,Busy,Active,CommandAborted,Error,ErrorIDMC_HaltInputs:Axis,ExecuteOutputs:Done,Busy,Active,CommandAborted,Error,ErrorIDMC_ReadStatusInputs:Axis,EnableOutputs:Valid,Busy,Error,ErrorID,ErrorStop,Disabled,Stopping,Homing,StandStill,DiscreteMotion,ContinuousMotion,SynchronizedMotionMC_ReadActualPositionInputs:Axis,EnableOutputs:Valid,Busy,Error,ErrorID,PositionMC_ReadActualVelocityInputs:Axis,EnableOutputs:Valid,Busy,Error,ErrorID,Velocity
The source of truth for these interfaces is:
If the ST source changes, rebuild the bundled .stlib with:
./scripts/build_motion_stlib_container.shThat recompiles the library with strucpp in a container and reapplies the
required ecmcMcApi.h header metadata.
This repo also ships a small caQtDM panel for the built-in plugin control and status PVs:
Example:
caqtdm -macro "IOC=c6025a-04,PLG_ID=0" /path/to/ecmc_plugin_strucpp/qt/ecmc_plugin_strucpp_main.uiThe plugin is intentionally generic, but it can currently only be loaded once. Load one host instance and point it at one ST logic library.
If you later need several independent ST logic modules in one IOC, the next step is to add an object-registration layer on top of this host. The installed ABI and wrapper headers already support that extension.
The panel targets the built-in records such as:
$(IOC):Plg-ST$(PLG_ID)-CtrlWord-RB$(IOC):Plg-ST$(PLG_ID)-SmpMs-RB$(IOC):Plg-ST$(PLG_ID)-ExeMsAct
This repo now also ships a reusable IOC/app-side build helper:
The intent is to remove most per-IOC boilerplate. A small src/Makefile can
now usually be just:
PROGRAM := machine
ECMC_PLUGIN_STRUCPP ?= ../../../ecmc_plugin_strucpp
include $(ECMC_PLUGIN_STRUCPP)/templates/strucpp_ioc_logic.makeFor multi-file ST projects, set ST_SOURCES in declaration order, for example:
PROGRAM := machine
ST_SOURCES := machine_types.st machine_fbs.st machine.st
ECMC_PLUGIN_STRUCPP ?= ../../../ecmc_plugin_strucpp
include $(ECMC_PLUGIN_STRUCPP)/templates/strucpp_ioc_logic.makeThe helper bundles those files into one generated ST source before running
strucpp, so helper FBs, types, and the final PROGRAM can live in separate
files. The same ${NAME} and ${NAME=default} placeholder syntax can be
expanded across all bundled ST source files before code generation.
If you need handwritten C++ in the same logic library, the helper also supports two escape hatches:
PROGRAM := machine
ST_SOURCES := machine_fbs.st machine.st
WRAPPER_CPP := custom_logic_wrapper.cpp
EXTRA_CPP_SOURCES := helper.cpp adapters/custom_fb.cpp
ECMC_PLUGIN_STRUCPP ?= ../../../ecmc_plugin_strucpp
include $(ECMC_PLUGIN_STRUCPP)/templates/strucpp_ioc_logic.makeWRAPPER_CPPoverrides the generated wrapper with your own C++ wrapperEXTRA_CPP_SOURCEScompiles extra handwritten C++ translation units into the same logic library
The sample IOC project in
examples/psi_ioc_examples/ioc_project_example
now includes concrete opt-in files for this path:
src/Makefile.with_cppsrc/custom_logic_wrapper.cppsrc/machine_helper.cpp
For a PSI-style IOC example that uses the bundled MC_* motion library and
exports EPICS-triggered command bits/values, see:
When WRAPPER_CPP points at your own file, the helper stops generating the
default wrapper and compiles the provided source instead.
That common include handles:
- ordered ST source bundling
strucppcode generation- export header generation from
// @epics ... - mapping file generation from
// @ecmc ... - substitutions generation from
// @epics ... - logic wrapper generation
- logic library build
- staging of
bin/<logic>.so,.map, and.substitutions
So the IOC project usually only needs:
- one ST source file
- one short
src/Makefile - one startup script with
require ecmc_plugin_strucpp ...
and only drops to handwritten C++ when you explicitly opt into one of the escape hatches above.
For the smoothest default path, this repo also ships a small scaffold tool:
Example:
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_new_ioc.py my_iocThat creates a minimal IOC project with:
src/main.stsrc/Makefile<IOC_NAME>_startup.script<IOC_NAME>_parameters.yaml- a top-level
Makefile
The generated scaffold follows the shortest current convention:
PROGRAM := mainLOGIC_NAME := main- startup loads
bin/main.so - mapping defaults to
bin/main.so.map - EPICS substitutions default to
bin/main.so.substitutions
The generated ST source uses direct EL7041 item mapping as a concrete example.
Adjust the // @ecmc ... lines to match the real slave and PDO items for your
machine.
To reduce the remaining manual work for direct mapping, this repo also ships a small declaration generator:
It takes a small manifest like:
I INT actual_position ec.s14.positionActual01
Q WORD drive_control ec.s14.driveControl01
Q INT velocity_setpoint ec.s14.velocitySetpoint01
M INT cycle_counter
VAR INT manual_velocity
and generates ST declarations with automatic %I/%Q/%M addresses:
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_declgen.py \
--input my_axis.manifest \
--output main.st \
--program MAINThat produces:
PROGRAM MAIN
VAR
actual_position AT %IW0 : INT; // @ecmc ec.s14.positionActual01
drive_control AT %QW0 : WORD; // @ecmc ec.s14.driveControl01
velocity_setpoint AT %QW2 : INT; // @ecmc ec.s14.velocitySetpoint01
cycle_counter AT %MW0 : INT;
manual_velocity : INT;
END_VAR
// Generated from my_axis.manifest. Add logic below.
END_PROGRAM
Current behavior:
%I/%Q/%Maddresses are assigned automaticallyBOOLvalues are bit-packed as%IXn.m/%QXn.m/%MXn.m- wider scalar types are byte-aligned and naturally aligned by width
VARentries generate plain non-located ST variables
To reduce the manual work even before declgen, this repo also ships:
It takes a short list of ecmc item names and creates a first-draft manifest
for strucpp_declgen.py.
Example input:
ec.s14.positionActual01
ec.s14.driveControl01
ec.s14.velocitySetpoint01
Example command:
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_manifestgen.py \
--input items.txt \
--output axis.manifestExample output:
# AREA TYPE NAME ECMC_ITEM
I INT actual_position ec.s14.positionActual01
Q WORD drive_control ec.s14.driveControl01
Q INT velocity_setpoint ec.s14.velocitySetpoint01
The generator is heuristic by design. It is meant to produce a useful first draft that you edit as needed, not a perfect type inference engine.
For a single front-door entry point, this repo now also ships:
It wraps the common workflows behind subcommands:
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_app_tool.py new-ioc my_ioc
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_app_tool.py manifest --input items.txt --output axis.manifest
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_app_tool.py declgen --input axis.manifest --output src/main.st
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_app_tool.py build --project my_ioc
python3 /path/to/ecmc_plugin_strucpp/scripts/strucpp_app_tool.py validate --project my_iocBehavior:
new-iocwrapsstrucpp_new_ioc.pydeclgenwrapsstrucpp_declgen.pymanifestwrapsstrucpp_manifestgen.pybuildrunsmake <target>in the chosen project directoryvalidateruns the helper'smake validate, defaulting to<project>/srcwhen that layout exists
Useful options:
--dry-runpasses-ntomake--make-arg STRUCPP=/path/to/strucppforwards extra make variable assignments
The scaffold only picks the smallest default shape. The generated
src/Makefile still uses the shared helper, so you can later extend it with:
ST_SOURCES := types.st fbs.st main.stWRAPPER_CPP := custom_wrapper.cppEXTRA_CPP_SOURCES := helper.cppANNOTATION_DEFINES := AXIS_INDEX=2
The same helper now also generates:
${LOGIC_LIB}.summary.txt
and supports a dry-run validation target:
make validateThat validation step checks the generated header, mapping file, and substitutions file against the bundled ST source before runtime.
The host expects the normal Cfg.LoadPlugin(...) config string format:
logic_lib=<path>;asyn_port=<plugin asyn port>;[mapping_file=<path>|input_item=<ecmc data item>|input_bindings=<offset:item[@bytes],...>];[output_item=<ecmc data item>|output_bindings=<offset:item[@bytes],...>];memory_bytes=<n>;sample_rate_ms=<n>;validate_report=<0|1>;run_before_epics_started=<0|1>
Startup-linked mapping example:
logic_lib=/abs/path/to/el7041_velocity_logic.so;asyn_port=PLUGIN.STRUCPP0;mapping_file=/abs/path/to/el7041_velocity_logic.so.map;memory_bytes=16
Contiguous image example:
logic_lib=/abs/path/to/machine_logic.so;input_item=ec0.s2.mm.inputDataArray01;output_item=ec0.s2.mm.outputDataArray01;memory_bytes=64
Direct scalar binding example:
logic_lib=/abs/path/to/el7041_velocity_logic.so;input_bindings=0:ec.s14.positionActual01@2;output_bindings=0:ec.s14.driveControl01@2,2:ec.s14.velocitySetpoint01@2;memory_bytes=16
logic_libabsolute path to the loadable logic librarymapping_filepath to a startup-linked manifest that maps exact STruCpp addresses like%IW0or%QW2toecmcDataItemnamesasyn_portdedicated plainasynPortDriverport owned byecmc_plugin_strucpp, defaults toPLUGIN.STRUCPP0input_itemecmcDataItemused as one contiguous%I*byte imageoutput_itemecmcDataItemused as one contiguous%Q*byte imageinput_bindingsdirect%I*bindings in the form<offset>:<item>[@bytes],...output_bindingsdirect%Q*bindings in the form<offset>:<item>[@bytes],...memory_bytesoptional%M*backing store size, defaults to256sample_rate_msrequested ST logic sample period in milliseconds, defaults to the full EtherCAT/plugin ratevalidate_reportoptional live startup validation report, defaults to0run_before_epics_startedoptional override for early execution, defaults to0; when left at0, the plugin skips STrun_cycle()calls until EPICS startup is complete
Use mapping_file when you want the plugin to inspect the current ST code at
startup, verify every used %I/%Q address, and link it directly to final
ecmcDataItem buffers once before the RT loop starts. Use input_item /
output_item when your logic naturally maps to one contiguous byte image, for
example an existing memmap. Use input_bindings / output_bindings when you
want explicit offset-to-item control without an external manifest. The EL6002
example in this repo intentionally uses contiguous images. The EL7041 example
and the IOC project examples use mapping_file and are the preferred default
pattern for new projects.
sample_rate_ms lets the host derive an integer execute divider from the
current ecmc sample time before realtime starts. The EtherCAT master still
runs at full rate, but the ST logic is only sampled/copied/run every Nth
cycle. For example, with a 1 ms EtherCAT period and sample_rate_ms=10, the
host derives execute_divider=10 and the ST logic runs every 10 ms.
validate_report=1 prints a compact live report of the resolved startup
bindings against the current ecmc items before realtime starts. This is
useful during bring-up when you want to confirm item names, directions, sizes,
and direct %I/%Q mappings from the actual IOC environment.
By default the plugin does not execute ST logic before EPICS startup has
completed. Set run_before_epics_started=1 only when you explicitly want
early startup execution. The ST helper ECMC_EpicsStarted() remains available
for logic that needs to branch on startup state even when early execution is
enabled.
If MAPPING_FILE is not provided and none of INPUT_ITEM, OUTPUT_ITEM,
INPUT_BINDINGS, or OUTPUT_BINDINGS are set, the plugin defaults the
mapping file to:
${LOGIC_LIB}.map
That is the preferred convention. App repos should generate the map next to the
logic library so the normal startup path needs no extra mapping macro, and in
the standard IOC layout can omit LOGIC_LIB too.
In mapping_file mode the host now checks, at startup:
%Ivs%Qdirection againstecmcDataItemInfo.dataDirection- exact byte width for direct scalar mappings
- compatible
ecmcEcDataTypefamily for the located width
The current logic ABI still only exposes located width, not the full IEC scalar
type, so the host can validate "64-bit compatible" but not distinguish LREAL
from LWORD when both use %IL.
The mapping-file format is intentionally small:
# comments are allowed
%IW0=ec.s14.positionActual01
%QW0=ec.s14.driveControl01
%QW2=ec.s14.velocitySetpoint01
The names in that file are read directly by the plugin. A leading ec.s...
means "use the current/default EtherCAT master index from ecmc before
realtime", so ec.s14.positionActual01 resolves to ec0.s14.positionActual01
or ec1.s14.positionActual01 depending on the configured master. Explicit
names like ec0.s14... and ec1.s14... remain valid and are not rewritten.
To avoid maintaining %IW0 / %QW2 addresses by hand, this repo also ships a
small generator in scripts/strucpp_mapgen.py.
The generators now also perform stronger checks before runtime, including:
- malformed
@ecmc/@epicsannotations - duplicate located addresses in generated forwards
- duplicate
@epicsexport names - conflicting mapping entries
- summary warnings for overlapping located addresses
ANNOTATION_DEFINES is applied while bundling ST source files and while
processing generated annotation metadata. For example, motion samples can use:
actual_position AT %IL0 : LREAL; // @ecmc ax${AXIS_INDEX}.enc.actpos
and the build helper can supply:
ANNOTATION_DEFINES := AXIS_INDEX=2so the generated map resolves to ax2.enc.actpos without editing the ST code.
Inline defaults are also supported in annotations, for example:
actual_position AT %IW0 : INT; // @ecmc ec.s${SLAVE=14}.positionActual01
drive_control AT %QW0 : WORD; // @ecmc ec${MASTER=0}.s${SLAVE=14}.driveControl01
and values from ANNOTATION_DEFINES still override those defaults:
ANNOTATION_DEFINES := SLAVE=18 MASTER=1It reads the generated STruCpp header forwards and, preferably, inline ST
annotations of the form:
actual_position AT %IW0 : INT; // @ecmc ec.s14.positionActual01
drive_control AT %QW0 : WORD; // @ecmc ec.s14.driveControl01
velocity_setpoint AT %QW2 : INT; // @ecmc ec.s14.velocitySetpoint01
and emits the final startup-linked mapping file automatically. It also still
accepts an external VAR_NAME=ecmcDataItem bindings file when you do not want
to keep the metadata in the ST source.
The plugin also exports numeric-only exprtk PLC helper functions through the
normal ecmcPluginData.funcs[] interface. These helpers operate directly on
the plugin's linked %I, %Q, and %M byte images.
This is the lightweight bridge between existing ecmc PLC / exprtk code and
ST logic running inside ecmc_plugin_strucpp. It lets an ecmc PLC task read
or write the same linked process image that the ST program uses.
Area selector values:
0=%I1=%Q2=%M
Available functions:
strucpp_get_bit(area, byte_offset, bit_index)strucpp_set_bit(area, byte_offset, bit_index, value)strucpp_get_u8(area, byte_offset)strucpp_set_u8(area, byte_offset, value)strucpp_get_s8(area, byte_offset)strucpp_set_s8(area, byte_offset, value)strucpp_get_u16(area, byte_offset)strucpp_set_u16(area, byte_offset, value)strucpp_get_s16(area, byte_offset)strucpp_set_s16(area, byte_offset, value)strucpp_get_u32(area, byte_offset)strucpp_set_u32(area, byte_offset, value)strucpp_get_s32(area, byte_offset)strucpp_set_s32(area, byte_offset, value)strucpp_get_f32(area, byte_offset)strucpp_set_f32(area, byte_offset, value)strucpp_get_f64(area, byte_offset)strucpp_set_f64(area, byte_offset, value)
Example:
var status;
status := strucpp_get_u16(0, 0); // read %IW0
strucpp_set_u16(1, 0, 16); // write %QW0
strucpp_set_bit(2, 4, 0, 1); // set %MX4.0
Out-of-range access returns NaN.
The host can also publish selected non-located ST variables as normal EPICS asyn parameters on the plugin's own dedicated asyn port. The intent is that the declaration lives in the ST source, not in a second C++ config list.
Annotation shape:
counter : INT; // @epics
manual_target : INT; // @epics rw
special_name : INT; // @epics custom.path.value
other_name : INT; // @epics custom.path.value rw
short_rec : INT; // @epics rec=Main-ShortRec
custom_pfx : INT; // @epics prefix=$(IOC): rec=Main-CustomPfx
named_asyn : INT; // @epics custom.path.value prefix=$(IOC): rec=Main-FullOverride rw
cmd_enable : BOOL; // @epics rw rec=Main-Cmd bit=0
cmd_execute : BOOL; // @epics rw rec=Main-Cmd bit=1
stat_busy : BOOL; // @epics rec=Main-Stat bit=0
stat_error : BOOL; // @epics rec=Main-Stat bit=1
Rules:
- the annotation lives on the ST variable declaration line
@epicswith no explicit name derives:plugin.strucpp0.<program>.<variable>- the first token after
@epics, when present, is the explicit exported asyn parameter name override rec=<record-suffix>optionally overrides the generated record suffix while still using the normalPprefixprefix=<PV-prefix>optionally overrides the prefix used for that one recordbit=<0..31>onBOOLdeclarations enables packed bitfield export; usegroup=<name>to choose the internal/asyn group name, or just userec=<name>and the same value will be used as the group whengroup=is omitted- packed
BOOLexports become one exportedUInt32Digitalvalue and one generatedmbbiDirectormbboDirectrecord - for example:
rec=Main-Valueprefix=$(IOC): rec=Main-Value - packed
BOOLexports require explicitbit=numbering and keep read-only and writable groups separate - optional final token
rwmakes the parameter writable from EPICS - default is read-only
- current support is for top-level program variables:
BOOL,SINT,USINT,BYTE,INT,UINT,WORD,DINT,UDINT,DWORD,REAL,LREAL; grouped export packing is available only forBOOL - duplicate exported names are rejected at startup unless they are explicit
members of the same grouped
BOOLexport
The application repo runs
scripts/strucpp_epics_exportgen.py to
turn those annotations into a small generated export header. The logic library
then exposes that export table through the logic ABI, and the host creates
matching asyn parameters at startup on the configured asyn_port.
The current implementation uses a plugin-owned plain asynPortDriver, not the
main ecmc asyn driver. Exported values are updated on change, and callback
flushing is deferred out of the RT loop through a small low-priority worker
thread in the plugin.
The plugin also publishes a small built-in control/status set on the same port:
plugin.strucpp0.ctrl.wordplugin.strucpp0.ctrl.rate_msplugin.strucpp0.stat.rate_msplugin.strucpp0.stat.exec_msplugin.strucpp0.stat.input_msplugin.strucpp0.stat.output_msplugin.strucpp0.stat.total_msplugin.strucpp0.stat.divplugin.strucpp0.stat.countplugin.strucpp0.stat.dbg_txt
The default substitutions file maps those internal asyn parameter names to shorter plugin-style record names:
Plg-ST0-CtrlWord-RBPlg-ST0-SmpMs-RBPlg-ST0-SmpMsActPlg-ST0-ExeMsActPlg-ST0-InMsActPlg-ST0-OutMsActPlg-ST0-TotMsActPlg-ST0-DivActPlg-ST0-CntActPlg-ST0-DbgTxtAct
ctrl.word uses:
- bit 0: enable ST execution
- bit 1: enable all timing measurements
- bit 2: enable ST debug prints
stat.exec_ms is the last measured ST execution time and is only updated while
the measurement bit is enabled.
stat.input_ms is the last measured input-side overhead before run_cycle(),
including binding gathers and %I copy-plan work, and is only updated while
the timing bit is enabled.
stat.output_ms is the last measured output-side overhead after run_cycle(),
including %Q copy-plan work, binding scatter, and export sync, and is only
updated while the timing bit is enabled.
stat.total_ms is the last measured total plugin cycle time around the ST
execution path, including plugin-side copying and export work, and is only
updated while the timing bit is enabled.
stat.count is intentionally rate-limited to a maximum PV update rate of
10 Hz.
This repo also ships macro-based EPICS database templates in db and a
generator,
scripts/strucpp_epics_substgen.py,
that turns the same // @epics ... annotations into a .substitutions file.
The generated substitutions keep the full internal asyn name in ASYN, but
derive a shorter EPICS record name in REC, following the other plugin
templates more closely. Grouped BOOL exports generate packed
mbbiDirect/mbboDirect records automatically.
In addition, the repo ships a default built-in substitutions file:
startup.cmd loads that built-in substitutions file by default unless
LOAD_DEFAULT_PVS=0 is passed in the require macro string.
That lets an application repo keep the ST source as the single source of truth for both:
- exported plugin-owned asyn parameters
- EPICS records connected to those parameters
Typical generated output is loaded like:
dbLoadTemplate("/absolute/path/to/machine_logic.so.substitutions",
"P=IOC:,PORT=PLUGIN.STRUCPP0")
The generated substitutions reference these generic templates:
db/ecmcStrucppBi.templatedb/ecmcStrucppBo.templatedb/ecmcStrucppLongIn.templatedb/ecmcStrucppLongOut.templatedb/ecmcStrucppAi.templatedb/ecmcStrucppAo.template
The plugin installs these public headers from src:
Those are the only pieces an application-side logic library needs from this repo.
ecmcStrucppMcWrapper.hpp is the first plugin-side bridge for the new
PLCopen-style runtime now being added to ecmc. It exposes simple C++ wrapper
objects like ecmcStrucpp::mc::MC_Power, MC_MoveAbsolute, and
MC_ReadStatus, while the actual motion semantics remain in ecmc through
ecmcMcApi.h.
Example shape:
#include "ecmcStrucppMcWrapper.hpp"
ecmcStrucpp::mc::AxisRef axis1{0};
ecmcStrucpp::mc::MC_Power power;
ecmcStrucpp::mc::MC_MoveVelocity move_vel;
power.run(axis1, true);
move_vel.run(axis1, true, 1000.0, 10000.0, 10000.0);For a new motion app, start from:
That gives you the minimum ST program plus logic wrapper pattern. In most cases you only need to:
- set
axis.AxisIndex - adjust the
%I/%Qlocated layout - rename the program and wrapper identifiers
If a generated generated/<program>_epics_exports.hpp file exists, the wrapper
template now picks it up automatically and switches to the export-aware logic
ABI without any extra hand edit in the wrapper.
Each logic library exports one symbol:
extern "C" const ecmcStrucppLogicApi* ecmc_strucpp_logic_get_api();Most applications should use the wrapper macro:
#include "ecmcStrucppLogicWrapper.hpp"
#include "my_program.hpp"
ECMC_STRUCPP_DECLARE_LOGIC_API("my_logic",
strucpp::Program_MYPROGRAM,
strucpp::locatedVars);That wrapper is designed to sit next to generated STruCpp output in your
application repo, not in this plugin repo.
This repo follows the same packaging style as the other ecmc_plugin_*
modules and uses /ioc/tools/driver.makefile.
The build needs access to the STruCpp runtime headers:
make STRUCPP=/path/to/strucppIf STRUCPP is not set, the makefile defaults to ../strucpp.
The bundled motion library is distributed with the repo, so normal users do not
need Node or strucpp just to consume MC_* blocks from an application repo.
Load the plugin through require, like the other ecmc_plugin_* modules.
That makes $(ecmc_plugin_strucpp_DIR) available and auto-executes
startup.cmd with the macro string from the require line.
Example:
require ecmc_plugin_strucpp sandst_a "REPORT=1"
That is the standard IOC-layout case:
bin/main.sobin/main.so.mapbin/main.so.substitutions
When your project uses a different logic-library path or explicit contiguous images, add the corresponding macros explicitly, for example:
require ecmc_plugin_strucpp sandst_a "PLUGIN_ID=0,LOGIC_LIB=/absolute/path/to/machine_logic.so,INPUT_ITEM=ec0.s2.mm.inputDataArray01,OUTPUT_ITEM=ec0.s2.mm.outputDataArray01,MEMORY_BYTES=64,REPORT=1"
The startup helper accepts:
PLUGIN_IDLOGIC_LIBASYN_PORTMAPPING_FILEINPUT_ITEMOUTPUT_ITEMINPUT_BINDINGSOUTPUT_BINDINGSMEMORY_BYTESSAMPLE_RATE_MSVALIDATE_REPORTRUN_BEFORE_EPICS_STARTEDLOAD_DEFAULT_PVSEPICS_SUBSTDB_PREFIXDB_MACROSREPORT
SAMPLE_RATE_MS is forwarded by startup.cmd as
sample_rate_ms=<n> in the plugin config string.
VALIDATE_REPORT is forwarded as validate_report=<0|1>.
RUN_BEFORE_EPICS_STARTED is forwarded as
run_before_epics_started=<0|1>.
Examples:
require ecmc_plugin_strucpp sandst_a "REPORT=1,VALIDATE_REPORT=1"
require ecmc_plugin_strucpp sandst_a "RUN_BEFORE_EPICS_STARTED=1"
There is also a concrete IOC example in
examples/iocsh_examples/loadPluginExample.cmd.
If EPICS_SUBST is provided, startup.cmd also calls dbLoadTemplate(...)
automatically after the plugin is loaded. The standard macro set passed by the
helper is:
P=$(DB_PREFIX)PORT=$(ASYN_PORT)
and DB_MACROS is appended unchanged if you need extra template macros.
If DB_PREFIX is omitted, it defaults to $(IOC).
If EPICS_SUBST is not provided, but DB_PREFIX or DB_MACROS is set,
startup.cmd defaults to:
${LOGIC_LIB}.substitutions
That is the preferred convention. Application repos should generate the
substitutions file next to the logic library so the normal startup path only
needs database macros, or only LOGIC_LIB when using a non-default library
path.
EPICS_SUBST remains the explicit override when you want to load a different
record file.
For a minimal EL7041 velocity-only sample that binds %IW0 to
positionActual01, %QW0 to driveControl01, and %QW2 to
velocitySetpoint01 through a startup-linked mapping file, see
examples/iocsh_examples/loadEL7041VelocityExample.cmd.
For a motion-data sample that binds %IL0 to ax1.enc.actpos and %QL0 to
ax1.traj.targetpos, see
examples/iocsh_examples/loadMotionActposMirrorExample.cmd.
For a real ST motion-library sample using MC_Power, MC_MoveAbsolute, and
MC_ReadActualPosition through one contiguous input/output image, see
examples/iocsh_examples/loadMcPowerMoveAbsoluteLibExample.cmd.
For a velocity-oriented ST motion-library sample using MC_Power,
MC_MoveVelocity, MC_ReadStatus, and MC_ReadActualVelocity, see
examples/iocsh_examples/loadMcPowerMoveVelocityLibExample.cmd.
For a relative-move ST motion-library sample using MC_Power,
MC_MoveRelative, MC_ReadStatus, and MC_ReadActualPosition, see
examples/iocsh_examples/loadMcPowerMoveRelativeLibExample.cmd.
For a new app repo:
- compile your ST with
-L /path/to/ecmc_plugin_strucpp/libs - include
src/ecmcStrucppLogicWrapper.hppin the tiny logic wrapper - link the generated logic module against the
STruCppruntime headers andecmcMcApi.h