From the previous discussion, it should be clear that many things happen as a result of record processing. The details of what happens are dependent on the record type. In order to allow new record types and new device types without impacting the core IOC system, the concept of record support and device support has been created. For each record type, a record support module exists. It is responsible for all record specific details. In order to allow a record support module to be independent of device specific details, the concept of device support has been created.
A record support module consists of a standard set of routines that can be called by database access routines. This set of routines implements record specific code. Each record type can define a standard set of device support routines specific to that record type.
By far the most important record support routine is process, which dbProcess calls when it wants to process a record. This routine is responsible for all the details of record processing. In many cases it calls a device support I/O routine. The next section gives an overview of what must be done in order to process a record. Next is a description of the entry tables that must be provided by record and device support modules. The remaining sections give example record and device support modules and describe some global routines useful to record support modules.
The record and device support modules are the only modules that are allowed to include the record specific include files as defined in epics/share/epicsH/rec. Thus they are the only routines that access record specific fields without going through database access.
Each record type also has zero or more sets of device support routines. Record types without associated hardware, e.g. calculation records, normally do not have any associated device support. Record types with associated hardware normally have a device support module for each device type. The concept of device support isolates IOC core software and even record support from device specific details.
Corresponding to each record type is a set of record support routines. The set of routines is the same for every record type. These routines are located via a Record Support Entry Table (RSET), which has the following structure
struct rset { /* record support entry table */ long number; /* number of support routine */ RECSUPFUN report; /* print report */ RECSUPFUN init; /* init support */ RECSUPFUN init_record; /* init record */ RECSUPFUN process; /* process record */ RECSUPFUN special; /* special processing */ RECSUPFUN get_value; /* get value field */ RECSUPFUN cvt_dbaddr; /* cvt dbAddr */ RECSUPFUN get_array_info; RECSUPFUN put_array_info; RECSUPFUN get_units; RECSUPFUN get_precision; RECSUPFUN get_enum_str; /* get string from enum item */ RECSUPFUN get_enum_strs; /* get all enum strings */ RECSUPFUN put_enum_str; /* put enum item from string */ RECSUPFUN get_graphic_double; RECSUPFUN get_control_double; RECSUPFUN get_alarm_double; };Each record support module must define its RSET. The external name must be of the form:
<record_type>RSETAny routines not needed for the particular record type should be initialized to the value NULL. Look at the example below for details.
Device support routines are located via a Device Support Entry Table (DSET), which has the following structure:
struct dset { /* device support entry table */ long number; /* number of support routines */ DEVSUPFUN report; /* print report */ DEVSUPFUN init; /* init support */ DEVSUPFUN init_record; /* init support for particular record */ DEVSUPFUN get_ioint_info; /* get io interrupt information */ /* other functions are record dependent*/ };Each device support module must define its associated DSET. The external name must be the same as the name which appears in devSup.ascii.
Any record support module which has associated device support must also include definitions for accessing its associated device support modules. The field "dset", which is located in dbCommon, contains the address of the DSET. It is given a value by iocInit.
/* Create RSET - Record Support Entry Table*/ #define report NULL #define initialize NULL static long init_record(); static long process(); #define special NULL static long get_value(); #define cvt_dbaddr NULL #define get_array_info NULL #define put_array_info NULL static long get_units(); static long get_precision(); #define get_enum_str NULL #define get_enum_strs NULL #define put_enum_str NULL static long get_graphic_double(); static long get_control_double(); static long get_alarm_double(); struct rset xxxRSET={ RSETNUMBER, report, initialize, init_record, process, special, get_value, cvt_dbaddr, get_array_info, put_array_info, get_units, get_precision, get_enum_str, get_enum_strs, put_enum_str, get_graphic_double, get_control_double, get_alarm_double}; /* declarations for associated DSET */ struct xxxdset { /* analog input dset */ long number; DEVSUPFUN dev_report; DEVSUPFUN init; DEVSUPFUN init_record; /* returns: (-1,0)=>(failure,success)*/ DEVSUPFUN get_ioint_info; DEVSUPFUN read_xxx; }; /* forward declaration for internal routines*/ static void alarm(); static void monitor();The above declarations define the Record Support Entry Table (RSET), a template for the associated Device Support Entry Table (DSET), and forward declarations to private routines.
The RSET must be declared with an external name of xxxRSET. It defines the record support routines supplied for this record type. Note that forward declarations are given for all routines supported and a NULL declaration for any routine not supported.
The template for the DSET is declared for use by this module.
static long init_record(pxxx,pass) struct xxxRecord *pxxx; int pass; { struct xxxdset *pdset; long status; if(pass==0) return(0); if((pdset = (struct xxxdset *)(pxxx->dset)) == NULL) { recGblRecordError(S_dev_noDSET,pxxx,"xxx: init_record"); return(S_dev_noDSET); } /* must have read_xxx function defined */ if( (pdset->number < 5) || (pdset->read_xxx == NULL) ) { recGblRecordError(S_dev_missingSup,pxxx,"xxx: init_record"); return(S_dev_missingSup); } if( pdset->init_record ) { if((status=(*pdset->init_record)(pxxx))) return(status); } return(0); }This routine, which is called by iocInit twice for each record of type xxx, checks to see if it has a proper set of device support routines and, if present, calls the init_record entry of the DSET.
During the first call to init_record (pass=0) only initializations relating to this record can be performed. During the second call (pass=1) initializations that may refer to other records can be performed. Note also that during the second pass, other records may refer to fields within this record. A good example of where these rules are important is a waveform record. The VAL field of a waveform record actually refers to an array. The waveform record support module must allocate storage for the array. If another record has a database link referring to the waveform VAL field then the storage must be allocated before the link is resolved. This is accomplished by having the waveform record support allocate the array during the first pass (pass=0) and having the link reference resolved during the second pass (pass=1).
static long process(pxxx) struct xxxRecord *pxxx; { struct xxxdset *pdset = (struct xxxdset *)(pxxx->dset); long status; unsigned char pact=pxxx->pact; if( (pdset==NULL) || (pdset->read_xxx==NULL) ) { /* leave pact true so that dbProcess doesnt keep calling*/ pxxx->pact=TRUE; recGblRecordError(S_dev_missingSup,pxxx,"read_xxx"); return(S_dev_missingSup); } /* pact must not be set true until read_xxx completes*/ status=(*pdset->read_xxx)(pxxx); /* read the new value */ if(!pact && pxxx->pact) return(0); /* return if beginning of asynch processing*/ pxxx->pact = TRUE; recGblGetTimeStamp(pxxx); /* check for alarms */ alarm(pxxx); /* check event list */ monitor(pxxx); /* process the forward scan link record */ recGblFwdLink(pxxx); pxxx->pact=FALSE; return(status); }The record processing routines are the heart of the IOC software. The record specific process routine is called by dbProcess whenever it decides that a record should be processed. Process decides what record processing really means. The above is a good example of what should be done. In addition to being called by dbProcess the process routine may also be called by asynchronous record completion routines.
The above model supports both synchronous and asynchronous device support routines. For example, if read_xxx is an asynchronous routine, the following sequence of events will occur:
static long get_value(pxxx,pvdes) struct xxxRecord *pxxx; struct valueDes *pvdes; { pvdes->field_type = DBF_FLOAT; pvdes->no_elements=1; (float *)(pvdes->pvalue) = &pxxx->val; return(0); } static long get_units(paddr,units) struct dbAddr *paddr; char *units; { struct xxxRecord *pxxx=(struct xxxRecord *)paddr->precord; strncpy(units,pxxx->egu,sizeof(pxxx->egu)); return(0); } static long get_graphic_double(paddr,pgd) struct dbAddr *paddr; struct dbr_grDouble *pgd; { struct xxxRecord *pxxx=(struct xxxRecord *)paddr->precord; if(paddr->pfield == (void *)(&pxxx->val)) { pgd->upper_disp_limit = pxxx->hopr; pgd->lower_disp_limit = pxxx->lopr; } else recGblGetGraphicDouble(paddr,pgd); return(0); } /* similar routines would be provided for get_control_double and get_alarm_double*/These are a few examples of various routines supplied by a typical record support package. The functions that must be performed by the remaining routines are described in Section 5 on page 72.
static void alarm(pxxx) struct xxxRecord *pxxx; { double val; float hyst,lalm,hihi,high,low,lolo; unsigned short hhsv,llsv,hsv,lsv; if(pxxx->udf == TRUE ){ recGblSetSevr(pxxx,UDF_ALARM,VALID_ALARM); return; } hihi=pxxx->hihi; lolo=pxxx->lolo; high=pxxx->high; low=pxxx->low; hhsv=pxxx->hhsv; llsv=pxxx->llsv; hsv=pxxx->hsv; lsv=pxxx->lsv; val=pxxx->val; hyst=pxxx->hyst; lalm=pxxx->lalm; /* alarm condition hihi */ if (hhsv && (val >= hihi || ((lalm==hihi) && (val >= hihi-hyst)))) { if(recGblSetSevr(pxxx,HIHI_ALARM,pxxx->hhsv)) pxxx->lalm = hihi; return; } /* alarm condition lolo */ if (llsv && (val <= lolo || ((lalm==lolo) && (val <= lolo+hyst)))) { if(recGblSetSevr(pxxx,LOLO_ALARM,pxxx->llsv)) pxxx->lalm = lolo; return; } /* alarm condition high */ if (hsv && (val >= high || ((lalm==high) && (val >= high-hyst)))) { if(recGblSetSevr(pxxx,HIGH_ALARM,pxxx->hsv)) pxxx->lalm = high; return; } /* alarm condition low */ if (lsv && (val <= low || (lalm==low) && (val <= low+hyst)))) { if(recGblSetSevr(pxxx,LOW_ALARM,pxxx->lsv)) pxxx->lalm = low; return; } /*we get here only if val is out of alarm by at least hyst*/ pxxx->lalm=val; return; }This is a typical set of code for checking alarms conditions for an analog type record. The actual set of code can be very record specific. Note also that other parts of the system can raise alarms. The algorithm is to always maximize alarm severity, i.e. the highest severity outstanding alarm will be reported.
The above algorithm also honors a hysteresis factor for the alarm. This is to prevent alarm storms from occurring in the event that the current value is very near an alarm limit and noise makes it continually cross the limit. The above algorithm ensures that the alarm being reported will not change unless the value changes by the hysteresis value.
static void monitor(pxxx) struct xxxRecord *pxxx; { unsigned short monitor_mask; float delta; monitor_mask = recGblResetAlarms(pxxx); /* check for value change */ delta = pxxx->mlst - pxxx->val; if(delta<0.0) delta = -delta; if (delta > pxxx->mdel) { /* post events for value change */ monitor_mask |= DBE_VALUE; /* update last value monitored */ pxxx->mlst = pxxx->val; } /* check for archive change */ delta = pxxx->alst - pxxx->val; if(delta<0.0) delta = 0.0; if (delta > pxxx->adel) { /* post events on value field for archive change */ monitor_mask |= DBE_LOG; /* update last archive value monitored */ pxxx->alst = pxxx->val; } /* send out monitors connected to the value field */ if (monitor_mask){ db_post_events(pxxx,&pxxx->val,monitor_mask); } return; }The first part of the code will be common to most record types. Note that nsta and nsev will have the value 0 after this routine completes. This is necessary to ensure that alarm checking starts fresh after processing completes. The code also takes care of raising alarm monitors when a record changes from an alarm state to the no alarm state. It is essential that record support routines follow the above model or else alarm processing will not follow the rules.
IMPORTANT: The record support module is responsible for calling db_post_event for any fields that change as a result of record processing. Also it should NOT call db_post_event for fields that do not change.
The name of each of these routines begins with "recGbl".
recGblSetSevr( void *precord, short nsta, short nsevr);Returns: (TRUE, FALSE) if (did, did not) change nsta and nsev.
unsigned short recGblResetAlarms(void *precord);Returns: Initial value for monitor_mask
recGblDbaddrError( long status, struct dbAddr *paddr, char *pcaller_name); /* calling routine name */This routine can be called whenever an error is returned from a call to dbNameToAddr, dbGetxxx, or dbPutxxx. It interfaces with the system wide error handling system to display the following information: Status information, process variable name, calling routine.
recGblRecordError( long status, void *precord, /* addr of record */ char *pcaller_name); /* calling routine name */This routine interfaces with the system wide error handling system to display the following information: Status information, record name, calling routine.
recGblRecsupError( long status, struct dbAddr *paddr, char *pcaller_name, /* calling routine name */ char *psupport_name); /* support routine name*/This routine interfaces with the system wide error handling system to display the following information: Status information, record name, calling routine, record support entry name.
recGblGetGraphicDouble( struct dbAddr *paddr, struct dbr_grDouble *pgd);This routine can be used by the get_graphic_double record support routine to obtain graphics values for fields that it doesn't know how to set.
recGblGetControlDouble( struct dbAddr *paddr, struct dbr_ctrlDouble *pcd);This routine can be used by the get_control_double record support routine to obtain control values for fields that it doesn't know how to set.
recGblGetAlarmDouble( struct dbAddr *paddr, struct dbr_alDouble *pcd);This routine can be used by the get_alarm_double record support routine to obtain control values for fields that it doesn't know how to set.
recGblGetPrec( struct dbAddr *paddr, long *pprecision);This routine can be used by the get_precision record support routine to obtain the precision for fields that it doesn't know how to set the precision.
recGblGetTimeStamp(void *precord)This routine gets the current time stamp.
recGblFwdLink( void *precord);This routine can be used by process to request processing of forward links.
recGblGetLinkValue( struct link *plink, void *precord, short dbrType, void *pdest, long *poptions, long *pnRequest);This routine gets a value from an input link. If the link is a constant this call amounts to a NOP.
recGblPutLinkValue( struct link *plink, void *precord, short dbrType, void *pdest, long *pnRequest);This routine writes a value to an output link. If the link is a constant this call amounts to a NOP.
recGblInitFastInLink( struct link *plink, void *precord, short dbrType, char *ca_string);Initialize a fast input link. This routine should be used if scalar data with options is desired. If the link is not a channel access link or a database link this amounts to a NOP. ca_string is the uppercase name of the field that Channel Access is to get a value from.
recGblInitFastOutLink( struct link *plink, void *precord, short dbrType, char *ca_string);Initialize a fast output link. This routine should be used if scalar data is to be written via the link. If the link is not a channel access link or a database link this amounts to a NOP. "ca_string" is the uppercase name of the field that Channel Access will take its value from.
recGblGetFastLink( struct link *plink, void *precord, void *pdest);Gets a value from a fast input link. This routine can only be used if the link was initialized via recGblInitFastInLink. If the link is a constant link this amounts to a NOP.
recGblPutFastLink( struct link *plink, void *precord, void *psource);Puts a value to a fast output link. This routine can only be used if the link was initialized via recGblInitFastOutLink. If the link is a constant link this amounts to a NOP.
report(void *precord); /* addr of record*/This routine is not used by most record types. Any action is record type specific.
init(void);This routine is called once at IOC initialization time. Any action is record type specific. Most record types do not need this routine.
init_record( void *precord, /* addr of record*/ int pass);iocInit calls this routine twice (pass=0 and pass=1) for each database record of the type handled by this routine. It must perform the following functions:
process(void *precord); /* addr of record*/This routine must follow the guidelines specified previously.
special( struct dbAddr *paddr, int after); /*(FALSE,TRUE)=>(Before,After)Processing*/This routine implements the record type specific special processing for the field referred to by dbAddr. Note that it is called twice. Once before any changes are made to the associated field and once after. File special.h defines special types. This routine is only called for user special fields. A field is defined to be user special in the ASCII record definition.
get_value( void *precord, /* addr of record*/ struct valueDes *p); /*addr of value description struct*/This routine returns a description of the VAL field of the record. The structure valueDes, which is defined in recSup.h, is defined as follows:
struct valueDes { int field_type, long no_elements, void *pvalue}
cvt_dbaddr(struct dbAddr *paddr);This routine is called by dbNameToAddr if the field has special set equal to SPC_DBADDR. A typical use is when a field refers to an array. This routine can change any combination of the dbAddr fields: no_elements, field_type, field_size, and special. For example if the VAL field of a waveform record is passed to dbNameToAddr, cvt_dbaddr would change dbAddr so that it refers to the actual array rather then VAL.
get_array_info( struct dbAddr *paddr, long *no_elements, long *offset);This routine returns the current number of elements and the offset of the first value of the specified array. The offset field is meaningful if the array is actually a circular buffer.
put_array_info( struct dbAddr *paddr, long old_offset, long nNew);This routine is called after new values have been placed in the specified array.
get_units( struct dbAddr *paddr, char *punits);This routine sets units equal to the engineering units for the field.
get_precision( struct dbAddr *paddr, long *precision);This routine gets the precision, i.e. number of decimal places, which should be used to convert the field value to an ASCII string. Note that recGblGetPrec should be called for fields not directly related to the value field.
get_enum_str( struct dbAddr *paddr, char *p);This routine sets *p equal to the ASCII string for the field value. The field must have type DBF_ENUM.
get_enum_strs( struct dbAddr *paddr, struct dbr_enumStrs *p);This routine gives values to all fields of structure dbr_enumStrs.
put_enum_str( struct dbAddr *paddr, char *p);Given an ASCII string, this routine updates the database field. It compares the string with the string values associated with each enumerated value and if it finds a match sets the database field equal to the index of the string which matched.
get_graphic_double( struct dbAddr *paddr, struct dbr_grDouble *p); /* addr of return info*/This routine fills in the graphics related fields of structure dbr_grDouble. Note that recGblGetGraphicDouble should be called for fields not directly related to the value field.
get_control_double( struct dbAddr *paddr, struct dbr_ctrlDouble *p); /* addr of return info*/This routine gives values to all fields of structure dbr_ctrlDouble. Note that recGblGetControlDouble should be called for fields not directly related to the value field.
get_alarm_double( struct dbAddr *paddr, struct dbr_alDouble *p); /* addr of return info*/This routine gives values to all fields of structure dbr_alDouble.
A device support routine has knowledge of the record definition. It also knows how to talk to the hardware directly or how to call a device driver which interfaces to the hardware. Thus the device support routines are the interface between hardware specific fields in a database record and device drivers or the hardware itself.
The common portion of every database record contains two device related fields:
/* 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}; static long init_record(pai) struct aiRecord *pai; { long status; /* ai.inp must be a CONSTANT or a PV_LINK or a DB_LINK or a CA_LINK*/ switch (pai->inp.type) { case (CONSTANT) : pai->val = pai->inp.value.value; break;The example is devAiSoft, which supports soft analog inputs. The INP field can be a constant or a database link or a channel access link. Only two routines are provided (the rest are declared NULL). The init_record routine first checks that the link type is valid. If the link is a constant it initializes VAL If the link is a Process Variable link it calls dbCaGetLink to turn it into a Channel Access link. The read_ai routine obtains an input value if the link is a database or Channel Access link, otherwise it doesn't have to do anything.
case (PV_LINK) : status = dbCaAddInLink(&(pai->inp), (void *)pai,"VAL"); if(status) return(status); break; case (DB_LINK) : break; default : recGblRecordError(S_db_badField, (void *)pai, "devAiSoft (init_record) Illegal INP field"); return(S_db_badField); } /* Make sure record processing routine does not perform any conversion*/ pai->linr=0; return(0); } static long read_ai(pai) struct aiRecord *pai; { long status; long options=0; long nRequest=1; status=recGblGetLinkValue(&(pai->inp.value.db_link),(void *)pai,DBR_DOUBLE, &(pai->val),&options,&nRequest); } if(status) return(status); return(2); /*don't convert*/ }
/* Create the dset for devAiTestAsyn */ 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; } devAiTestAsyn={ 6, NULL, NULL, init_record, NULL, read_ai, NULL}; /* control block for callback*/ struct callback { CALLBACK callback; sruct dbCommon *precord; WDOG_ID wd_id; }; static void myCallback(pcallback) struct callback *pcallback; { struct dbCommon *precord=pcallback->precord; struct rset *prset=(struct rset *)(precord->rset); dbScanLock(precord); *(prset->process)(precord); dbScanUnlock(precord); } static long init_record(pai) struct aiRecord *pai; { struct callback *pcallback; /* ai.inp must be a CONSTANT*/ switch (pai->inp.type) { case (CONSTANT) : pcallback = (struct callback *)(calloc(1,sizeof(struct callback))); pai->dpvt = (void *)pcallback; callbackSetCallback(myCallback,pcallback); pcallback->precord = (struct dbCommon *)pai; pcallback->wd_id = wdCreate(); pai->val = pai->inp.value.value; pai->udf = FALSE; break; default : recGblRecordError(S_db_badField, (void *)pai, "devAiTestAsyn (init_record) Illegal INP field"); return(S_db_badField); } return(0); } static long read_ai(pai) struct aiRecord *pai; { struct callback *pcallback=(struct callback *)(pai->dpvt); int wait_time; /* ai.inp must be a CONSTANT*/ switch (pai->inp.type) { case (CONSTANT) : if(pai->pact) { printf("%s Completed\n",pai->name); return(2); /* don`t convert*/ } else { wait_time = (int)(pai->val * vxTicksPerSecond); if(wait_time<=0) return(0); callbackSetPriority(pai->prio,pcallback); printf("%s Starting asynchronous processing\n",pai->name); wdStart(pcallback->wd_id,wait_time,callbackRequest,(int)pcallback); pai->pact = TRUE; return(0); } default : if(recGblSetSevr(pai,SOFT_ALARM,VALID_ALARM)) { if(pai->stat!=SOFT_ALARM) { recGblRecordError(S_db_badField, (void *)pai, "devAiTestAsyn (read_ai) Illegal INP field"); } } } return(0); }
report( FILE fp, /* file pointer*/ int interest);This routine is responsible for reporting all I/O cards it has found. If interest is (0,1) then generate a (short, long) report. If a device support module is using a driver, it normally does not have to implement this routine because the driver generates the report.
init( int after);This routine is called twice at IOC initialization time. Any action is device specific. This routine is called twice: once before the database records are initialized and once after. after has the value (0,1) (before, after) record initialization.
init_record( void *precord); /* addr of record*/The record support init_record routine calls this routine.
get_ioint_info( int cmd, struct dbCommon *precord, IOSCANPVT *ppvt);This is called by the I/O interrupt scan task. If cmd is (0,1) then this routine is being called when the associated record is being (placed in, taken out of) an I/O scan list. See the chapter on scanning for details.
It should be noted that a previous type of I/O event scanning is still supported. It is not described in this document because, hopefully, it will go away in the near future. When it calls this routine the arguments have completely different meanings.
In general only device support routines know how to call device drivers. Since device support varies widely from device to device, the set of routines provided by a device driver is almost completely driver dependent. The only requirement is that routines report and init must be provided. Device support routines must, of course, know what routines are provided by a particular device driver.
File drvSup.h describes the format of a driver support entry table. File drvSup.ascii defines the supported device drivers.