1# @Language: Markdown
2# @Software: VS Code/MacDown
3# @Author  : Di Wang
4# @Email   : [email protected]

asyn

阅读本文需要对epics中的概念比较熟悉. 建议阅读EPICS AppDevGuide. 也许是历史原因, asyn文档中的概念很混乱. 很大程度上是源于一词多义与函数命名上的随意. 比如非常多的相似Callback函数名称. 也比如driver, support, implement等词, 既可以表达asyn中的专用概念, 也会作寻常用法使用. 还比如对driver一词的使用. 有时候是epics driver support, 有时候又是port driver, 有时候又写成device driver. 而port driver, 一般来说指一系列hardware driver, 但又容易与为了减少代码编写量而引入的asynPortDriver这一个c++ class的名字混淆. 因此我决定撰写此文便于将来的自己理解.

aysn的出现

20世纪90年代中期对于epics驱动开发有这么几个问题

  1. 每一个新的device都需要编写多个record类型的device support, 比如ai, ao, mbbi, mbbo等
  2. 当多个record访问一个device时, 需要处理同步和互斥的问题
  3. 支持不同访问协议的同一设备需要重复编写驱动.

于是Marty Kraimer提出了asyn的想法, 和Mark Rivers一起开发了最初版本的asyn.

asyn如何解决这些问题?

  1. 使用接口, 在device support和driver之间定义了一套通用的接口. 比如connect, read, write, report等等. 这些函数需要由底层驱动实现.
  2. 引入了port的概念. 一个port对应一条通信路径, driver会register port, device support会连接到一个port
  3. asynManager负责port的管理, 同时处理同步与互斥
  4. 提供了一套标准的epics device support, 涵盖了常用的大多数record type. 这样引入新设备时候只需要专注于实现与设备通信的接口.

asyn 中的一些概念

  • user, 在epics环境下, 就是指某个record type的device support.
  • port driver, 也就是hardware driver, 表示实际与hardware交互的代码. port driver必须实现asynCommon, 然后实现多个communication interface, 比如asynOctet, asynInt32.

在asyn中, user指使用asyn port的一端,一般来说就是epics record. 而port driver则是与各式各样hardware交互的底层驱动, 在port driver中,需要实现一些接口, 比如标准接口asynCommon, 命令读写接口asynOctet。但如果协议比较复杂, 可以继续添加中间层, 减少port driver的编写代码量.

例如gpib, 就添加了asynGpibasynGpibPort两个接口, 前者由epics record support调用, 然后会调用asynGpibPort中的函数, 而asynGpibPort中的函数由port driver实现. 这样的话就可以把一些不涉及具体GPIB硬件的函数移到asynGpib中.

asyn 流程

asynManager作为中间层. 服务于epics device support和hardware driver.

对于epics device support

  • 在epics record init过程中会调用pasynManager->createAsynUser(processCallback, 0), 获取一个没有绑定port的asynUser
  • 解析record的INP或者OUT field 指定的port name和port addr
  • 调用connectDevice来让asynUser连接上这个port. 因为asynManager知道所有的port driver, 所以它依靠port name就能顺利把port的信息放到asynUser里.
  • 这时候asynUser就可以调用pasynInterface = pasynManager->findInterface(pasynUser, asynInt32Type, 1) 得到想调用的interface. 比如此时的pasynInterface->pinterface 就是一个asynInt32的接口, 使用这个接口才可以与hardware通信.
  • 对于asynchronous的设备, 肯定不能直接读取, 于是就在record被process(例如定时Scan的时候), 调用pasynManager->queueRequest(pasynUser, 0, 0), 把请求放到port thread里
  • asynManager调用Callback函数processCallback(createAsynUser时候注册的).
  • 在Callback函数中会调用pasynInterface->pinterface->read来读写hardware.读写完成后将结果放到一个暂存空间, 然后再调用epics base 提供的callbackRequestProcessCallback函数来触发record的process. 将暂存空间中的值放到record的value field里.
  • 对于enum类型的record, 如bi, mbbi等, 还会注册一个interruptCallbackEnum回调函数, 这些回调函数会调用setEnums来设置enum的string和value. 在port driver中使用doCallbacksEnum来把string和对应的value数组传给这个回调函数.

