Feedback Search Top Backward Forward
EDM/2

A Hello World Device Driver - Part 1/6

The Driver

Written by Alger Pike

 
Part1 Part2 Part3 Part4 Part5 Part6

Introduction

[NOTE: You can download the source code here. You will need the Watcom compiler, and the Watcom devHelp library, which you can grab from the author's site at http://avenger.mri.psu.edu/devhelp.zip ]

A very hot subject in OS/2 programming is the writing of a device driver. Although there are a many good books out on the subject, I always felt that what was lacking in this area was a hello world device driver. Every other form of programming, from simple C to advanced GUI programming, has a hello world application. I know from my own experience that if code like this had been around, it probably would have taken me much less time than it did to learn how to write a device driver. Here I will present the basic anatomy of an OS/2 device driver and provide code for a driver that just loads and does not do anything. This way a beginner can concentrate on what makes the driver work.

What is a Device Driver?

Exactly what is a device driver? Simply put, a device driver is a piece of code which is dedicated to controlling a particular device. It is the job of this code to handle I/O and interrupts for the device. Also, the device driver will manage any memory that the device may use for Direct Memory Access. The device can be anything from a video card to a data acquisition board. A driver is necessary because many of the previous operations must occur at ring level 0 for them to be successful under OS/2, and for code to run in ring 0, it must be in a device driver.

A device driver is really a special type of executable and because of this, the driver must have special startup-code. This code consists of a device header. The header for the hello world driver is shown in Figure 1.


_HEADER segment word public use16 'DATA'

        extrn Strategy_ : far

        dd 0FFFFFFFFh   ;; Link to next header
        dw 9980h        ;; Device attribute word
        dw Strategy_    ;; Strategy entry point
        dw 0            ;; IDC entry point
        db "Hello$  "   ;; Device driver name
        dq 0            ;; Reserved
        dd 00000011h    ;; Capabilities bit strip

_HEADER ends
Figure 1.) Format of the OS/2 device driver header

The header keeps all the vital information on the device driver: the link to the next driver, device attributes, the strategy entry point, and various other items. The items of most importance to the programmer are the strategy entry point and the device name. The name of the driver is important because with this name, the driver can be opened for use by an application. Once opened, the driver must know where to go to execute its code. The strategy entry point is used for this task. The strategy entry point is a pointer to the location of the code that the driver contains. Once initialized and running, all requests from applications find their way through the strategy section.

The strategy section of a device driver can be likened to the message loop of a Presentation Manager (PM) application. At various times the kernel makes requests to the driver, and depending on the value of the request, the driver will execute a section of its code. Let us take a look at a typical Strategy section, the one from the hello world device driver:


if (rp->Command <
      sizeof(StratDispatch)/sizeof(StratDispatch[0]))
    rp->Status = StratDispatch[rp->Command](rp);
  else
    rp->Status = RPDONE | RPERR_COMMAND;

  return;
Figure 2.) A common strategy section

StratDispatch is an array of pointers to functions. The function with the array element rp->Command is called with the parameter of rp. All requests are filtered through this routine. The request produces the proper action based on the value of rp->Command. A list of the defined actions can be found in strategy.c.

A device driver as I mentioned contains executable code, but it is a special type of executable. The operating system suspends execution of the driver until a request is made for one of the driver's services. The requests are made using a special API call, DosDevIOCtl. This function coordinates the passing and receiving of data to and from the device driver. It also tells the device driver which one of its user functions to execute. The user functions are placed in the IOCTL section of the strategy routine. The operating system sends the driver an IOCtl request number and a function number. This is very much like when a PM application receives a WM_COMMAND message. In the WM_COMMAND section the different user functions are resolved via a number using a switch statement. The driver uses a similar construct to make its different function calls. The functions are resolved inside the strategy IOCTL function with a number via a switch statement. In addition to user routines, there are also several special actions a driver must execute at various times. The proper code is dispatched when the request is made. The most important of these special functions is the INIT function.

The INIT Function

The request for the INIT function is made for you automatically, once the header has been loaded. While the driver executes its INIT code, it is in a special mode, INIT mode, entered at system boot time. It is actually at ring level 3 at this point in its execution. During this time, a request is made by the kernel for the driver to initialize itself. The RPINIT value, you can think of it as a WM_CREATE message, is sent to the driver's strategy section. This value when processed by the switch statement causes the driver to execute its initializing code. Two important jobs of the initialization routine are to save the value of the entry point for the device's DevHlp routines and to set the size of data and code segments. If the size of the segments remain zero, at the end of INIT the driver will unload. Figure 3 is this code fragment for the hello world device driver:


DevInit(rp->In.DevHlp);
DosPutMessage(1, WELCOME_LENGTH, WELCOME)
  // Signal that we've installed successfully
  // by setting the size of our code and
  // data segments.
  rp->Out.FinalCS = OffsetFinalCS;
  rp->Out.FinalDS = OffsetFinalDS;
Figure 3.) Init routine for hello.sys

The only time that the driver can get entry to the devhelp routines and set the size of the data and code segments is during INIT . Without this step the driver cannot execute its own code properly or may even be forced to unload itself. Besides doing the above critical operations, the INIT section serves several other purposes. During initialization, OS/2 allows some ring 0 operations from ring 3. Instructions like in and out can be executed without trouble. Using these calls, the driver can initialize the device that it runs. Also, a limited number of API calls that are not available to the driver at other times can be executed here. The INIT function is where one can output messages that will display at boot time. These messages are displayed using the DosPutMessage API call. Once initialized, the driver enters into what is known as kernel mode. In this mode, the kernel keeps track of and dispatches requests made by user applications.

Readying the Driver

After being loaded the driver remains dormant, waiting for applications, or in some cases other drivers, to make requests to it. Since this step must be done before an application can access functions, let us now examine how this is done. There is a special API call which is used for making a device driver ready for use by an application. This is the DosOpen call. To ready the driver, proceed as follows:


rc = DosOpen((PSZ) "Hello$   ",
             &driver_handle,
             &ActionTaken,
             0,
             0,
             FILE_OPEN,
             OPEN_SHARE_DENYNONE
               | OPEN_ACCESS_READWRITE,
             NULL);
Figure 4.) Code to open hello.sys for use by an application

As this call is made, there is another special request that OS/2 makes to the driver; the RPOPEN request. The request sends the RPOPEN value to the strategy section. Since RPOPEN has a different value than RPINIT, the switch statement knows to execute the code for the RPOPEN function. The code for the hello world device's open function is shown below:


return RPDONE;
Figure 5.) Minimum required code for the RPOPEN request

Note that the open function is required to return the RPDONE value. If it does not, the DosOpen call will return an error and not allow the application to open the driver. This can be useful for devices which can only handle input from one application at a time. If the device is currently in use, an error message can be sent back to the second application trying to use the driver. This might come in handy for a data acquisition program. Imagine if you tried to take different scans with the same data board at the same time.

Communicating with the Driver

Now that the driver is opened successfully, it waits for the application to make requests for its services. As I mentioned above, all the code for the user requests is kept in the IOCTL function inside a switch statement so that the calls can be resolved. Let us now make a request to our driver and see what happens. The request is made using the following API:


rc = DosDevIOCtl(driver_handle,
                 0x91,
                 0x01,
                 0,0,0,0,0,0);
Figure 6.) Requried code to pass the driver a user request

Notice the three parameters which take values. The first is the driver handle. This is the file handle for the driver which we obtained with the DosOpen call. The next two parameters tell the driver which category and function to execute within the driver. The parameters which are zero deal with passing function parameters to, and returning values from your driver. We will examine these in part three of this series. The call itself, in addition to passing parameters, gives the driver a request for the IOCTL function via the RPIOCTL value. This causes the IOCTL function to execute. Let look at the code for the IOCTL section:


RPIOCtl FAR* rp = (RPIOCtl FAR*)_rp;
  if(rp->Category != IOCTLCAT)
     return RPDONE;
  switch (rp->Function)
     {
     default:
     break;
     }
  return RPDONE;
Figure 7.) Minimal IOCtl section

Look at the if statement first. If the category is not supported by the driver then the code should exit. This step is necessary because sometimes the operating system will send the driver unsupported requests. Having the driver return normally when these requests occur can avoid errors. The next statements make up the switch for the user functions. As you can see, the hello world driver just returns normally no matter what the number of the function.

Closing the Driver

The final step in using the device driver is to close the driver. Once again there is a standard API call that does this for you. You make the call like this:


DosClose(driver_handle);
Figure 8.) Closing the driver

As when the driver is opened, OS/2 makes a special request to the driver; this time the RPCLOSE request. Here the RPCLOSE value is sent to the driver. The code for the hello world device's close function is shown below:


return RPDONE;
Figure 9.) Returning from the close section

Once again all this driver does is to return the RPDONE value. This is to let the application know that the driver has been closed without any problems. For a single use driver you would want to undo your variables which tell the driver that it is in use. Once the device is closed it is free for use by another application.

Summary

In this section we have looked at a very simple OS/2 device driver. It is a driver which has all the basic requirements of a driver, but does not do anything useful. The driver has all the functionality it will need to handle user requests. In the next part of this series, we learn how to add those requests to the driver and actually make it do something useful.