Here you can find some best practice configurations for common use cases.
The corresponding startup files are located in examples/PSI/best_practice/.
Public PLC best-practice examples include:
examples/PSI/best_practice/plcs/basic/examples/PSI/best_practice/plcs/masterless/examples/PSI/best_practice/plcs/motion/stepper_bissc_forw_back_seq/Use of macros makes the code more generic. When loading a PLC file with “loadPLCFile.cmd”, custom macros can be defined in “PLC_MACROS”:
${SCRIPTEXEC} ${ecmccfg_DIR}loadPLCFile.cmd, "FILE=./cfg/main.plc, INC=.:./cfg/, DESC='Test', SAMPLE_RATE_MS=1000, PLC_MACROS='BO_S_ID=${ECMC_EC_SLAVE_NUM}'"
NOTE: ECMC_EC_SLAVE_NUM expands to the ID of the last added slave.
In addition to the custom macros, a few macros, that are often needed, are predefined:
A common use case is that some initiation is needed, could be triggering of a custom homing sequence:
if(${SELF}.firstscan) {
var plc:=${SELF_ID};
${DBG=#}println('PLC ',plc,' is starting up');
};
After macro expansion the code would look like this (for PLC id=0,DBG=''):
if(plc0.firstscan) {
var plc:=0;
println('PLC ',plc,' is starting up');
};
All EtherCAT related information/data is accessible through the pattern “ec<master_id>.s<slave_id>.
Toggle an output:
${M}.s${BO_S_ID}.binaryOutput${BO_CH=01}:=not(${M}.s${BO_S_ID}.binaryOutput${BO_CH=01});
${DBG=#}println('State: ', ${M}.s${BO_S_ID}.binaryOutput${BO_CH});
After macro expansion with the following macros the code would look like this:
ec0.s10.binaryOutput01:=not(ec0.s10.binaryOutput01);
#println('State: ', ec0.s10.binaryOutput01);
Since all PLC files and PLC libs are parsed through MSI the “include” and “substitute” commands can be used.
When using the include command, the file location dir of the file must be added in the INC parameter when loading the PLC:
${SCRIPTEXEC} ${ecmccfg_DIR}loadPLCFile.cmd, "FILE=./cfg/main.plc, INC=.:./cfg/, DESC='Test', SAMPLE_RATE_MS=1000, PLC_MACROS='BO_S_ID=${ECMC_EC_SLAVE_NUM}'"
The “INC” parameter can contain several directories separated with a “:”, making it possible to include PLC files from several locations/modules.
As a demo use case let’s consider that a few outputs needs to be toggled. NOTE: There are simpler ways to write this specific code but it’s used to demo how code can be divided.
Lets first define some code that toggles a bit (toggle_output.plc_inc):
# Example of simple include file that toggles an binary output
${M}.s${BO_S_ID}.binaryOutput${BO_CH}:=not(${M}.s${BO_S_ID}.binaryOutput${BO_CH});
${DBG=#}println('State: ', ${M}.s${BO_S_ID}.binaryOutput${BO_CH});
This code snippet then can be included in a main plc-file by using the “include” keyword. Each include can then be included with different macros by using the “substitute” keyword:
substitute "BO_CH=01"
include "toggle_output.plc_inc"
substitute "BO_CH=02, DBG="
include "toggle_output.plc_inc"
The above code would expand to:
ec0.s10.binaryOutput01:=not(ec0.s10.binaryOutput01);
#println('State:', ec0.s10.binaryOutput01);
ec0.s10.binaryOutput02:=not(ec0.s10.binaryOutput02);
println('State: ', ec0.s10.binaryOutput02);
The resulting code will toggle two different outputs, the state of the last output will be printed.
NOTE: Macros cannot be used in the filename when including a file. Instead the dir should be defined in the INC param when loading the PLC, see above.
There are two good ways to handle printouts:
plc<id>.dbg or ${SELF}.dbg flag: Accessible bit from generic plc panel. Printouts can be switched on/off in runtime.plc<id>.dbg or ${SELF}.dbgThe variable plc<id>.dbg or ${SELF}.dbg can be used to turn on and of debug printouts for an PLC:
if(${SELF}.dbg) {
println('Time: ',ec_get_time());
println('Time MONO: ',ec_get_time_frm_src(1));
println('Time REAL: ',ec_get_time_frm_src(0));
};
This allows turning on/off printouts in runtime by writing to the <prefix>PLC<id>-DbgCmd PV which is accessible in the generic plc panel (can be started from ecmcMain.ui).
Only use the plc<id>.dbg variable for dbg purpose. It should always be safe to write to this variable.
Adding a DBG macro can be useful to be able to turn on/off printouts. Typically during commissioning it can be useful to have many printouts, but later when system goes into production, it could be a good idea to turn (some) printouts off.
Example of a printout that can be turned on/off (default off)
${DBG=#}println('Value: ', ${M}.s${BO_S_ID}.binaryOutput${BO_CH});
Will result in the below if setting the DBG='' (and some other macros, see above):
println('Value: ', ec0.s10.binaryOutput01);
Always add a description when creating a PLC by setting the DESC macro when calling loadPLCFile.cmd.
Example:
${SCRIPTEXEC} ${ecmccfg_DIR}loadPLCFile.cmd, "FILE=./cfg/main.plc, INC=.:./cfg/, DESC='Toggle some bits', SAMPLE_RATE_MS=1000, PLC_MACROS='BO_S_ID=${ECMC_EC_SLAVE_NUM}'"
The description can maximum be 40 chars long.
In ecmccfg/plc_lib some code snippets are accessible. These are installed in ecmccfg module and can be accesses in ${ecmccfg_DIR}.
Sofar, the following code is accessible:
By declaring variables the ecmc plc code will be simpler to read. A declaration block needs to be added starting with “VAR” and ending with “END_VAR”:
VAR
<declarations>
END_VAR
<plc code>
The declaration needs to comply with the following syntax:
VAR
<var_name> : <address>;
END_VAR
The following “addresses” are supported:
global.<name>static.<name>ec<mid>ec<mid>.s<sid>ec<mid>.s<sid>.<name>ax<id>ax<id>.trajax<id>.encax<id>.drvax<id>.monax<id>.traj.<name>ax<id>.enc.<name>ax<id>.drv.<name>ax<id>.mon.<name>ds<id>ds<id>.<name>plc<id><name>The variables will then be replaced/substituted with the addresses during load time.
Example of plc file with declaration section and code section:
VAR
// Globals
gTest : global.test;
// Statics
sTest : static.test;
// EtherCAT I/0
actPos : ${M}.s${DRV_SID}.positionActual01;
mySlave : ${M}.s${DRV_SID};
coolingValveBO : ${M}.s${BO_SID=2}.binaryOutput02;
// Axis data
targetPos : ax${AX_ID=1}.traj.targetpos;
myAxis : ax1;
myTraj : ax${AX_ID=1}.traj;
// Data storage
buffer : ds${DS_ID=0};
// Constants
pi : 3.1415;
// PLC
myPLC : ${SELF}
END_VAR
coolingValveBO:=not(coolingValveBO);
println('mySlave.controlWord: ', mySlave.driveControl${CH=01});
if(myTraj.targetpos<>static.oldTarget) {
println('new target: ',myTraj.targetpos );
};
static.oldTarget := myTraj.targetpos;
if(gTest+ 1 > 10+sTest+mySlave.positionActual01) {
println('actPos:', actPos);
};
if(gTest+ 1> 10+mySlave.positionActual01) {
println('actPos:', actPos);
};
static.pini:=1;
println('actPos:', actPos, ' myAxis enc: ', myAxis.enc.actpos+pi);
gTest += 1;
println('buffer index: ', buffer.index);
if(myTraj.setpos>0) {
myTraj.setpos+=1;
}
As an example, the first row of the code section:
coolingValveBO:=not(coolingValveBO);
will be converted to:
${M}.s${BO_SID=2}.binaryOutput02:=not(${M}.s${BO_SID=2}.binaryOutput02);
and:
if(gTest+ 1 > 10+sTest+mySlave.positionActual01) {
will be converted to:
if(global.test+ 1 > 10+static.test+${M}.s${DRV_SID}.positionActual01) {
PLC variables are exposed on the ecmc asyn port and can then be wrapped by normal EPICS records.
The asyn naming convention is:
plcs.plc<id>.static.<name>plcs.global.<name>Examples:
plcs.plc0.static.seqStepplcs.plc0.static.doMotionplcs.global.modeBare PLC_VAR names default to static variables in the last loaded PLC, using
ECMC_PLC_ID. Use SCOPE=global if the variable should instead resolve to
plcs.global.<name>.
For numeric values:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarAnalog.cmd \
"NAME=M1-State,PLC_VAR=seqStep,EGU=step,PREC=0"
For boolean values:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarBinary.cmd \
"NAME=M1-DoMtn,PLC_VAR=doMotion,ONAM=Run,ZNAM=Stop"
For a global variable:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarBinary.cmd \
"NAME=Mode,PLC_VAR=mode,SCOPE=global,ONAM=Remote,ZNAM=Local"
These scripts load the new templates:
ecmcPlcVarAnalog.db creates $(DEV):$(NAME) as an aoecmcPlcVarBinary.db creates $(DEV):$(NAME) as a boSo the examples above create PVs such as:
$(IOC):M1-State$(IOC):M1-DoMtn$(IOC):ModeDEV defaults to IOC, but a different prefix can be provided:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarAnalog.cmd \
"DEV=IOC_TEST,NAME=Counter,PLC_VAR=counter,EGU=counts,PREC=0"
PLC_ID can also be provided explicitly when the wrapper is not called
directly after loadPLCFile.cmd, or when the variable should be taken from
another PLC than the last one loaded:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarAnalog.cmd \
"NAME=Counter,PLC_VAR=counter,PLC_ID=3,EGU=counts,PREC=0"
Extra record macros can be passed directly through the script call, either as
named script parameters like EGU, PREC, ESLO, EOFF, DESC, HOPR,
LOPR, DRVH, and DRVL, or through DB_MACROS for less common fields.
Example with extra template macros:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarAnalog.cmd \
"NAME=Counter,PLC_VAR=counter,EGU=counts,PREC=0,DB_MACROS='HHSV=MAJOR,HSV=MINOR'"
PLC_VAR supports these forms:
counter: resolved as plcs.plc${ECMC_PLC_ID}.static.counterstatic.counter: resolved as plcs.plc${ECMC_PLC_ID}.static.counterglobal.mode: resolved as plcs.global.modeplcs.plc3.static.counter: used as-isFor bare names that should be global, set SCOPE=global:
${SCRIPTEXEC} ${ecmccfg_DIR}addPlcVarBinary.cmd \
"NAME=Mode,PLC_VAR=mode,SCOPE=global,ONAM=Remote,ZNAM=Local"
PLC_ID can be provided explicitly, but immediately after loadPLCFile.cmd it
normally does not need to be, because it defaults to the last loaded PLC id
from ECMC_PLC_ID. If the wrapper call happens later, or should target another
PLC than the last one loaded, then set PLC_ID explicitly. For example,
PLC_VAR=counter,PLC_ID=3 resolves to plcs.plc3.static.counter.
The older Set...-RB naming is still available:
dbLoadRecords("ecmcPlcAnalog.db",
"P=$(IOC):,PORT=MC_CPU1,ASYN_NAME=plcs.plc${ECMC_PLC_ID}.static.seqStep,REC_NAME=-M1-State")
dbLoadRecords("ecmcPlcBinary.db",
"P=$(IOC):,PORT=MC_CPU1,ASYN_NAME=plcs.plc${ECMC_PLC_ID}.static.doMotion,REC_NAME=-M1-DoMtn")
These records are also useful when EPICS should both write the PLC variable and read
back its current value, but the new wrapper scripts provide the cleaner DEV:NAME
PV naming.
static and when to use globalstatic for values owned by one PLC, for example a state machine step,
command bit, or internal setpoint.global for values that must be shared between several PLCs or between a
PLC and a common EPICS control PV.If a plain input or output record is needed instead of the standard readback template, link directly to the same asyn name.
Read a PLC variable into EPICS:
record(ai,"$(P)FromPLC"){
field(DTYP, "asynFloat64")
field(INP, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=0))T_SMP_MS=$(T_SMP_MS=1000)/TYPE=asynFloat64/plcs.plc0.static.toEpics?")
field(SCAN, "I/O Intr")
}
Write an EPICS value into a PLC variable:
record(ao,"$(P)ToPLC"){
field(DTYP, "asynFloat64")
field(OUT, "@asyn($(PORT),$(ADDR=0),$(TIMEOUT=0))T_SMP_MS=$(T_SMP_MS=1000)/TYPE=asynFloat64/plcs.plc0.static.fromEpics=")
field(SCAN, "Passive")
}
Notes:
? at the end of the asyn path reads a value.= at the end of the asyn path writes a value.PORT is normally MC_CPU1.addPlcVarBinary.cmd, ecmcPlcBinary.db, or asynInt32.