注意processCallback这个Callback是负责实现异步的record process, 而当hardware值变化时通知record则是通过interruptCallback完成.

对于port driver

  • 实现与hardware交互的接口, 比如connect serial的时候就要使用OS API连接到tty.
  • 注册一个iocsh函数, 方便启动ioc时候用户设置port的名字和各种参数(比如IP或serial的baud rate)
  • 这个iocsh函数会调用registerPort, 在asynManager中, 会新生成一个线程, 用来处理这个port.
  • 调用registerInterface注册实现的接口, 其中asynCommon接口必须提供. 在port thread中会调用asynCommon提供的pasynCommon->connect(drvPvt,pasynUser)方法连接hardware. 常用的还有aysnOctet接口中的read, write函数, asynInt32中的read, write函数

asyn interface

interface, 在asyn中更强调其在面向对象编程(OO)中的特点, 即抽象方法. 在c语言中需要使用函数指针达成这一目标(在OO环境中需谨慎使用"实现"一词). 在asyn的不同层级中互相调用这些抽象方法, 有些抽象方法由asyn中的一些component(比如asynManager实现了asynManager接口)实现, 而另一些抽象方法需要最终实现为与hardware交互的代码. 比如connect, read, write等方法.

常用接口

  • asynCommon, 包含三个函数, report, connect, disconnect
  • asynTrace, debug用, 不用管细节.
  • asynLockPortNotify, 用于addrChangeDriver, 不过似乎有bug(R4-44-2)
  • asynDrvUser, 用于传递参数给port driver. 似乎也是最近引入的. 常用用法: 在record的INP field中填写一个参数, 然后这个参数会被传递给port driver, port driver决定在收到这个参数后如何处理返回值. 这个接口主要用在asynPortDriver中, 为了使用这个接口还专门开发了两个class: paramList class和paramVal class (其实就是用了一个std::vector, 记录了参数名和其index)用于处理这个参数.
  • asynXXX, 一系列接口, 比如asynOctet, asynInt32, asynFloat64Array, asynOption 等等. port driver需要根据hardware的情况选择性实现一部分.

interrupt 处理

支持interrupt的接口会有registerInterruptUser函数, 其实就是注册一下interruptCallbackInt32 callback到asynManager里, 这个callback基本都是用来process record的.

 1typedef struct asynInt32 {
 2    asynStatus (*write)(void *drvPvt, asynUser *pasynUser, epicsInt32 value);
 3    asynStatus (*read)(void *drvPvt, asynUser *pasynUser, epicsInt32 *value);
 4    asynStatus (*getBounds)(void *drvPvt, asynUser *pasynUser,
 5                           epicsInt32 *low, epicsInt32 *high);
 6    asynStatus (*registerInterruptUser)(void *drvPvt,asynUser *pasynUser,
 7                           interruptCallbackInt32 callback, void *userPvt,
 8                           void **registrarPvt);
 9    asynStatus (*cancelInterruptUser)(void *drvPvt, asynUser *pasynUser,
10                           void *registrarPvt);
11} asynInt32;

对于asynManager

提供了registerInterruptSource 函数, 产生一个与interface type相关的interruptBase的变量. 里面有一个链表callbackList.

提供了addInterruptUser函数, 把record希望注册的Callback函数放到interruptBase 中的链表中.

提供了interruptStart函数, 从interruptBase中取出链表中的Callback元素, 然后一个个执行.

对于epics device support

当某个record希望响应interrupt时, 需要调用addInterruptUser .

record init时候调用pPvt->pint32->registerInterruptUser(pPvt->int32Pvt,pPvt->pasynUser, pPvt->interruptCallback,pPvt,&pPvt->registrarPvt);, 虽然函数是个接口, 但各个asynXXXBase已经实现了, 可以在port driver中覆盖, 但似乎都使用默认值即可. 不像read write函数一样需要被覆盖. registerInterruptUser函数在asynXXXBase中实现, 该版本会调用pasynManager->addInterruptUser(pasynUser,pinterruptNode), 其中interruptNode内部包含了一个Callback函数. 把这个Callback函数加入到一个链表中. 这样interrupt发生时, 就可以用Callback来process自己.

