A handy tool for device support with EPICS is asynDriver, which dramatically reduces the amount of code needed to write device support for EPICS records. To learn more about how asyn works and specifically about asyn port drivers, we include some suggested reading and lectures.

Webinar on asyn.

Webinar on asyn port drivers.

Asyn's git repository, has an example port driver in the TestAsynPortDriver directory.

Example port driver which we will go through the process of writing lower down.

Important Notes


While writing a port driver there are a few things that are important to keep in mind:

  1. When the port driver is created, it will create a parameter list that will allow you to read and write to and from your epics records, however this parameter list is a local to your port driver. This can lead to weird behavior where you write to a record through your port driver and then try and read the value. If you read through the port driver you will get the value you wrote, however if your record doesn't scan or doesn't allow for inputs when you try and read through channel access, you will get the value before it was written to or your port driver may crash.

  2. You decide upon your supported data types within the constructor of your port driver, as well as whether you would like to support working with multiple records.

  3. While generally the default behaviour we inherit from the base asyn port driver is fine, we can create our own implementation with our own desired behaviour to overwrite the base class.

Writing A Port Driver


To help you out with that we walk through implementing a much simpler port driver that essentially counts and puts the value back into an Epics record.

This is expecting the same file structure seen here. We will be walking through the steps to create the port driver seen here. There are some best practices about where to add these lines and how to write these files I don't know them.


