Introduction

Imagine a number of different devices that one would like to integrate into EPICS. They use a common communication protocol or software library, but are otherwise substantially different. A concrete example would be a number of PLCs that have different roles and attached peripherals. The device variables exposed by each device are different, and although the interface library or protocol allows one to reach all variables, implementing separate EPICS support for each device is tedious.

It is convenient to have generic EPICS device support implementing the common protocol (or using the common library), then binding records to the variables available on each device solely through EPICS database record definitions.

Consider a fictitious example:

record(ai, "$(PREFIX):DEV1:Temperature") {
    field(DTYP, "myDevSup")
    field(INP, "@dev=1 type=float addr=0x5042")
}

record(longin, "$(PREFIX):DEV1:Status") {
    field(DTYP, "myDevSup")
    field(INP, "@dev=1 type=short addr=0x50a1")
}

record(ai, "$(PREFIX):DEV2:Temperature") {
    field(DTYP, "myDevSup")
    field(INP, "@dev=2 type=float addr=0xfb03")
}

The myDevSup EPICS device support layer and the underlying driver need know nothing about the device variables, they merely know how to parse the string given in the records’ INP/OUT links and shuffle the requested data to and from the registers at the specified addresses. Depending on how communication to the device is implemented, it is even possible that no connection to a device exists until a record requesting it is initialized.

Autoparam features

autoparamDriver makes implementing such a generic driver easier by

  • dynamic creation of handles for each device device variable that is requested by EPICS records during IOC initialization;

  • providing facilities for forwarding hardware interrupts to I/O Intr records;

  • being based on asynPortDriver with all the benefits this brings — most importantly, generic EPICS device support layer with a number of useful features;

  • supplementing asynPortDriver with a more homogeneous C++ interface

    • by allowing registration of handler functions instead of requiring the driver to override read and write methods and dispatch “manually”.

    • by providing a templated setParam() in lieu of separate setIntegerParam(), setDoubleParam() etc.

Concepts and terminology

See also

Variables, records and reasons

What is a device variable?

A device variable is a piece of data on a device. A process variable is a piece of data accessible via Channel Access or PV Access protocol on an EPICS network. The goal of a driver based on autoparamDriver is to map device variables to EPICS records, which in turn make them available as process variables on the network.

When mentioning a “variable”, this documentation always refers to a device variable, never a process variable.

How does a record refer to a device variable?

As autoparamDriver is based on asyn, it leverages its generic device support and INST_IO link parsing. It relies on asyn’s DrvUser interface to obtain a so-called “reason string”. A record thus looks like:

record(ai, "$(PREFIX):DEV1:Temperature") {
    field(DTYP, "asynFloat64")
    field(INP, "@asyn($(PORT_NAME)) this is a reason string")
}

Here, the macro $(PORT_NAME) names an asyn port with a driver subclassed from Autoparam::Driver. When the record is initialized, the driver will be given the entire reason string. It is split on the first space and the first word (in the above example, “this”) is called a function while the rest are called arguments.

The combination of a function and its arguments is called a device address. Any record referring to the same combination of function and arguments will have the same address, and will thus bind to the same device variable. When records are being initialized, the following happens:

Each device variable is backed by a parameter. This term refers to asyn-managed cache of device variable properties (c.f. asynPortDriver::createParam()), such as general status, alarm status, and (for scalars) value. While handlers (described below) are used to update records on request from the EPICS database, parameters are used to update records on request from the driver (e.g. in response to hardware interrupts).

How does the driver refer to a device variable?

As the IOC is initialized, the driver will automatically identify the requested variables and instantiate parameters as described above. Instances of Autoparam::DeviceVariable serve as handles:

  • when a record is processed, the driver is given a DeviceVariable identifying which data the record is interested in;

  • when the driver wants to update I/O Intr records asynchronously, it uses DeviceVariable to specify which parameters to update.

The Autoparam::DeviceVariable class as used by the Autoparam::Driver base class does not do much: apart from being used as a handle, it provides access to the function and the Autoparam::DeviceAddress, and that’s it. However, DeviceVariable is polymorphic and it is expected that the driver subclassing Autoparam::Driver will deal with subclasses of DeviceVariable; see Autoparam::Driver::createDeviceVariable(). The subclass (or subclasses, there can be several) can contain anything the driver needs to work with the variable, like data type conversion, hardware interrupt subscription, etc.

Similarly, Autoparam::DeviceAddress is a polymorphic class. The only requirement is that it is equality-comparable to other addresses and that two addresses compare equal when they refer to the same device variable. The intent is that DeviceAddress represents the parsed device address that is later used to construct DeviceVariable.

Record processing

How does the driver react to record processing?

A driver subclassing Autoparam::Driver registers handlers for functions by calling Autoparam::Driver::registerHandlers() in its constructor. The registerHandlers() method associates the combination of a function name and a value type (see Autoparam::AsynType) with a read handler, a write handler and an interrupt registrar. The signatures depend on the value type; they are grouped and documented in Autoparam::Handlers structures.

