How To Write Device Support that uses Asyn Driver

From EPICSWIKI
Revision as of 23:10, 19 April 2005 by AndrewJohnson (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Introduction

Writing software that calls Asyn is confusing until you understand the approach it expects you to use. There are very good reasons behind the design of Asyn which is intended to simplify the work involved in writing robust, portable code, but this does result in the need to learn the Asyn approach. Once you understand the choices that Asyn has made though, writing the software is actually simpler than it would be without asyn.

This document assumes that you're writing an EPICS Device Support layer for a moderately complicated instrument or device that interfaces through an RS232 or similar serial port, and hence you need to use the asynOctet generic serial interface. If you're wanting to use some other Asyn interface then the techniques discussed here should still apply, but some of the specific code will need to change. If the device you're controlling is simple you may be better off using the devGpib interface layer (which also works with serial devices now that it's been converted to use Asyn). If it's available the Streams serial device layer from Dirk Zimoch may be even better, as it will be able to send and parse many typical serial message formats without your having to write any C code at all.

Getting Started

First, you will need to write the code for a basic device support layer. I won't cover the details of that here but will assume that you are familiar with the device support interface. In fact I'm not really going to talk about device support very must at all, other than to explain how the design of Asyn intrinsically allows you to implement asynchronous device support. It is also possible to use Asyn to implement synchrous device support, but I'm not going to talk about that here at all.

Assuming you've got your basic device support framework sorted out, there are usually only a couple of places where your code will actually need to interact with the Asyn software at all: 1) In the per-record device initialization code, and 2) when actually performing the I/O. The first part is required to support the latter, so I'll talk about the two together.

Our code needs to pull in some headers first:

#include <asynDriver.h>
#include <asynOctet.h>

The first header is needed for all code that is going to call Asyn; the second only if you're planning on using the serial handling, which we are for the purposes of this document.

What is an asynUser?

An asynUser is a structure that Asyn uses to hold information about an individual I/O transaction. Initially you will probably create an asynUser for each record, since at least in theory the user could create a database that processes all your records at once. For more complex I/O schemes where you share a single I/O transaction amongst multiple records you may reduce the number of asynUser objects you create, but in any case you will need one of these for each independent transaction that could concievably be active at once, hence typically one per record in the simple case.

Since you can't really do anything much in Asyn without an asynUser, lets start by creating one:

asynUser *pau = pasynManager->createAsynUser(myCallback, myTimeout);

The first thing to notice is that we're not calling a regular function, we're actually making a call through a function pointer found in the global pasynManager structure. Why? Well apparently it simplifies matters for the authors of Asyn, or so they tell me, so that's the way we have to do it.

Next note that we're passing in two parameters to the call, myCallback and myTimeout. These are both names of functions that we have to provide and which will be called by Asyn at an appropriate time - we'll come back to them again later.

It's worth knowing that you really can't create, copy or destroy an asynUser object yourself, because it's actually implemented as a part of a larger structure which the asynManager routines use to hold other information related to your connection. You must use the methods provided by asynManager.

Finally don't bother checking the pau value returned by the above method for a NULL value. If createAsynUser() wasn't able to allocate enough memory for the structure it won't bother to return at all; if there's not enough spare RAM at this stage, our IOC is never going to be able to run properly (that's the theory anyway, don't blame me I didn't write it...).

asynUser Internals

So we created an asynUser, what does it look like and what can we actually do with it?

The asynDriver Documentation describes the fields inside the asynDriver structure. Some of these fields we are allowed or even expected to set, whereas others should not be touched. The fields we'll be using are:

typedef struct asynUser {  /* These fields are in the wrong order */
    void *userPvt;         /* Ours to play with */
    void *userData;        /* Ditto */
    double timeout;        /* I/O operation time limit, in seconds */
    char *errorMessage;    /* Explanation for any Asyn errors */
    other fields ...
} asynUser;

I.e. we get two void * pointers for our code to use, and and a timeout setting which is how we we get to control how long the lower level driver waits for responses before giving up. This isn't the only timeout we get to set, it's just the one used by the code that implements the I/O operations. The errorMessage string is useful to display and/or log in the event that any of the routines we call gives us back an error status.

Many of the pasynManager routines take an asynUser * parameter as their first argument, and that's how we will set up a connection to the devices we want to talk to, register exception callbacks and queue or cancel requests to perform I/O.

Connecting

If we want to talk to a real world device that already has an asyn Port registered, we have to connect to it by name:

asynStatus status = pasynManager->connectDevice(pau, portName, 0);
if (status != asynSuccess) {
    printf("Can't connect to port %s : %s\n", portName, pau->errorMessage);
    pasynManager->freeAsynUser(pau);
    return -1;
}

...


...

Spread around the source file and in the initialization routine, we'll have bits of code like these:

typedef struct myPvt {
    asynUser *pau;
    dbCommon *prec;
} myPvt;
asynUser *pau;
myPvt *dpvt = (myPvt *) calloc(1, sizeof(myPvt));
if (!dpvt) goto err;
dpvt->prec = precord;
pau = pasynManager->createAsynUser(myCallback, myTimeout);
pau->userPvt = dpvt;
dpvt->pau = pau;



This document is not yet complete...

--AndrewJohnson 18:10, 19 Apr 2005 (CDT)