Added records to support detection of the current operation mode (position or velocity), whether one is allowed to change between the two and a record to actually change between the two. Also added a doMoveVelocity method which should be implemented by derived drivers if they support velocity mode.
438 lines
22 KiB
Markdown
438 lines
22 KiB
Markdown
# 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`
|
|
|
|
`asynMotorAxis` and `asynMotorController` are provided by the shared `asynMotor`
|
|
library. The drivers "a" and "b" should then include the "sinqMotor" repository
|
|
as submodules and directly compile against the source code of `sinqMotor`.
|
|
|
|
### Versioning
|
|
|
|
Versioning of `sinqMotor` and of derived drivers follows the SemVer standard
|
|
(https://semver.org/lang/de/). This standard is also used by the "require"
|
|
extension for EPICS (https://github.com/paulscherrerinstitute/require) used at
|
|
SINQ.
|
|
|
|
### 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 run by
|
|
"exampleDriver" is a file looking like this:
|
|
```
|
|
#!/usr/local/bin/iocsh
|
|
|
|
# Load libraries needed for the IOC
|
|
require exampleDriver, 1.0.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
|
|
< exampleDriver1.cmd
|
|
< exampleDriver2.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
|
|
files `exampleDriver1.cmd` or `exampleDriver2` then look like this:
|
|
|
|
```
|
|
# Define the name of the controller and the corresponding port
|
|
epicsEnvSet("DRIVER_PORT","exampleDriver1")
|
|
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","$(exampleDriver_DB)/sinqMotor.db")
|
|
dbLoadTemplate("$(TOP)/$(DRIVER_PORT).substitutions", "INSTR=$(INSTR)$(DRIVER_PORT):,CONTROLLER=$(DRIVER_PORT)")
|
|
epicsEnvSet("SINQDBPATH","$(exampleDriver_DB)/turboPmac.db")
|
|
dbLoadTemplate("$(TOP)/$(DRIVER_PORT).substitutions", "INSTR=$(INSTR)$(DRIVER_PORT):,CONTROLLER=$(DRIVER_PORT)")
|
|
dbLoadRecords("$(exampleDriver_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,
|
|
`exampleDriver1.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, CANSETMODE }
|
|
{ 1, "lin1", "Linear motor doing whatever", mm, Pos, 0.001, 200, 1, 1.0, 1, 1, 0 }
|
|
{ 2, "rot1", "First rotary motor", degree, Neg, 0.001, 200, 0, 1.0, 0, 1, 0 }
|
|
{ 3, "rot2", "Second rotary motor", degree, Pos, 0.001, 200, 0, 0.0, 1, 0, 0 }
|
|
{ 5, "rot3", "Surprise: Third rotary motor", degree, Pos, 0.001, 200, 1, 2.0, 0, 0, 1 }
|
|
}
|
|
```
|
|
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](#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 characters
|
|
- `ENABLEMOVWATCHDOG`: Sets `setWatchdogEnabled` 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](#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.
|
|
- `CANSETMODE`: If set to any value other than 0, the operation mode of the
|
|
motor can be changed. See section [Velocity mode](#velocity-mode).
|
|
|
|
|
|
### 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.
|
|
|
|
### Velocity mode
|
|
|
|
The motor record was originally designed for motors which operate in _position_
|
|
mode: They are given a certain position, move to that position and then stop
|
|
on their own. Some motors however operate in a continuous _velocity_ mode, where
|
|
they move (usually rotate) with a fixed velocity until told to stop or change
|
|
the velocity. To support this operation mode, `sinqMotor` provides three
|
|
additional records:
|
|
|
|
- `$(INSTR)$(M):Mode`: This read-only record returns 0 if the motor is in
|
|
position mode and 1 if it is in velocity mode. Defaults to 0.
|
|
- `$(INSTR)$(M):CanSetMode`: If the value of this record is other than zero, the
|
|
motor mode can be changed during operation. Defaults to 0, but can be
|
|
overwritten in the substitution file with `CANSETMODE` or from within the driver
|
|
(e.g. by reading out a parameter from the hardware).
|
|
- `$(INSTR)$(M):SetMode`: This record can be used to switch between operation
|
|
modes if `$(INSTR)$(M):CanSetMode` is not zero. Currently accepted values are
|
|
`0` for position mode and `1` for velocity mode.
|
|
|
|
When in velocity mode, writing to the `VELO` field of the motor record directly
|
|
triggers the corresponding velocity change if the driver supports it. Writing
|
|
to the `VAL` field does nothing in velocity mode.
|
|
|
|
## Developer guide
|
|
|
|
### File structure
|
|
|
|
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 (except for
|
|
`forcedPoll`, which is called in `poll`).
|
|
|
|
Adding new virtual methods breaks the ABI and therefore warrants a new major
|
|
version number!
|
|
|
|
#### sinqController.h
|
|
- `couldNotParseResponse`: Write a standardized message if parsing a deviceresponse failed.
|
|
- `paramLibAccessFailed`: Write a standardized message if accessing the parameter library failed.
|
|
- `stringifyAsynStatus`: Convert the enum `asynStatus` 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" ( = after `wakePoller()` 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 calls `doReset` and performs some fast polls after `doReset` returns.
|
|
- `doReset`: This is an empty function which should be overwritten by concrete driver implementations.
|
|
- `moveVelocity`: This function checks if the motor is in velocity mode. If that is the case, it calls `doMoveVelocity`.
|
|
- `doMoveVelocity`: This is an empty function which should be overwritten by concrete driver implementations.
|
|
- `move`: This function checks if the motor is in position mode. If that is the
|
|
case, it then sets the absolute target position in the parameter library and then calls `doMove`.
|
|
- `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 around `forcedPoll` which does the following checks before calling `forcedPoll`:
|
|
- Are there any outstanding fast polls (method `outstandingForcedFastPolls` of
|
|
the controller returns a value greater zero)?
|
|
- Was the axis moving last time its status was polled?
|
|
- Is adaptive polling disabled?
|
|
- Did an idle period pass since the last poll?
|
|
If all of these conditions are false, no poll is performed. Otherwise, the
|
|
`forcedPoll` method is called. This method should not be called in the driver
|
|
code itself if a poll is needed - use `forcedPoll` instead!
|
|
|
|
- `forcedPoll`: This is a wrapper around `doPoll` which performs some
|
|
bookkeeping tasks before and after calling `doPoll`:
|
|
|
|
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 than `asynSuccess` or if an old error
|
|
message is waiting in the temporary buffer, set `motorStatusProblem` to true,
|
|
otherwise to false. If an old error message is waiting in the temporary
|
|
buffer, but `doPoll` returned `asynSuccess`, overwrite the paramLib entry for
|
|
`motorMessageText` 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 constant 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.
|
|
|
|
#### macros.h
|
|
Contains macros used in `sinqMotor` and derived drivers:
|
|
|
|
- **HIDDEN**
|
|
|
|
By default, the symbols of classes and functions are hidden to avoid symbol
|
|
clashes when loadingmultiple shared libraries which use `sinqMotor`. In order
|
|
to compile this library with exported symbols, specifiy `-DHIDDEN= ` as a
|
|
compiler flag (if using the given Makefile, this needs to be added to the
|
|
`USR_CFLAGS`).
|
|
|
|
Derived libraries can use the same mechanism via the macro `HIDDEN` (defined
|
|
in `macros.h`):
|
|
```
|
|
class HIDDEN turboPmacController : public sinqController
|
|
```
|
|
|
|
### 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 one
|
|
- `make 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 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 1.0`
|
|
- `cd ..`
|
|
|
|
Then, the fixation of the version to 1.0 needs to be committed in the parent
|
|
repository:
|
|
|
|
- `git commit -m "Update sinqMotor to 1.0"`
|
|
|
|
After this commit, running `make install` will use the correct driver version
|
|
for compilation.
|
|
|
|
If your driver uses another driver as a static dependency via git submodule
|
|
which in turn includes a sinqMotor submodule, it is not necessary to specify the
|
|
version of sinqMotor. Instead, specify the desired commit of the direct
|
|
dependency, commit this change and then update all submodules:
|
|
|
|
- `cd turboPmac`
|
|
- `git checkout 1.0`
|
|
- `cd ..`
|
|
- `git commit -m "Update turboPmac to 1.0"`
|
|
- `git submodule update --init --recursive`
|
|
|
|
This will update sinqMotor to the version specified in the 1.0 commit of turboPmac.
|