对于port driver

注册interface时候, 也注册了interface的registerInterruptUser函数.

调用pasynManager->registerInterruptSource(portName,&pdrvPvt->asynInt32, &pdrvPvt->asynInt32Pvt); 其实就是由asynManager生成一个interruptBase类型的变量, 然后分配给asynInt32Pvt.

当interrupt发生时, 会依次调用这个链表中所有的Callback函数. 这个过程一般发生在isr里, 而isr自然是由port driver实现. 在isr里调用interruptStart函数, 得到这个链表, 然后调用Callback去process这个record.

asynXXXBase

关于asynXXXBase.c, 其实就是简化了注册asynXXX接口过程, 实现了一些默认的读写方法, 实现了registerInterruptUser, 调用registerInterface来注册这些默认实现. 在port driver里, 可以这样写

1    pasynInt32->write = int32Write;
2    pasynInt32->read = int32Read;
3    pasynInt32->getBounds = int32GetBounds;
4    pdrvPvt->asynInt32.interfaceType = asynInt32Type;
5    pdrvPvt->asynInt32.pinterface  = pasynInt32;
6    pdrvPvt->asynInt32.drvPvt = pdrvPvt;
7    status = pasynInt32Base->initialize(pdrvPvt->portName, &pdrvPvt->asynInt32);

asynXXXArrayBase.h更进一步, 定义了一个超大macro. 但由于port driver里还是需要重新实现read write函数, 所以实际上没省略多少代码量. 然后又搞出了一个asynStandardInterfacesBase, 由它实现实际的registerInterface, 这部分已经集成在asynPortDriver里. 可以看到里面使用了pasynStandardInterfacesBase->initialize(portName, pInterfaces, this->pasynUserSelf, this);.

使用技巧

可以设置对应的info field来开启初始化读, hardware值变化时output record的值也随之更新, FIFO的大小.

 1record(stringout, "$(P)SoOctetRB")
 2{
 3   field(PINI, "1")
 4   field(DTYP, "asynOctetWrite")
 5   field(OUT,  "@asyn($(PORT),$(ADDR),$(TIMEOUT))OCTET_VALUE")
 6   field(VAL,  "test")
 7   info(asyn:INITIAL_READBACK,"1")
 8   info(asyn:READBACK,"1")
 9   info(asyn:FIFO, "$(FIFO)")
10}

在ioc中加入asynRecord, 其中IMAX和OMAX代表Max. size of input/output array

1dbLoadRecords("db/asynRecord.db", "P=<ioc>, R=<record>, PORT=<port>, ADDR=<addr>, OMAX=<omax>, IMAX=<imax>")

asynPortDriver

几个关键函数

  • createParam, 创建parameter
  • setIntegerParam
  • setDoubleParam
  • callParamCallbacks会调用callCallbacks, 而callCallbacks会根据parameter的类型来调用不同的函数. 这些函数又会调用pInterrupt->callback(pInterrupt->userPvt, pInterrupt->pasynUser, value);, 其中pInterrupt->callback中存的是epics device support中通过registerInterruptUser注册的interruptCallback. 最后就是会更新value, 然后process record.

