From Hello World to Real World - Part 4/6

From EDM2
Jump to: navigation, search

Written by Alger Pike

Part 1

Part 2

Part 3

Part 4

Part 5

Part 6

Writing a Device Driver for your own Data Acquisition Card

Introduction

In my first series of articles, we examined the functionality of an OS/2 device driver. We examined the driver hello.sys, and used it to look at how an OS/2 device driver works. We extended the basic driver by adding an IOCTL call which would beep at a frequency corresponding to the data we sent it. We are now at the point, where we can start writing our own driver for our own device. I will go through how to write a device driver for a data acquisition card. There are a few issues involved with writing a Data Acquisition (DA) Device driver which are important: 1) Allowing only one application to use the device, 2) Speed of the input/output (I/O) and, 3) maximizing performance where possible by using interrupts.

For a Data Acquisition card, it is important to limit the number of applications that can open the driver to one. In a DOS world, this is easy to do; you run your application and use the device. In DOS, other processes which might want to use the device cannot be run at the same time. As we all know, in an OS/2 world the situation is quite different; many applications can be running at the same time and make requests for the same resources. Imagine running two instances of your DA application both trying to take a scan; disaster could result. By the same token, you may want to take data with one instance of the program, and do data analysis with another. To meet both of these goals, other applications must be denied access to the device, when it is in use. The device driver can easily be made to handle this. In this way other applications can run but not use the device's services. This will be the subject of part I.

When our driver is in use by the application, we want the requests to be made fast. By design, Data Acquisition cards are not complex devices; they are straightforward to program. Many times, a single in or out instruction is all that is required to complete a useful operation, a single line of code. You might then ask , Why can't I write an input output privilege level (IOPL) 16-bit dll, or use TESTCFG$ and do the same thing? Well, you can but these two methods are slow and not well suited for real-time high frequency data collection. When set up properly, I/O is much faster from a ring 0 16-bit segment than from a ring 2 IOPL 16-bit segment. And like IOPL, TESTCFG$, although ring 0 code, goes through many thunking and parameter checking routines; this is very slow. Part II will detail a way to directly call the in/out instruction without incurring any of this overhead. You might say the difference does not matter, but I put two versions of the same program to the test. Both programs performed one million I/O instructions to the same I/O port. One version used TESTCFG$ and one AD3110$. My driver, AD3110$, had TESTCFG$ beat hands down.

A common use for a data acquisition card is to use it as a timer. In most cases an out instruction can be used to program the timer and start it counting. There are several methods to determine when the timer has reached its terminal count. One method which is not very efficient is polling. With polling, an in instruction is used to monitor the count done bit. If this bit is not one, read in the bit again. This method wastes processor resources because it is busy reading in from the port while it could be doing other things. With the interrupt method, the D/A card performs the count entirely on its own. When it is finished, it generates an interrupt. This can be used to signal to the application that it is time to continue with the processing. Using interrupts frees up the processor to run other threads while we are waiting for the counter to finish. It is not trivial to set up an interrupt routine, but the resulting performance enhancement is well worth the effort. Part III will detail how to add an interrupt routine to your driver that will signal back to the application when the interrupt occurs.

Part I: 1 Device, 1 Application

There are two places in the device driver code that can used to limit device availability. The first is in the init section, which runs during the device driver loading. The second is in the open function, called when we issue a DosOpen to Open the driver. These two sections are used to make sure that 1) the D/A card is installed in your system, 2) the card is in an acceptable state to begin working, and 3) the card can be used only by one application at any time.

In the init section, you should first find out if your board is installed in the computer. Most libraries for D/A cards have such a function; you can copy the code from the library directly into the init section of your driver. For the board I am using, I read in from its control register. If the result is not 0xFFFF, then I assume that the board is present. Keep in mind that this method does not discriminate between the data card and other boards that could be at this address. It also assumes the device in the power up state will have at least one bit low at the designated I/O port; this is true for my card. If the board is not installed, the driver does not load and no application can use it. The code for a typical init section might look like this:

WORD16 StratInit(RP FAR* _rp)
{
  RPInit FAR* rp = (RPInit FAR*)_rp;
  int result=0;
  WORD16 result16;

  //MANDATORY: Initialize the Device Driver libraries.
  DevInit(rp->In.DevHlp);
  Device_Help=Dev_Entry;

  //Read from the Control Register to See if Board Exists
  result = inpw(0x280 + 4);

  //If TRUE no board is installed
  if (result == 0xFFFF)
  {

    //Signal that we will not install without a  board
    rp->Out.FinalCS = 0;
    rp->Out.FinalDS = 0;

    //Tell User no board is installed
    DosPutMessage(1, NOBOARD_LENGTH, NOBOARD);
    return RPDONE;
  }

  //A board is present Reset it
  inp(0x280 + 14);

  //We need to delay about six ms for board to reset
  //Beep at inaudible frequecy
  DosBeep(10, 6);

  //Clear Read that happens during Reset
  outp(0x280 + 10, 0);

  //Allocate Context Hook for Interrupt processing
  Hook1 = DevCtxAllocate((VOID FAR*) ctx_hand);
  if (!Hook1)
  {

    //Signal that we will not install without a  hook
    rp->Out.FinalCS = 0;
    rp->Out.FinalDS = 0;

    //Tell User no hook is availible
    DosPutMessage(1, IRQERROR_LENGTH, IRQERROR);
    return RPDONE;
  }

  //Signal that we've installed successfully by setting the size of
  //our code and data segments.
  rp->Out.FinalCS = OffsetFinalCS;
  rp->Out.FinalDS = OffsetFinalDS;

  //Print a sign on message to the console.
  DosPutMessage(1, WELCOME_LENGTH, WELCOME);
  return RPDONE;
}

Figure 1. Init section code: This code detects the D/A card, if found it rests the board.

If you are using the Hello.sys package, the init section is in init.c; you can make the changes required for your board there.

After I determine whether or not the board exists, I then send the board a command which resets the board. It so happens that with my board the reset process causes a D/A conversion to be made. The result of the conversion is stored in its buffer. The remaining code clears this value from the buffer. These steps are not mandatory in a driver but are good practice. This way you know the state of the board when you begin to use it. The final lines of code allocate the context hook; this will be explained in part III.

After the init has run and the driver has loaded, we will not make another request to the driver until we try to use it. This occurs when we open the driver using the DosOpen API call. When we do this, the operating system is going to send the diver an RPOPEN request. In the open request, we first check to see if the driver is already in use. If so, we do not allow the second application to open the driver, by sending back an error message. If not, we proceed, allowing the requester to open the driver. Again, if you are following along with the hello.sys package, you can implement your own open request code in open.c. A typical open request might look something like this:

WORD16 StratOpen(RP FAR* _rp)
{
  RPIOCtl FAR* rp = (RPIOCtl FAR*)_rp;

  //Is driver open
  if(opencount ==0)
  {

    //Driver is not in use; tell others it is now
    opencount = 1;
  }
  else
  { 
    //Device in use; Deny access!
    return (RPDONE | RPERR);
  }

  //Reset Board
  inp(0x280 + 14);

  //We need to delay about six ms for board to reset
  //Beep at inaudible frequency
  DosBeep(10, 6);

  //Clear Read that happens during Reset
  outp(0x280 + 10, 0);

  //Get ready to do I/O to the ports
  acquire_gdt();
  return RPDONE;
}

Figure 2. Open Section: This code determines if the device is in use; if so, others are denied access.

Also in the open section, I reset the board again. You can never reset your board too many times [It seems as if this tautology might have exceptions. Ed]. After the board is reset, I make the call to initialize the global descriptor table (gdt) selector used to make the I/O calls. More on that later in part II.

The complementary code for the open section is put into the close section (close.c). This section of code is called when the application closes the driver with the DosClose API call. In this section, I reset the open count, telling the driver another application may open it. Also, I reset the board again, primarily to prevent damage to the board or my equipment that could be caused by leaving the board in an unknown state. The code is as follows for the close section:

WORD16 StratClose(RP FAR* _rp)
{
  //Reset to allow another application to open driver now that we are done
  opencount = 0;

  //Reset Board
  inp(0x280 + 14);

  //We need to delay about six ms for board to reset

  //Beep at inaudible frequency
  DosBeep(10, 6);

  //Clear Read that happens during Reset
  outp(0x280 + 10, 0);
  return RPDONE;
}

Figure 3, Close Section: The application is done using the device so its frees the resource for use by another application.

Conclusion

In this part, I have shown you some of the basic requirements of a D/A device driver, and how to implement them in an OS/2 device driver. Of course, the code for this driver will probably not work on your device, but you get the feel of what kinds of things you should be including in your init, open and close sections. In the next section, we will examine some of the possible ways we can make I/O requests to our device. I hope to show why I think calling I/O code via a call gate is the fastest option.