Handlers take a reference to Autoparam::DeviceVariable as the first argument. The task of a read handler is to read the value of the requested variable from the device and return it (for scalars) or write it to the provided buffer (for arrays/waveforms). The task of the write handler is to write the value given as its second argument to the requested variable on the device.

Both read and write handlers can be NULL. In this case, a default handler is used. For scalars, the default read handler simply returns the value stored in the parameter associated with the device variable while the write handler stores the value provided by the record in that same parameter. For arrays, both handlers return an error since array parameters cannot store values themselves.

asyn interfaces: passing data between records and the driver

Looking again at the short example record above (How does a record refer to a device variable?), notice that it uses the DTYP field to choose one of asyn EPICS device support modules. These are documented in the asynDriver EPICS support documentation and need to be well understood both by driver authors and database authors:

  • The driver author needs to choose an appropriate interface for each device function. Each function can only be bound to one interface.

  • The database author needs to know which interface is used for a particular function in order to fill in the DTYP field correctly. Records also change behavior based on device support; for example, the ai record can use both asynInt32 and asynFloat64 interfaces, but behaves differently with regard to conversion.

From the point of view of autoparamDriver, there is a 1:1 mapping between the basic asyn interfaces (i.e. excluding the “averaging” and similar higher-level device support code) and the types of data that they are meant to convey between the records and the device. This mapping is documented in Autoparam::AsynType. Note, however, that the subclassed driver should not need to use this struct, or refer to the asyn interfaces (or parameter types) as such.

Instead, the driver implements a mapping from one data type to another. As an example, consider a device that has a function for reading a 16-bit unsigned big-endian integer. One needs to choose the appropriate data type supported by the asyn/EPICS side, which turns out to be epicsInt32. Thus, the driver needs to register a handler (using Autoparam::Driver::registerHandlers()) for epicsInt32 that talks to the device in terms of epicsUInt16 with endianness conversion.

There are some subtleties regarding working with big 32-bit integers, digital I/O and strings that are discussed under Topics of interest.

How does the driver process I/O Intr records?

There are three mechanisms that can be used to push values into I/O Intr records that are appropriate for different situations:

  • during or after running write or read handlers,

  • in response to hardware interrupts,

  • or at any other time, in particular from a background scanning thread.

Which mechanism is appropriate depends on the device; they may also be combined.

During or after running write or read handlers

By default, should the write handler for some variable complete successfully, the driver will automatically update the cached parameter value and process the callbacks registered by I/O Intr records that are bound to the same variable to update them with the written value. This follows the behavior of default (i.e. NULL) handlers and is appropriate when a device variable is not really backed by hardware, but is a “soft” variable in the driver.

It may also be appropriate when the device variable is a “write-only” variable and does not allow the driver to read back the value. In that case, the last written value is the only data available, and updating the parameter after a write allows one to have a NULL read handler that simply returns the last written data.

While the default (i.e. NULL) write handler always behaves like this, this automatic processing of interrupts can be overridden for normal handlers either

The latter also allows reads to update I/O Intr records bound to the same device variable. This is an edge use case and is thus not done by default, but the mechanism is there and can be used explicitly.

A more common use case is a “write-read” operation which writes to the device and obtains a readback of the value in the same transaction. The default behavior of write handlers is not appropriate: while it does update the value of I/O Intr records, it uses the value that was written. To instead use the value that was read back, the write handler should

From a background scanning thread

The approach used for write-read operations is generally applicable and can be used anywhere. In particular, some devices can only operate efficiently if data is requested periodically in large batches, and the driver needs to do this kind of update in a background thread. When data arrives, the background thread can update many scalar parameters by calling Autoparam::Driver::setParam(), then call asynPortDriver::callParamCallbacks() once. For arrays, Autoparam::Driver::doCallbacksArray() does both operations at the same time.

Note that handlers are called with the driver locked. When using the above functions (or any other driver function, for that matter) from a different context (such as a background thread), ensure that the driver is locked (see asynPortDriver::lock() and asynPortDriver::unlock()).

To make it easier for the background thread to know which device variables are of interest, Autoparam::Driver::getInterruptVariables() returns a list of DeviceVariable that one or more records have subscribed to. Be aware that the list can change at any time, both during database initialization and during runtime due to SCAN field changes.

In response to hardware interrupts

Setting a parameter and calling the callbacks can be done in response to hardware interrupts as well, in the same way as from a background thread. However, hardware interrupts may need to be enabled, or, for network-connected devices, an event subscription needs to be set up. This could, in principle, be done by obtaining the list of required variables using the Autoparam::Driver::getInterruptVariables() method. However, as this list can change at any time, something would need to check the list periodically and enable or disable the appropriate interrupts.

A more appropriate approach is to register a function that is called whenever a record’s SCAN field changes to or from I/O Intr. Such an Autoparam::InterruptRegistrar can be registered together with read and write handlers.