其中callCallbacks函数如下, 当调用setIntegerParam类函数更新parameter value时, 会把这个parameter的index放到flags这个vector里, 这样就可以只调用写入新值的parameter的callback.

 1asynStatus paramList::callCallbacks(int addr)
 2{
 3    int index;
 4    asynStatus status = asynSuccess;
 5
 6    if (!interruptAccept) return asynSuccess;
 7
 8    try {
 9        for (size_t i = 0; i < this->flags.size(); i++)
10        {
11            index = this->flags[i];
12            paramVal *param(getParameter(index));
13            if (!param->isDefined()) continue;
14            switch(param->type) {
15                case asynParamInt32:
16                    status = int32Callback(index, addr);
17                    break;
18                case asynParamInt64:
19                    status = int64Callback(index, addr);
20                    break;
21                case asynParamUInt32Digital:
22                    status = uint32Callback(index, addr, this->vals[index]->uInt32CallbackMask);
23                    this->vals[index]->uInt32CallbackMask = 0;
24                    break;
25                case asynParamFloat64:
26                    status = float64Callback(index, addr);
27                    break;
28                case asynParamOctet:
29                    status = octetCallback(index, addr);
30                    break;
31                default:
32                    break;
33            }
34        }
35    }
36    catch (ParamListInvalidIndex&) {
37        return asynParamBadIndex;
38    }
39    flags.clear();
40    return status;
41}

asynTrace

三种mask, 分别用于定义日志级别, 日志打印格式, 日志额外信息, marco定义值如下:

 1#define ASYN_TRACE_ERROR     0x0001
 2#define ASYN_TRACEIO_DEVICE  0x0002
 3#define ASYN_TRACEIO_FILTER  0x0004
 4#define ASYN_TRACEIO_DRIVER  0x0008
 5#define ASYN_TRACE_FLOW      0x0010
 6#define ASYN_TRACE_WARNING   0x0020
 7
 8/* traceIO mask definitions*/
 9#define ASYN_TRACEIO_NODATA 0x0000
10#define ASYN_TRACEIO_ASCII  0x0001
11#define ASYN_TRACEIO_ESCAPE 0x0002
12#define ASYN_TRACEIO_HEX    0x0004
13
14/* traceInfo mask definitions*/
15#define ASYN_TRACEINFO_TIME 0x0001
16#define ASYN_TRACEINFO_PORT 0x0002
17#define ASYN_TRACEINFO_SOURCE 0x0004
18#define ASYN_TRACEINFO_THREAD 0x0008

example usage, R4-35之后, 参数设置可以更加灵活, 不止可以使用数字, 还可以用不区分大小写的字符串.

1asynSetTraceMask port,0,ASYN_TRACE_ERROR
2asynSetTraceIOMask port,0,ascii+escape
3asynSetTraceInfoMask port,0,1+port+TRACEINFO_SOURCE|ASYN_TRACEINFO_THREAD

日志级别 TraceMask

默认为ASYN_TRACE_ERROR

  • ASYN_TRACE_ERROR, error
  • ASYN_TRACEIO_DEVICE, epics device support级别
  • ASYN_TRACEIO_FILTER , 在device support和hardware driver之间的级别
  • ASYN_TRACEIO_DRIVER , hardware driver
  • ASYN_TRACE_FLOW , 会输出各种调用关系
  • ASYN_TRACE_WARNING

打印级别 TraceIOMask

默认为ASYN_TRACEIO_NODATA

  • ASYN_TRACEIO_NODATA,
  • ASYN_TRACEIO_ASCII
  • ASYN_TRACEIO_ESCAPE
  • ASYN_TRACEIO_HEX

额外信息 TraceInfoMask

默认为ASYN_TRACEINFO_TIME

  • ASYN_TRACEINFO_TIME
  • ASYN_TRACEINFO_PORT
  • ASYN_TRACEINFO_SOURCE
  • ASYN_TRACEINFO_THREAD

some evil struct

 1typedef struct interruptNode{
 2    ELLNODE node;
 3    void    *drvPvt;
 4}interruptNode;
 5typedef struct interruptBase {
 6    ELLLIST      callbackList;
 7    ELLLIST      addRemoveList;
 8    BOOL         callbackActive;
 9    BOOL         listModified;
10    port         *pport;
11    asynInterface *pasynInterface;
12}interruptBase;
13
14typedef struct interruptNodePvt {
15    ELLNODE  addRemoveNode;
16    BOOL     isOnList;
17    BOOL     isOnAddRemoveList;
18    epicsEventId  callbackDone;
19    interruptBase *pinterruptBase;
20    interruptNode nodePublic;
21}interruptNodePvt;