文章目录
一、概述
除了记录支持模块之外,每个记录类型可以有任意数量的设备支持模块。设备支持的目的是对记录处理例程隐藏硬件特定的细节。因此,可以为新设备开发支持而不改变记录支持例程。
设备支持例程了解记录定义。它还知道如何直接与硬件对话,或如何调用与硬件接口的设备驱动程序。因此,设备支持例程是数据库记录中的硬件特定字段与设备驱动程序或硬件本身之间的接口。
Release 3.14.8引入了扩展设备支持的概念,它提供了一个可选的接口,当记录的地址在运行时发生变化时,设备支持可以实现该接口来获取通知。这允许记录被重新连接到不同类型的I/O设备,或者仅仅是同一设备上的不同信号。
数据库 common 包含两个与设备相关的字段:
- dtype:设备类型
- dset:设备支持输入表的地址
DTYP字段包含设备ASCII定义定义的菜单选项索引。iocInit使用这个字段和devsupf .h中定义的设备支持结构来初始化字段DSET。因此,记录支持可以通过DSET字段定位其关联的设备支持。
设备支持模块可以分为两个基本类:同步和异步的。同步设备支持用于可被访问而不延迟I/O的硬件。许多基于寄存器的设备是同步设备。其他设备,例如所有GPIB设备,只能通过I/O请求访问,这可能需要花费大量的时间来完成。这些设备必须具有关联的异步设备支持。异步设备支持使得创建具有链接记录的数据库更加困难。
如果一个设备的访问延迟小于几微秒,那么同步设备支持是合适的。如果一个设备引起超过100微秒的延迟,那么异步设备支持是合适的。如果延迟在这些值之间,你对该做什么的猜测和我的一样好。也许你应该问问硬件设计师为什么要创建这样的设备。
如果一个设备需要很长时间来接受请求,除了异步设备支持之外还有另一个选项。可以创建一个驱动程序,定期轮询它所连接的所有输入设备。设备支持只返回最新的轮询值。对于输出,设备支持只是通知驱动程序必须写入一个新值。EPICS Allen Bradley设备/驱动程序支持就是一个很好的例子。
二、同步设备支持模块例程
/* Create the dset for devAiSoft */
long init_record();
long read_ai();
struct {
long number;
DEVSUPFUN report;
DEVSUPFUN init;
DEVSUPFUN init_record;
DEVSUPFUN get_ioint_info;
DEVSUPFUN read_ai;
DEVSUPFUN special_linconv;
}devAiSoft={
6,
NULL,
NULL,
init_record,
NULL,
read_ai,
NULL
};
epicsExportAddress(dset,devAiSoft);
static long init_record(void *precord)
{
aiRecord *pai = (aiRecord *)precord;
long status;
/* ai.inp : 输入特性,类型为DBLINK
struct link {
struct dbCommon *precord; //指向记录所属链接的指针
short type; //类型
unsigned short flags;
struct lset *lset;
char *text; // 原始链接文本
union value value;
};
typedef struct link DBLINK; */
/* ai.inp必须为CONSTANT, PV_LINK, DB_LINK 或 CA_LINK*/
switch (pai->inp.type) {
case (CONSTANT) :
/* 将pat.inp字段的值,按照DBF_DOUBLE类型,写入pai.val*/
if(recGblInitConstantLink(&pai->inp,DBF_DOUBLE,&pai->val))
//写入成功时,将未定义标志设置为FALSE,此时写入完成
pai->udf = FALSE;
break;
case (PV_LINK) :
case (DB_LINK) :
case (CA_LINK) :
break;
default :
//其它情况下,会发出报错信息
recGblRecordError(S_db_badField, (void *)pai,"devAiSoft (init_record) Illegal INP field");
return(S_db_badField);
}
/* 确保记录处理例程不执行任何转换 */
pai->linr=0;
return(0);
}
static long read_ai(void *precord)
{
aiRecord*pai =(aiRecord *)precord;
long status;
/*
从一个数据库链接引用字段获取值,格式:
long dbGetLink(
struct db_link *plink, // 指向链接字段的指针
short dbrType,
void *pbuffer, // 返回数据的指针
long *options, // 选项的指针
long *nRequest); // 所需元素数目的指针
• options:如果不需要选项,可以是NULL
• nRequest:对于标量,可以是NULL
*/
//status=dbGetGetLink(&(pai->inp.value.db_link),(void *)pai,DBR_DOUBLE,&(pai->val),0,1)替换成下面函数
status=dbGetLink(&(pai->inp.value.db_link),DBR_DOUBLE,&(pai->val),0,1)
if (pai->inp.type!=CONSTANT && RTN_SUCCESS(status)) {
pai->udf = FALSE;
}
return(2); /* 不转换 */
}
这个例子是devAiSoft,支持软模拟输入。INP字段可以是常量、数据库链接或通道访问链接。只提供了两个例程(其余声明为NULL)。init_record例程首先检查链接类型是否有效。如果链接是常量,则初始化VAL。如果链接是过程变量链接,则调用dbCaGetLink将其转换为通道访问链接。如果链接是数据库或通道访问链接,read_ai例程将获得一个输入值,否则它不需要做任何事情。
三、异步设备支持模块例程
这个例子展示了如何编写一个异步设备支持例程。它的操作顺序如下:
- 第一次调用时,PACT是FALSE。它安排一个回调(myCallback)例程在DISV字段指定的秒数之后被调用。
- 它打印一条消息,说明处理已经开始,将PACT设置为TRUE,然后返回。记录处理例程返回打,但是未完成处理。
- 当指定的时间过去时,调用myCallback。它调用dbScanLock来锁定记录,调用process,并调用dbScanUnlock来解锁记录。它直接调用记录支持模块的process,通过dbCommon中的RSET字段定位该条目,而不是调用dbProcess。dbProcess不会调用process,因为PACT是TRUE。
- 当process执行时,它再次调用read_ai。这时候PACT是TRUE。
- read_ai打印一条消息,说明processing已经完成,并返回状态为2。通常会返回值0。值2告诉记录支持例程不要尝试任何转换。这是模拟输入记录使用的一个约定(一个糟糕的约定!)
- 当read_ai返回时,processing例程完成记录处理。
此时记录已被完全处理。下一次process被调用时,所有一切重头开始。
请注意,这是一个人工示例,因为此表单的实际代码更可能使用callbackRequestProcessCallbackDelayed函数来执行所需的处理。
static void myCallback(CALLBACK *pcallback)
{
struct dbCommon *precord;
struct typed_rset *prset;
callbackGetUser(precord,pcallback);
prset = (struct typed_rset *)(precord->rset);
dbScanLock(precord);
(*prset->process)(precord);
dbScanUnlock(precord);
}
static long init_record(struct aiRecord *pai)
{
CALLBACK *pcallback;
switch (pai->inp.type) {
case (CONSTANT) :
pcallback = (CALLBACK *)(calloc(1,sizeof(CALLBACK)));
callbackSetCallback(myCallback,pcallback);
callbackSetUser(pai,pcallback);
pai->dpvt = (void *)pcallback;
break;
default :
recGblRecordError(S_db_badField,(void *)pai,"devAiTestAsyn (init_record) Illegal INP field");
return(S_db_badField);
}
return(0);
}
static long read_ai(struct aiRecord *pai)
{
CALLBACK *pcallback = (CALLBACK *)pai->dpvt;
if(pai->pact) {
pai->val += 0.1; /* Change VAL just to show we’ve done something. */
pai->udf = FALSE; /* We modify VAL so we are responsible for UDF too. */
printf("Completed asynchronous processing: %s\n",pai->name);
return(2); /* don’t convert*/
}
printf("Starting asynchronous processing: %s\n",pai->name);
pai->pact=TRUE;
callbackRequestDelayed(pcallback,pai->disv);
return(0);
}
/* Create the dset for devAiTestAsyn */
struct {
long number;
DEVSUPFUN report;
DEVSUPFUN init;
DEVSUPFUN init_record;
DEVSUPFUN get_ioint_info;
DEVSUPFUN read_ai;
DEVSUPFUN special_linconv;
}devAiTestAsyn={
6,
NULL,
NULL,
init_record,
NULL,
read_ai,
NULL
};
epicsExportAddress(dset,devAiTestAsyn);
void callbackRequestProcessCallbackDelayed(epicsCallback *pcallback, //回调函数
int Priority, //优先级
void *pRec, //指定记录的指针
double seconds) //延时秒数
四、设备支持例程
本节介绍DSET中定义的例程。任何不适用于特定记录类型的例程都必须声明为NULL。
#ifdef __cplusplus
extern "C" {
typedef long (*DEVSUPFUN)(void *); /* 设备支持函数指针 */
#else
typedef long (*DEVSUPFUN)(); /*设备支持函数指针 */
#endif
#ifndef USE_TYPED_DSET
typedef struct dset { /* 设备支持入口表 */
long number; /* 支持例程数 */
DEVSUPFUN report; /* 打印报告 */
DEVSUPFUN init; /* 初始化支持层 */
DEVSUPFUN init_record; /* 为特定记录初始化设备 */
DEVSUPFUN get_ioint_info; /* 获取IO中断信息 */
/* 其它函数依赖于记录 */
} dset;
#else
typedef typed_dset dset;
#endif /* USE_TYPED_DSET */
/* exists only to disambiguate dset dbCommon::dset */
typedef dset unambiguous_dset;
4.1 生成设备报告
long report( int interest );
这个例程负责报告它找到的所有I/O卡。提供interest是为了支持不同类型的报告,或者控制显示多少详细信息。如果一个设备支持模块正在
使用一个驱动程序,它可能选择不实现这个例程,因为驱动程序生成报告。
4.2 初始化设备处理
long init( int after );
这个例程在IOC初始化时调用两次。任何动作都是特定于设备的。这个例程被调用两次:一次是在初始化任何数据库记录之前,在所有记录初始化之后,在扫描任务开始之前。after的值在记录初始化之前为0,在记录初始化之后为1。
4.3 初始化特定记录
long init_record(void *precord); /* addr of record*/
记录支持init_record例程调用这个例程。
4.4 获取I/O中断信息
long get_ioint_info(int cmd,struct dbCommon *precord,IOSCANPVT *ppvt);
这由I/O中断扫描任务调用。如果cmd是(0,1),那么当相关的记录被(放入,取出)一个I/O扫描列表时,这个例程被调用。
4.5 其它设备支持例程
所有其他设备支持例程都是特定于记录类型的。
五、扩展设备支持
本节描述设备支持层支持对记录的硬件地址进行在线更改所需的其他行为和例程。
5.1 基本原理
在R3.14.8之前的版本中,可以更改记录的INP或OUT字段的值,但是(除非使用了软设备支持),这通常对设备支持的行为没有任何影响。一些设备支持已经被写入,每次处理时都会检查这个硬件地址字段的更改,但它们是少数,而且在任何情况下,它们都不提供在运行时在不同设备支持层之间切换的任何方法,因为没有软件可以在iocInit之后为DSET字段查找新值。
扩展设备接口经过精心设计,以保持与现有设备和记录支持层的最大向后兼容性,因此,它不能仅仅在DSET中引入新的例程:
- 不同的记录类型有不同数量的DSET例程
- 每个设备支持层定义自己的DSET结构布局
- 一些设备支持层将自己的例程添加到DSET (GPIB, BitBus)
由于基本和扩展设备支持层都必须在同一个IOC中共存,因此执行了一些关于是否允许更改特定记录的设备地址的规则:
- 在iocInit上连接到设备支持层的记录不允许在运行时更改地址字段。
- 扩展设备支持层不需要同时实现add_record和del_record例程,因此某些设备可能只允许单向更改。
- 旧设备支持层在字段更改之前被通知并允许拒绝地址更改(它无法看到新地址)。
- 更改字段后通知新设备支持层,它可以拒绝接受记录。在这种情况下,记录将被设置为永久繁忙(PACT=true),直到地址被接受为止。
- 记录支持层也可以通过使它们的地址字段特殊化来获得关于这个过程的通知,在这种情况下,记录类型的特殊例程可以在新地址显示给
设备支持层之前拒绝接受它。
如果地址更改被拒绝,对输入或输出字段的更改将导致向执行更改的软件传递错误或异常。如果这是一个通道访问客户端,结果是生成
一个异常回调。
要切换到不同的设备支持层,必须在INP或OUT字段之前更改DTYP字段。
如果一个记录被设置为I/O中断扫描,但新层不支持,扫描将被更改为被动。
5.2 初始化 / 注册
实现扩展行为的设备支持必须在设备支持条目表中提供init例程。对这个例程的第一次调用(传递0)中,它在对devExtend的调用中注册其设备支持扩展表(DSXT)的地址。
此注册要求的唯一例外是当设备支持使用CONSTANT的链接类型时。在这种情况下,系统将自动为特定的支持层注册一个空的DSXT(这个DSXT所指向的add_record和del_record例程都不做任何事情并返回零)。这个例外允许现有的软通道设备支持层继续工作而不需要任何修改,因为iocCore软件已经照顾到PV链路地址的变化。
下面是DSXT和注册它的初始化例程的示例:
static struct dsxt myDsxt = {
add_record, del_record
};
static long init(int pass) {
if (pass==0) devExtend(&myDsxt);
return 0;
}
只能在设备支持初始化过程的第一个过程中调用devExtend,并为该设备支持层注册DSXT。如果在其他时间调用,它将记录一条错误消息并立即返回。
5.3 设备支持扩展表
struct dsxt的完整定义可在devSup.h中找到,当前如下所示:
typedef struct dsxt {
long (*add_record)(struct dbCommon *precord);
long (*del_record)(struct dbCommon *precord);
} dsxt;
将来可能会增加此表以支持其他功能;这种扩展只能通过更改devSup.h头文件和重建EPICS Base和所有支持模块来实现,因此记录类型和设备支持都不允许对该表进行任何私人使用。
这两个函数指针是一种方法,通过这两个函数指针,可以向扩展设备支持通知它正在被给定的记录实例,或者正在从它的控制中移出的记录实例。在这两种情况下,唯一的参数是指向相关记录的指针,代码必须将其转换为记录类型的适当指针。例程的返回值应为零表示成功,或者返回EPICS错误状态代码。
5.4 添加记录例程
long add_record(struct dbCommon *precord);
调用此函数是为了向设备支持提供一个新记录。在iocInit期间,在pass 0和pass 1调用常规设备支持init_record例程(在上面的12.4.3节中描述)之间,它也被调用。在转换现有设备支持层时,此例程通常与旧的init_record例程非常相似,尽管在某些情况下,可能需要根据所涉及的特定记录类型执行更多的工作。这些情况下需要的额外代码通常可以直接从记录类型实现本身复制。这是必要的,因为记录类型不知道正在发生的地址更改,因此设备支持必须自行执行任何位掩码 生成和/或回读值转换。本文档并不试图描述各种不同标准记录类型的所有必要处理,尽管以下(不完整)列表作为设备支持作者的帮助:
- mbbi/mbbo记录类型:设置SHFT,将NOBT和SHFT转换为MASK
- bi/bo记录类型:设置SHFT,转换SHFT为MASK
- analog记录类型:计算ESLO和EOFF
- 输出记录类型:可能从硬件读取当前值并返回转换为VAL,或者将当前记录输出值发送给硬件。这种行为不是必需的,也没有定义,应该做什么也不明显。如果使用OROC和/或OIF=Incremental没有记录,可能会出现并发症;这个问题的解决方案还有待社区考虑。
如果add_record例程发现任何错误,比如在链接地址中,它应该返回一个非零的错误状态值来拒绝记录。这将导致设置记录的PACT字段,从而阻止对该记录的任何进一步处理,直到接受对它的其他地址更改为止。
5.5 删除记录例程
long del_record(struct dbCommon *precord);
调用此函数是为了通知设备支持更改记录的硬件地址的请求,并允许设备支持人员释放它可能专用于此特定记录的任何资源。
在调用此例程之前,如果已将记录设置为I/O中断,则它的扫描字段将更改为被动。这可以确保在调用del_record成功返回后,设备支持的get_ioint_info例程不会被调用,尽管如果del_record例程拒绝地址更改,也可能导致错过中断。
如果设备支持由于某种原因无法与硬件断开连接,此例程应返回非零错误状态值,这将阻止更改硬件地址。在这种情况下,如果扫描字段最初设置为I/O中断,扫描字段将恢复。
成功调用del_record后,该记录的DPVT字段设置为NULL,PACT被清除,以供新设备支持人员使用。
5.6 初始化记录例程
来自DSET的init_record例程(第12.4.3节)由记录类型调用,并且仍然必须提供,因为记录类型的每个记录初始化是在首次调用DSXT的add_record例程后的一段时间后运行的。大多数记录类型此时执行记录字段的初始化,扩展的设备支持层可能需要修复记录覆盖的任何内容。以下(不完整)列表是为设备支持作者提供的帮助:
- mbbi/mbbo记录类型:从SHFT计算掩码
- analog记录类型:计算ESLO和EOFF
- output记录类型:从硬件执行初始原始值的回读。