Trying to lock the controller from one of its axes may lead to segfaults, hence it is not advisable.
sinqmotor
Overview
This library offers base classes for EPICS motor drivers (sinqAxis
and sinqController
) of PSI SINQ. These classes are extensions of the classes asynMotorAxis
and asynMotorController
from the asynMotor
framework (https://github.com/epics-modules/motor/tree/master/motorApp/MotorSrc) and bundle some common functionality.
User guide
Architecture of EPICS motor drivers at SINQ
The asyn-framework offers two base classes asynMotorAxis
and asynMotorController
. At SINQ, we extend those classes by two children sinqAxis
and sinqController
which are not complete drivers on their own, but serve as a framework extension for writing drivers. The concrete drivers are then created as separated libraries, an example is the TurboPMAC-driver: https://git.psi.ch/sinq-epics-modules/turboPmac.
The full inheritance chain for two different motor drivers "a" and "b" looks like this:
asynController -> sinqController -> aController
asynAxis -> sinqAxis -> aAxis
asynController -> sinqController -> bController
asynAxis -> sinqAxis -> bAxis
Those inheritance chains are created at runtime by loading shared libraries. These libraries must be compatible to each other (see next section).
Versioning
In order to make sure the shared libraries are compatible to each other, we use the "require" framework extension for EPICS (https://github.com/paulscherrerinstitute/require). If a shared library has another library as a dependency, it is checked whether the latter is already loaded. If yes, the loaded version is considered compatible if:
- no specific version was required by the former library
- the already loaded version matches the required version exactly
- major and minor numbers are the same and already loaded patch number is equal to the required one or higher
- major numbers are the same and already loaded minor number is higher than the required one
- the already loaded version is a test version and the required version is not a test version These rules are in complicance with the SemVer standard (https://semver.org/lang/de/)
If the dependency hasn't been loaded yet, it is loaded now. In case no specific version is required, the latest numbered version is used.
Because these rules are checked sequentially for each required dependency and no unloading is performed, it is important to consider the order of required libraries. Consider the following example:
require "libDriverA" # sinqMotor 1.2 is specified as a dependency
require "libDriverB" # sinqMotor 1.0 is specified as a dependency
require
first checks the dependencies of libDriverA
and sees that sinqMotor 1.2
is required. It therefore load sinqMotor 1.2
and then libDriverA
. Now the next require
starts analyzing the dependencies of libDriverB
and sees that sinqMotor 1.0
is required. Since sinqMotor 1.2
is already loaded, rule 4) is applied and libDriverB
is assumed to be compatible with sinqMotor 1.2
as well (which it should be according to SemVer).
When the order is inverted, the following happens:
require "libDriverB" # sinqMotor 1.0 is specified as a dependency
require "libDriverA" # sinqMotor 1.2 is specified as a dependency
require
first checks the dependencies of libDriverB
and sees that sinqMotor 1.0
is required. It therefore load sinqMotor 1.0
and then libDriverB
. Now the next require
starts analyzing the dependencies of libDriverA
and sees that sinqMotor 1.2
is required. Since sinqMotor 1.0
is already loaded, require
cannot load sinqMotor 1.2
. Therefore the IOC startup is aborted with an error message.
In order to make the setup script more robust, it is therefore recommended to explicitly add a dependency version which is compatible to all required libraries:
require "sinqMotor", "1.2"
require "libDriverB" # sinqMotor 1.0 is specified as a dependency
require "libDriverA" # sinqMotor 1.2 is specified as a dependency
The IOC startup now succeeds because we made sure the higher version is loaded first.
Please see the README.md of https://github.com/paulscherrerinstitute/require for more details.
To find out which version of sinqMotor is needed by a driver, refer to its Makefile (line sinqMotor_VERSION=x.x.x
, where x.x.x is the minimum required version).
IOC startup script
An EPICS IOC for motor control at SINQ is started by executing a script with the IOC shell. In its simplest form, an IOC for two controllers is a file looking like this:
#!/usr/local/bin/iocsh
# Load libraries needed for the IOC
require sinqMotor, 1.0.0
require actualDriver, 1.2.0
# Define environment variables used later to parametrize the individual controllers
epicsEnvSet("TOP","/ioc/sinq-ioc/sinqtest-ioc/")
epicsEnvSet("INSTR","SQ:SINQTEST:")
# Include other scripts for the controllers 1 and 2
< actualDriver.cmd
< actualDriver.cmd
iocInit()
The first line is a so-called shebang which instructs Linux to execute the file with the executable located at the given path - the IOC shell in this case. The controller script "mcu1.cmd" looks like this: The script for controller 1 ("turboPmac1.cmd") for a Turbo PMAC (see https://git.psi.ch/sinq-epics-modules/turboPmac) has the following structure. The scripts for other controller types can be found in the README.md of their respective repositories.
# Define the name of the controller and the corresponding port
epicsEnvSet("DRIVER_PORT","actualDriver1")
epicsEnvSet("IP_PORT","p$(DRIVER_PORT)")
# Create the TCP/IP socket used to talk with the controller. The socket can be adressed from within the IOC shell via the port name
drvAsynIPPortConfigure("$(IP_PORT)","172.28.101.24:1025")
# Create the controller object with the defined name and connect it to the socket via the port name.
# The other parameters are as follows:
# 8: Maximum number of axes
# 0.05: Busy poll period in seconds
# 1: Idle poll period in seconds
# 1: Socket communication timeout in seconds
actualDriverController("$(DRIVER_PORT)", "$(IP_PORT)", 8, 0.05, 1, 1);
# Define some axes for the specified motor controller at the given slot (1, 2 and 5). No slot may be used twice!
actualDriverAxis("$(DRIVER_PORT)",1);
actualDriverAxis("$(DRIVER_PORT)",2);
actualDriverAxis("$(DRIVER_PORT)",5);
# Set the number of subsequent timeouts
setMaxSubsequentTimeouts("$(DRIVER_PORT)", 20);
# Set the number of forced fast polls performed after the poller is "woken up".
# When the poller is "woken up", it performs the specified number of polls with
# the previously stated busy poll period.
setForcedFastPolls("$(DRIVER_PORT)", 10);
# Configure the timeout frequency watchdog: A maximum of 10 timeouts are allowed in 300 seconds before an alarm message is sent.
setThresholdComTimeout("$(DRIVER_PORT)", 300, 10);
# Parametrize the EPICS record database with the substitution file named after the motor controller.
epicsEnvSet("SINQDBPATH","$(sinqMotor_DB)/sinqMotor.db")
dbLoadTemplate("$(TOP)/$(DRIVER_PORT).substitutions", "INSTR=$(INSTR)$(DRIVER_PORT):,CONTROLLER=$(DRIVER_PORT)")
epicsEnvSet("SINQDBPATH","$(actualDriver_DB)/turboPmac.db")
dbLoadTemplate("$(TOP)/$(DRIVER_PORT).substitutions", "INSTR=$(INSTR)$(DRIVER_PORT):,CONTROLLER=$(DRIVER_PORT)")
dbLoadRecords("$(sinqMotor_DB)/asynRecord.db","P=$(INSTR)$(DRIVER_PORT),PORT=$(IP_PORT)")
Substitution file
The substitution file is a table containing axis-specific information which is used to create the axis-specific PVs. To work with sinqMotor, "mcu1.substitutions" needs to look like this (the order of columns does not matter):
file "$(SINQDBPATH)"
{
pattern
{ AXIS, M, DESC, EGU, DIR, MRES, MSGTEXTSIZE, ENABLEMOVWATCHDOG, LIMITSOFFSET, CANSETSPEED, ADAPTPOLL }
{ 1, "lin1", "Linear motor doing whatever", mm, Pos, 0.001, 200, 1, 1.0, 1, 1 }
{ 2, "rot1", "First rotary motor", degree, Neg, 0.001, 200, 0, 1.0, 0, 1 }
{ 3, "rot2", "Second rotary motor", degree, Pos, 0.001, 200, 0, 0.0, 1, 0 }
{ 5, "rot3", "Surprise: Third rotary motor", degree, Pos, 0.001, 200, 1, 2.0, 0, 0 }
}
The variable SINQDBPATH
has been set in "mcu1.cmd" before calling dbLoadTemplate
.
Mandatory parameters
AXIS
: Index of the axis, corresponds to the physical connection of the axis to the MCU.M
: The full PV name is created by concatenating the variables INSTR, DRIVER_PORT and M. For example, the PV of the first axis would be "SQ:SINQTEST:mcu1:lin1".EGU
: Engineering units. For a linear motor, this is mm, for a rotaty motor, this is degree.DIR
: If set to "Neg", the axis direction is inverted.MRES
: This is a scaling factor determining the resolution of the position readback value. For example, 0.001 means a precision of 1 um. A detailed description can be found in section Motor record resolution MRES.
Optional parameters
The default values for those parameters are given for the individual records in db/sinqMotor.db
DESC
: Description of the motor. This field is just for documentation and is not needed for operating a motor. Defaults to the motor name.MSGTEXTSIZE
: Buffer size for the motor message record in characters. Defaults to 200 charactersENABLEMOVWATCHDOG
: SetssetWatchdogEnabled
during IOC startup to the given value. Defaults to 0.LIMITSOFFSET
: If the motor limits are read out from the controller, they can be further reduced by this offset in order to avoid errors due to slight overshoot on the motor controller. For example, if this value is 1.0 and the read-out limits are [-10.0 10.0], the EPICS limits are set to [-9.0 9.0]. This parameter uses engineering units (EGU). Defaults to 0.0.CANSETSPEED
: If set to 1, the motor speed can be modified by the user. Defaults to 0.ADAPTPOLL
: If set to any value other than 0, adaptive polling is enabled for this particular axis. Adaptive polling is designed to reduce the communication load in case some axis is moving. By default, if at least one axis is moving, all axes are polled using the busy / moving poll period (see IOC startup script). Adaptive polling modifies this behaviour so that the affected axis is only polled with the busy / moving poll period if it itself is moving. This setting is ignored for "forced fast polls" (when the poller is woken up, e.g. after an axis received a move command). Defaults to 1.
Motor record resolution MRES
The motor record resolution (index motorRecResolution_ in the parameter library, MRES in the motor record) is NOT a conversion factor between user units (e.g. mm) and motor units (e.g. encoder steps), but a scaling factor defining the resolution of the position readback field RRBV. This is due to an implementation detail of EPICS described here: https://epics.anl.gov/tech-talk/2018/msg00089.php https://github.com/epics-modules/motor/issues/8
Basically, the position value in the parameter library is a double which is then truncated to an integer in devMotorAsyn.c (because it was originally meant for converting from engineering units to encoder steps, which are by definition integer values). Therefore, if we want a precision of 1 millimeter, we need to set MRES to 1. If we want one of 1 micrometer, we need to set MRES to 0.001. The readback value needs to be multiplied with MRES to get the actual value.
In the driver, we use user units. Therefore, when we interact with the parameter library, we need to account for MRES. This means:
- When writing position or speed to the parameter library, we divide the value by the motor record resolution.
- When reading position or speed from the parameter library, we multiply the value with the motor record resolution.
Index and motor record field are coupled as follows: The parameter motorRecResolution_ is coupled to the field MRES of the motor record in the following manner:
- In sinqMotor.db, the PV (motor_record_pv_name) MOTOR_REC_RESOLUTION is defined as a copy of the field (motor_record_pv_name).MRES .
- The PV name MOTOR_REC_RESOLUTION is coupled in asynMotorController.h to the constant motorRecResolutionString
- ... which in turn is assigned to motorRecResolution_ in asynMotorController.cpp This way of making the field visible to the driver is described here: https://epics.anl.gov/tech-talk/2020/msg00378.php This is a one-way coupling, changes to the parameter library via setDoubleParam are NOT transferred to (motor_record_pv_name).MRES or to (motor_record_pv_name):Resolution.
Additional records
sinqMotor
provides a variety of additional records. See db/sinqMotor.db
for the complete list and the documentation.
Developer guide
Base classes
sinqMotor offers a variety of additional methods for children classes to standardize certain patterns (e.g. writing messages to the IOC shell and the motor message PV). For a detailed description, please see the respective function documentation in the .h-files. All of these functions can be overwritten manually if e.g. a completely different implementation of poll
is required. Some functions are marked as virtual, because they are called from other functions of sinqMotor and therefore need runtime polymorphism. Functions without that marker are not called anywhere in sinqMotor.
sinqController.h
couldNotParseResponse
: Write a standardized message if parsing a device response failed.paramLibAccessFailed
: Write a standardized message if accessing the parameter library failed.stringifyAsynStatus
: Convert the enumasynStatus
into a human-readable string.checkComTimeoutWatchdog
: Calculates the timeout frequency (number of timeouts in a given time) and informs the user if a specified limit has been exceeded.setThresholdComTimeout
: Set the maximum number of timeouts and the time window size for the timeout frequency limit. This function is also available in the IOC shell.checkMaxSubsequentTimeouts
: Check if the number of subsequent timeouts exceeds a specified limit.setMaxSubsequentTimeouts
: Set the limit for the number of subsequent timeouts before the user is informed. This function is also available in the IOC shell.setForcedFastPolls
: Set the number of forced fast polls which are performed after the poller has been "woken up" ( = afterwakePoller()
is called). This function is also available in the IOC shell.
sinqAxis.h
-
enable
: This function is called if the$(INSTR)$(M):Enable
PV from db/sinqMotor.db is set. This is an empty function which should be overwritten by concrete driver implementations. -
reset
: This function is called when the$(INSTR)$(M):Reset
PV from db/sinqMotor.db is set. It callsdoReset
and performs some fast polls afterdoReset
returns. -
doReset
: This is an empty function which should be overwritten by concrete driver implementations. -
move
: This function sets the absolute target position in the parameter library and then callsdoMove
. -
doMove
: This is an empty function which should be overwritten by concrete driver implementations. -
home
: This function sets the internal status flags for the homing process and then calls doHome. -
doHome
: This is an empty function which should be overwritten by concrete driver implementations. -
poll
: This is a wrapper arounddoPoll
which performs some bookkeeping tasks before and after callingdoPoll
:Before calling
doPoll
:- Check if the paramLib already contains an old error message. If so, put it into a temporary bufffer
After calling
doPoll
:- Call
checkMovTimeoutWatchdog
. If the movement timed out, create an error message for the user - Update the readback-value for the axis enablement.
- If
doPoll
returns anything other thanasynSuccess
or if an old error message is waiting in the temporary buffer, setmotorStatusProblem
to true, otherwise to false. If an old error message is waiting in the temporary buffer, butdoPoll
returnedasynSuccess
, overwrite the paramLib entry formotorMessageText
with the old error message. - Run
callParamCallbacks
- Reset
motorMessageText
AFTER updating the PVs. This makes sure that the error message is shown for at least one poll cycle. - Return the status of
doPoll
-
motorPosition
: Returns the parameter library value of the motor position, accounted for the motor record resolution (see section "Motor record resolution MRES") -
setMotorPosition
: Writes the given value into the parameter library, accounted for the motor record resolution (see section "Motor record resolution MRES") -
setVeloFields
: Populates the motor record fields VELO (actual velocity), VBAS (minimum allowed velocity) and VMAX (maximum allowed velocity) from the driver. -
setAcclField
: Populates the motor record field ACCL from the driver. -
startMovTimeoutWatchdog
: Starts a watchdog for the movement time. This watchdog compares the actual time spent in a movement operation with an expected time, which is calculated based on the distance of the current and the target position. -
checkMovTimeoutWatchdog
: Check if the watchdog timed out. -
setWatchdogEnabled
: Enables / disables the watchdog. This function is also available in the IOC shell. -
setOffsetMovTimeout
: Set a linear offset for the expected movement time. This function is also available in the IOC shell. -
setScaleMovTimeout
: Set a scaling factor for the expected movement time. This function is also available in the IOC shell.
msgPrintControl.h
In addition to the two extension classes this library also includes a mechanism which prevents excessive repetitions of the same error message to the IOC shell via the classes msgPrintControl
and msgPrintControlKey
. A detailed description of the mechanism can be found in the docstring of msgPrintControl
. The implementation of the poll
function of sinqAxis
also contains an example how to use it. Using this feature in derived drivers is entirely optional.
Versioning
The versioning is done via git tags. Git tags are recognized by the PSI build system: If you tag a version as 1.0, it will be built into the directory /ioc/modules/sinqMotor/1.0. The tag is directly coupled to a commit so that it is always clear which source code was used to build which binary.
All existing tags can be listed with git tag
in the sinqMotor directory. Detailed information (author, data, commit number, commit message) regarding a specific tag can be shown with git show x.x.x
, where x.x.x
is the name of your version. To create a new tag, use git tag x.x.x
. If the tag x.x.x
is already used by another commit, git will show a corresponding error.
Dependencies
This library is based on the PSI version of the EPICS motor record, which can be found here: https://git.psi.ch/epics_driver_modules/motorBase
. We use a branch with a bugfix which is currently not merged into master due to resistance of the PSI userbase: https://git.psi.ch/epics_driver_modules/motorBase/-/tree/pick_fix-lockup-VAL-HOMF-VAL
. This library can be build with the following steps, assuming GCC and make are available:
git clone https://git.psi.ch/epics_driver_modules/motorBase/-/tree/pick_fix-lockup-VAL-HOMF-VAL
cd motorBase
git tag 7.2.2
. The latest version on master is currently 7.2.1, hence we increment the bugfix version counter by onemake install
Usage as dynamic dependency
The makefile in the top directory includes all necessary steps for compiling a shared library of sinqMotor together with the header files into /ioc/modules
(using the PSI EPICS build system). Therefore it is sufficient to clone this repository to a suitable location (git clone https://git.psi.ch/sinq-epics-modules/sinqmotor/-/tree/main
). Afterwards, switch to the directory (cd sinqmotor
) and run make install
.
To use the library when writing a concrete motor driver, include it in the makefile of your application / library the same way as other libraries such as e.g. asynMotor
by adding REQUIRED+=sinqMotor
to your Makefile. The version can be specified with sinqMotor_VERSION=x.x.x.
Usage as static dependency
This repository is included as a git submodule in some of the driver repositories depending upon sinqMotor. When installing via a Makefile (make install
) using the PSI build system, the following git command is executed within /ioc/tools/driver.makefile
:
git submodule update --init --recursive
This forces each submodule to be checked out at the latest commit hash stored in the remote repository. However, this is usually unwanted behaviour, since the higher-level drivers are usually designed to be compiled against a specific version of sinqMotor. In order to set the submodule to a specific version, the following steps need to be done BEFORE calling make install
:
cd sinqMotor
git checkout 0.1
cd ..
Then, the fixation of the version to 0.1 needs to be committed in the parent repository:
git commit -m "Update sinqMotor to 0.1"
After this commit, running make install
will use the correct driver version for compilation.