Step 1. Modify some files so we can use Asyn and create a port driver

  • to include in configure/RELEASE

    ASYN_MODULE_VERSION = R4.39-1.0.1 #Replace with whatever the current up-to-date version is
    ASYN = $(EPICS_MODULES)/asyn/$(ASYN_MODULE_VERSION)
    EPICS_BASE = $(BASE_SITE_TOP)/$(BASE_MODULE_VERSION)
  • to include in $(YourAppName)App/src/Makefile

    $(YourAppName)_DBD += asyn.dbd
    $(YourAppName_LIBS += asyn




Step 2. Create the EPICS Records we will modify and read with our port driver

  • In $(YourAppName)App/Db create counter.db
## counter.db  

record(ai, "$(USER):one")
{
    field(DTYP, "asynFloat64")
    field(INP, "@asyn($(PORT),$(ADDR),$(TIMEOUT))COUNTER")
    field(SCAN, "I/O Intr")
}

record(ao, "$(USER):two")
{
    field(DTYP, "asynInt32")
    field(OUT, "@asyn($(PORT),$(ADDR),$(TIMEOUT))STEP")
    field(PINI, "1")
    field(VAL, "23")
}
  • In $(YourAppName)App/Db/Makefile add the following line

DB += counter.db

We use "I/O Intr" within the SCAN field of our EPICS records, this is an interrupt driven scan so whenever we update a value within the record, it'll process.


Step 3. Defining your port driver

  • Again in $(YourAppName)/src create two files
    • $(NAME).cpp
    • $(NAME).h
  • Within $(NAME).h

    counter.h
    #include <iostream>
    #include <cstdint>
    #include <memory>
    #include <functional>
    #include <map>
    
    #include <iocsh.h>
    #include <epicsExport.h>
    #include <epicsThread.h>
    #include <epicsEvent.h>
    #include <epicsTimer.h>
    #include <epicsTypes.h>
    #include <asynPortDriver.h>
    
    using namespace std;
    
    #define CounterString "COUNTER"
    #define StepString "STEP"
    
    class CounterDriver : public asynPortDriver {
      public:
        CounterDriver(const char *portName);
        void counterTask(void);
      
      protected:
        int CounterIndex;
        int StepIndex;
    };
    

  • Understanding counter.h

    There's some important things we defined in this file, so let's break them down a little.

    • These strings are used to define the records which our port driver will connect and modify, they correspond to the INP or OUT field of your EPICS record.

      #define CounterString "COUNTER"
      #define StepString "STEP"
      
    • This is where we state the capabilities of our port driver, any methods of the port driver we want to override or any unique functions. We include some protected integers here to store the indexes of the parameters with the port driver's parameter list.

      class CounterDriver : public asynPortDriver {
        public:
          CounterDriver(const char *portName);
          void counterTask(void);
        
        protected:
          int CounterIndex;
          int StepIndex;
      };

  • Within $(NAME).cpp
counter.cpp
#include "counter.h"
#include <unistd.h>

void counterTask(void *driverPointer); //Necessary otherwise we get an error about functions being declared

CounterDriver::CounterDriver(const char *portName) : asynPortDriver                                                      (
                                                      portName,
                                                      1,
                                                      asynFloat64Mask | asynInt32Mask | asynDrvUserMask,
                                                      asynFloat64Mask | asynInt32Mask,
                                                      ASYN_MULTIDEVICE,
                                                      1,
                                                      0,
                                                      0
                                                     )
{
    createParam(CounterString, asynParamFloat64, &CounterIndex);
    createParam(StepString, asynParamInt32, &StepIndex);
    asynStatus status;
    status = (asynStatus)(epicsThreadCreate("CounterTask", epicsThreadPriorityMedium, epicsThreadGetStackSize(epicsThreadStackMedium), (EPICSTHREADFUNC)::counterTask, this) == NULL);
    if (status)
    {
        printf("Thread didn't launch please do something else");
        return;
    }
}

void counterTask(void* driverPointer)
{
    CounterDriver *pPvt = (CounterDriver *) driverPointer;
    pPvt->counterTask();
}

void CounterDriver::counterTask(void)
{
    int step;
    double count;
    while(1)
    {
        sleep(1); //Necessary because the thread will go faster than I/O Scan will accept and write inputs
        getDoubleParam(CounterIndex, &count); 
        getIntegerParam(StepIndex, &step);

        setDoubleParam(CounterIndex, count + step);
        setIntegerParam(StepIndex, step + 1);
        callParamCallbacks();
    }
}

extern "C" 
{
    int CounterDriverConfigure(const char* portName)
    {
        new CounterDriver(portName);
        return asynSuccess;
    }
    static const iocshArg initArg0 = {"portName", iocshArgString};
    static const iocshArg * const initArgs[] = {&initArg0};
    static const iocshFuncDef initFuncDef = {"CounterDriverConfigure", 1, initArgs};
    static void initCallFunc(const iocshArgBuf *args)
    {
        CounterDriverConfigure(args[0].sval);
    }
    void CounterDriverRegister(void)
    {
        iocshRegister(&initFuncDef, initCallFunc);
    }
    epicsExportRegistrar(CounterDriverRegister);
}

asynDrvUserMask is necessary whenever working with multiple parameters, your port driver will not give you the behavior you expect when working with multiple records if this flag is not here

Here's the meat of our port driver, where we initialize our port driver and create parameters, that our port driver can access and modify; for more information about the options that are inherited from the base asynport driver, refer to here.

CounterDriver::CounterDriver(const char *portName) : asynPortDriver                                                     (
                                                      portName,
                                                      1,
                                                      asynFloat64Mask | asynInt32Mask | asynDrvUserMask,
                                                      asynFloat64Mask | asynInt32Mask,
                                                      ASYN_MULTIDEVICE,
                                                      1,
                                                      0,
                                                      0
                                                     )
{
    createParam(CounterString, asynParamFloat64, &CounterIndex);
    createParam(StepString, asynParamInt32, &StepIndex);
    asynStatus status;
    status = (asynStatus)(epicsThreadCreate("CounterTask", epicsThreadPriorityMedium, epicsThreadGetStackSize(epicsThreadStackMedium), (EPICSTHREADFUNC)::counterTask, this) == NULL);
    if (status)
    {
        printf("Thread didn't launch please do something else");
        return;
    }
}





counterTask is a class specific method, in the constructor we launch a thread where the port driver completes this task, the code is fairly straightforward, feel free to do whatever you want.

void counterTask(void* driverPointer)
{
    CounterDriver *pPvt = (CounterDriver *) driverPointer;
    pPvt->counterTask();
}

void CounterDriver::counterTask(void)
{
    int step;
    double count;
    while(1)
    {
        sleep(1); //Necessary because the thread will go faster than I/O Scan will accept and write inputs
        getDoubleParam(CounterIndex, &count); 
        getIntegerParam(StepIndex, &step);

        setDoubleParam(CounterIndex, count + step);
        setIntegerParam(StepIndex, step + 1);
        callParamCallbacks();
    }
}




This extern "C" block allows us to create our port driver through the iocsh, essentially it's a block of code that wraps the code to launch our port driver and lets us call it from iocsh.

extern "C" {
  int CounterDriverConfigure(const char* portName) {
    new CounterDriver(portName);
    return asynSuccess;
  }
  static const iocshArg initArg0 = {"portName", iocshArgString};
  static const iocshArg * const initArgs[] = {&initArg0};
  static const iocshFuncDef initFuncDef = {"CounterDriverConfigure", 1, initArgs};
  static void initCallFunc(const iocshArgBuf *args)
  {
    CounterDriverConfigure(args[0].sval);
  }
  void CounterDriverRegister(void) {
    iocshRegister(&initFuncDef, initCallFunc);
  }
  epicsExportRegistrar(CounterDriverRegister);
}

The values you expect to be present in your epics records may not match up with the local copy of the parameter in your port driver's parameter list



Step 4: Create a .dbd file (This file should be in $(YourAppName)App/src/

  • To create our port driver from iocshell we need to add a <name>.dbd file with the following line
registrar(CounterDriverRegister)
  • We also need to add the following lines to our $(YourAppName)App/src/Makefile
CountingTest_DBD += <name>.dbd
CountingTest_SRCS += counter.cpp

USR_CPPFLAGS += -std=c++11

Step 5: Modifying your st.cmd

Replace the things surrounded like <THIS> with your items.


  • Add the following lines to iocBoot/ioc<Ioc Name>/st.cmd
CounterDriverConfigure("<PORTNAME>")
dbLoadRecords("db/counter.db", "USER=<USERNAME>,PORT=<PORTNAME>,ADDR=0,TIMEOUT=0") #For more detailed reading see the links at the top

Step 6: Use Make to build your application and then run ./st.cmd, you should be able to observe your pv's through camonitor and edit them with caput. Try doing things with other records.


Congratulations on making a port driver.

  • No labels