Added adaptive polling

See documentation in README.md for parameter ADAPTPOLL
This commit is contained in:
2025-04-16 13:05:48 +02:00
parent 4c3254687d
commit db03ffea0e
6 changed files with 126 additions and 46 deletions

View File

@ -29,15 +29,15 @@ An EPICS IOC for motor control at SINQ is started by executing a script with the
# Load libraries needed for the IOC # Load libraries needed for the IOC
require sinqMotor, 1.0.0 require sinqMotor, 1.0.0
require turboPmac, 1.2.0 require actualDriver, 1.2.0
# Define environment variables used later to parametrize the individual controllers # Define environment variables used later to parametrize the individual controllers
epicsEnvSet("TOP","/ioc/sinq-ioc/sinqtest-ioc/") epicsEnvSet("TOP","/ioc/sinq-ioc/sinqtest-ioc/")
epicsEnvSet("INSTR","SQ:SINQTEST:") epicsEnvSet("INSTR","SQ:SINQTEST:")
# Include other scripts for the controllers 1 and 2 # Include other scripts for the controllers 1 and 2
< turboPmac1.cmd < actualDriver.cmd
< turboPmac2.cmd < actualDriver.cmd
iocInit() iocInit()
``` ```
@ -46,7 +46,7 @@ The script for controller 1 ("turboPmac1.cmd") for a Turbo PMAC (see https://git
``` ```
# Define the name of the controller and the corresponding port # Define the name of the controller and the corresponding port
epicsEnvSet("NAME","turboPmac1") epicsEnvSet("NAME","actualDriver1")
epicsEnvSet("ASYN_PORT","p$(NAME)") epicsEnvSet("ASYN_PORT","p$(NAME)")
# 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 # 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
@ -58,12 +58,12 @@ drvAsynIPPortConfigure("$(ASYN_PORT)","172.28.101.24:1025")
# 0.05: Busy poll period in seconds # 0.05: Busy poll period in seconds
# 1: Idle poll period in seconds # 1: Idle poll period in seconds
# 1: Socket communication timeout in seconds # 1: Socket communication timeout in seconds
turboPmacController("$(NAME)", "$(ASYN_PORT)", 8, 0.05, 1, 1); actualDriverController("$(NAME)", "$(ASYN_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! # Define some axes for the specified motor controller at the given slot (1, 2 and 5). No slot may be used twice!
turboPmacAxis("$(NAME)",1); actualDriverAxis("$(NAME)",1);
turboPmacAxis("$(NAME)",2); actualDriverAxis("$(NAME)",2);
turboPmacAxis("$(NAME)",5); actualDriverAxis("$(NAME)",5);
# Set the number of subsequent timeouts # Set the number of subsequent timeouts
setMaxSubsequentTimeouts("$(NAME)", 20); setMaxSubsequentTimeouts("$(NAME)", 20);
@ -74,7 +74,7 @@ setThresholdComTimeout("$(NAME)", 300, 10);
# Parametrize the EPICS record database with the substitution file named after the motor controller. # Parametrize the EPICS record database with the substitution file named after the motor controller.
epicsEnvSet("SINQDBPATH","$(sinqMotor_DB)/sinqMotor.db") epicsEnvSet("SINQDBPATH","$(sinqMotor_DB)/sinqMotor.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)") dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
epicsEnvSet("SINQDBPATH","$(turboPmac_DB)/turboPmac.db") epicsEnvSet("SINQDBPATH","$(actualDriver_DB)/turboPmac.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)") dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
dbLoadRecords("$(sinqMotor_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)") dbLoadRecords("$(sinqMotor_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)")
``` ```
@ -87,11 +87,11 @@ To work with sinqMotor, "mcu1.substitutions" needs to look like this (the order
file "$(SINQDBPATH)" file "$(SINQDBPATH)"
{ {
pattern pattern
{ AXIS, M, DESC, EGU, DIR, MRES, MSGTEXTSIZE, ENABLEMOVWATCHDOG, LIMITSOFFSET, CANSETSPEED } { 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, "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 } { 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 } { 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 } { 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`. The variable `SINQDBPATH` has been set in "mcu1.cmd" before calling `dbLoadTemplate`.
@ -114,6 +114,7 @@ be further reduced by this offset in order to avoid errors due to slight oversho
on the motor controller. For example, if this value is 1.0 and the read-out limits 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. 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. - `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. Defaults to 1.
### Motor record resolution MRES ### Motor record resolution MRES

View File

@ -140,6 +140,18 @@ record(longout, "$(INSTR)$(M):CanSetSpeed") {
field(VAL, "$(CANSETSPEED=0)") field(VAL, "$(CANSETSPEED=0)")
} }
# If this PV has a value other than 0, adaptive polling for this axis is enabled.
# The standard motor record behaviour is to poll all axis with the busy / move poll
# period if at least one of the axes is moving. Adaptive polling changes this so
# that only axes which were moving in the last poll are polled with the busy / move poll
# period and all other axes are polled with the idle poll period.
record(longout, "$(INSTR)$(M):AdaptivePolling") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) ADAPTIVE_POLLING")
field(PINI, "YES")
field(VAL, "$(ADAPTPOLL=1)")
}
# The timeout mechanism for movements can be enabled / disabled by setting # The timeout mechanism for movements can be enabled / disabled by setting
# this PV to 1 / 0. # this PV to 1 / 0.
# This record is coupled to the parameter library via motorEnableMovWatchdog -> MOTOR_ENABLE_MOV_WATCHDOG. # This record is coupled to the parameter library via motorEnableMovWatchdog -> MOTOR_ENABLE_MOV_WATCHDOG.

View File

@ -15,6 +15,8 @@ sinqAxis::sinqAxis(class sinqController *pC, int axisNo)
offsetMovTimeout_ = 30; offsetMovTimeout_ = 30;
targetPosition_ = 0.0; targetPosition_ = 0.0;
epicsTimeGetCurrent(&lastPollTime_);
// This check is also done in asynMotorAxis, but there the IOC continues // This check is also done in asynMotorAxis, but there the IOC continues
// running even though the configuration is incorrect. When failing this // running even though the configuration is incorrect. When failing this
// check, the IOC is stopped, since this is definitely a configuration // check, the IOC is stopped, since this is definitely a configuration
@ -115,6 +117,46 @@ asynStatus sinqAxis::poll(bool *moving) {
asynStatus poll_status = asynSuccess; asynStatus poll_status = asynSuccess;
int homing = 0; int homing = 0;
int homed = 0; int homed = 0;
int adaptivePolling = 0;
/*
If adaptive polling is enabled:
- Check if the motor was moving during the last poll
- If yes, perform the poll
- If no, check if the last poll was at least an idlePollPeriod ago
- If yes, perform the poll
- If no, skip it
*/
pl_status =
pC_->getIntegerParam(axisNo_, pC_->adaptivePolling(), &adaptivePolling);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "adaptivePolling", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
};
// Using the EPICS timestamp here, see
// https://docs.epics-controls.org/projects/base/en/latest/epicsTime_h.html#_CPPv414epicsTimeStamp
// Get the current time
epicsTimeStamp ts;
epicsTimeGetCurrent(&ts);
if (adaptivePolling != 0) {
// Motor wasn't moving during the last poll
if (getWasMovingFlag() == 0) {
// Add the idle poll period
epicsTimeStamp earliestTimeNextPoll = lastPollTime_;
epicsTimeAddSeconds(&earliestTimeNextPoll, pC_->idlePollPeriod());
if (epicsTimeLessThan(&earliestTimeNextPoll, &ts) == 1) {
*moving = false;
return asynSuccess;
}
}
}
// Update the start time of the last poll
lastPollTime_ = ts;
/* /*
At the beginning of the poll, it is assumed that the axis has no status At the beginning of the poll, it is assumed that the axis has no status
@ -223,36 +265,42 @@ asynStatus sinqAxis::move(double position, int relative, double minVelocity,
double maxVelocity, double acceleration) { double maxVelocity, double acceleration) {
// Status of parameter library operations // Status of parameter library operations
asynStatus pl_status = asynSuccess; asynStatus status = asynSuccess;
double motorRecResolution = 0.0; double motorRecResolution = 0.0;
// ========================================================================= // =========================================================================
// When a new move is done, the motor is not homed anymore
pl_status = setIntegerParam(pC_->motorStatusHomed(), 0);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusHomed_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
pl_status = setIntegerParam(pC_->motorStatusAtHome(), 0);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusAtHome_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
pl_status = pC_->getDoubleParam(axisNo_, pC_->motorRecResolution(),
&motorRecResolution);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorRecResolution_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
// Store the target position internally // Store the target position internally
targetPosition_ = position * motorRecResolution; targetPosition_ = position * motorRecResolution;
return doMove(position, relative, minVelocity, maxVelocity, acceleration); status = doMove(position, relative, minVelocity, maxVelocity, acceleration);
if (status != asynSuccess) {
return status;
}
// Since the move command was successfull, we assume that the motor has
// started its movement.
status = setIntegerParam(pC_->motorStatusHomed(), 0);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorStatusHomed_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
status = setIntegerParam(pC_->motorStatusAtHome(), 0);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorStatusAtHome_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
status = pC_->getDoubleParam(axisNo_, pC_->motorRecResolution(),
&motorRecResolution);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorRecResolution_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
// Needed for adaptive polling
setWasMovingFlag(1);
return pC_->callParamCallbacks();
} }
asynStatus sinqAxis::doMove(double position, int relative, double minVelocity, asynStatus sinqAxis::doMove(double position, int relative, double minVelocity,

View File

@ -7,6 +7,7 @@ Stefan Mathis, November 2024
#ifndef sinqAxis_H #ifndef sinqAxis_H
#define sinqAxis_H #define sinqAxis_H
#include "asynMotorAxis.h" #include "asynMotorAxis.h"
#include <epicsTime.h>
class epicsShareClass sinqAxis : public asynMotorAxis { class epicsShareClass sinqAxis : public asynMotorAxis {
public: public:
@ -351,11 +352,17 @@ class epicsShareClass sinqAxis : public asynMotorAxis {
// Internal variables used in the movement timeout watchdog // Internal variables used in the movement timeout watchdog
time_t expectedArrivalTime_; time_t expectedArrivalTime_;
time_t offsetMovTimeout_; time_t offsetMovTimeout_;
double scaleMovTimeout_; double scaleMovTimeout_;
bool watchdogMovActive_; bool watchdogMovActive_;
// Store the motor target position for the movement time calculation // Store the motor target position for the movement time calculation
double targetPosition_; double targetPosition_;
/*
Store the time since the last poll
*/
epicsTimeStamp lastPollTime_;
private: private:
sinqController *pC_; sinqController *pC_;
}; };

View File

@ -53,7 +53,7 @@ sinqController::sinqController(const char *portName,
asynStatus status = asynSuccess; asynStatus status = asynSuccess;
// Handle to the asynUser of the IP port asyn driver // Handle to the asynUser of the IP port asyn driver
ipPortAsynOctetSyncIO_ = nullptr; pasynOctetSyncIOipPort_ = nullptr;
// Initial values for the average timeout mechanism, can be overwritten // Initial values for the average timeout mechanism, can be overwritten
// later by a FFI function // later by a FFI function
@ -81,9 +81,9 @@ sinqController::sinqController(const char *portName,
We try to connect to the port via the port name provided by the constructor. We try to connect to the port via the port name provided by the constructor.
If this fails, the function is terminated via exit. If this fails, the function is terminated via exit.
*/ */
pasynOctetSyncIO->connect(ipPortConfigName, 0, &ipPortAsynOctetSyncIO_, pasynOctetSyncIO->connect(ipPortConfigName, 0, &pasynOctetSyncIOipPort_,
NULL); NULL);
if (status != asynSuccess || ipPortAsynOctetSyncIO_ == nullptr) { if (status != asynSuccess || pasynOctetSyncIOipPort_ == nullptr) {
errlogPrintf("Controller \"%s\" => %s, line %d:\nFATAL ERROR (cannot " errlogPrintf("Controller \"%s\" => %s, line %d:\nFATAL ERROR (cannot "
"connect to MCU controller).\n" "connect to MCU controller).\n"
"Terminating IOC", "Terminating IOC",
@ -252,6 +252,16 @@ sinqController::sinqController(const char *portName,
exit(-1); exit(-1);
} }
status = createParam("ADAPTIVE_POLLING", asynParamInt32, &adaptivePolling_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\" => %s, line %d:\nFATAL ERROR (creating a "
"parameter failed with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
exit(-1);
}
status = createParam("ENCODER_TYPE", asynParamOctet, &encoderType_); status = createParam("ENCODER_TYPE", asynParamOctet, &encoderType_);
if (status != asynSuccess) { if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR, asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
@ -356,7 +366,7 @@ asynStatus sinqController::couldNotParseResponse(const char *command,
int line) { int line) {
asynStatus pl_status = asynSuccess; asynStatus pl_status = asynSuccess;
asynPrint(ipPortAsynOctetSyncIO_, ASYN_TRACE_ERROR, asynPrint(pasynOctetSyncIOipPort_, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nCould not interpret " "Controller \"%s\", axis %d => %s, line %d:\nCould not interpret "
"response \"%s\" for command \"%s\".\n", "response \"%s\" for command \"%s\".\n",
portName, axisNo, functionName, line, response, command); portName, axisNo, functionName, line, response, command);
@ -385,7 +395,7 @@ asynStatus sinqController::paramLibAccessFailed(asynStatus status,
int line) { int line) {
if (status != asynSuccess) { if (status != asynSuccess) {
asynPrint(ipPortAsynOctetSyncIO_, ASYN_TRACE_ERROR, asynPrint(pasynOctetSyncIOipPort_, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\n Accessing the " "Controller \"%s\", axis %d => %s, line %d:\n Accessing the "
"parameter library failed for parameter %s with error %s.\n", "parameter library failed for parameter %s with error %s.\n",
portName, axisNo, functionName, line, parameter, portName, axisNo, functionName, line, parameter,
@ -654,9 +664,9 @@ asynStatus setMaxSubsequentTimeouts(const char *portName,
if (ptr == nullptr) { if (ptr == nullptr) {
/* /*
We can't use asynPrint here since this macro would require us We can't use asynPrint here since this macro would require us
to get a ipPortAsynOctetSyncIO_ from a pointer to an asynPortDriver. to get a pasynOctetSyncIOipPort_ from a pointer to an asynPortDriver.
However, the given pointer is a nullptr and therefore doesn't However, the given pointer is a nullptr and therefore doesn't
have a ipPortAsynOctetSyncIO_! printf is an EPICS alternative which have a pasynOctetSyncIOipPort_! printf is an EPICS alternative which
works w/o that, but doesn't offer the comfort provided works w/o that, but doesn't offer the comfort provided
by the asynTrace-facility by the asynTrace-facility
*/ */

View File

@ -291,6 +291,7 @@ class epicsShareClass sinqController : public asynMotorController {
int motorAcclFromDriver() { return motorAcclFromDriver_; } int motorAcclFromDriver() { return motorAcclFromDriver_; }
int motorHighLimitFromDriver() { return motorHighLimitFromDriver_; } int motorHighLimitFromDriver() { return motorHighLimitFromDriver_; }
int motorLowLimitFromDriver() { return motorLowLimitFromDriver_; } int motorLowLimitFromDriver() { return motorLowLimitFromDriver_; }
int adaptivePolling() { return adaptivePolling_; }
int encoderType() { return encoderType_; } int encoderType() { return encoderType_; }
// Additional members // Additional members
@ -298,14 +299,14 @@ class epicsShareClass sinqController : public asynMotorController {
double idlePollPeriod() { return idlePollPeriod_; } double idlePollPeriod() { return idlePollPeriod_; }
double movingPollPeriod() { return movingPollPeriod_; } double movingPollPeriod() { return movingPollPeriod_; }
asynUser *asynUserSelf() { return pasynUserSelf; } asynUser *asynUserSelf() { return pasynUserSelf; }
asynUser *ipPortAsynOctetSyncIO() { return ipPortAsynOctetSyncIO_; } asynUser *pasynOctetSyncIOipPort() { return pasynOctetSyncIOipPort_; }
// ========================================================================= // =========================================================================
protected: protected:
// Pointer to the port user which is specified by the char array // Pointer to the port user which is specified by the char array
// `ipPortConfigName` in the constructor // `ipPortConfigName` in the constructor
asynUser *ipPortAsynOctetSyncIO_; asynUser *pasynOctetSyncIOipPort_;
double movingPollPeriod_; double movingPollPeriod_;
double idlePollPeriod_; double idlePollPeriod_;
msgPrintControl msgPrintControl_; msgPrintControl msgPrintControl_;
@ -345,6 +346,7 @@ class epicsShareClass sinqController : public asynMotorController {
int motorAcclFromDriver_; int motorAcclFromDriver_;
int motorHighLimitFromDriver_; int motorHighLimitFromDriver_;
int motorLowLimitFromDriver_; int motorLowLimitFromDriver_;
int adaptivePolling_;
int encoderType_; int encoderType_;
#define LAST_SINQMOTOR_PARAM encoderType_ #define LAST_SINQMOTOR_PARAM encoderType_