OS/2 Device Drivers for Dummies - The Beginning

Written by Mike Greene

1. Introduction
Let me first state that I fully intend to plagiarize as much as possible, deal with it. There seems to be only a few options for the hobbyist programmer to easily learn how to build a device driver for eCS-OS/2. Some good examples that exist are Alger Pike's EDM/2 articles from years ago, so they are somewhat dated. Another example is contained in the Open Watcom directory [samples\os2\pdd]. Of course there are various driver examples available from many additional sources, just use Google. This text deals with 16 bit eCS-OS/2 drivers.

I am using several printed and electronic text references which can be found both online or through book sites such as Amazon.com:
 * 1) OS/2 Physical Device Driver Reference (link)
 * 2) Design of OS/2 by Harvey M. Deitel and Michael S. Kogan (note 1)
 * 3) Writing OS/2 2.1 Device Drivers in C by Steven J. Mastrianni (note 2)
 * 4) Device driver information contained on the second eCS CD
 * 5) Writing OS/2 Device Drivers by Raymond Westwater

Reference 3 and 5 are very good, but only deal with 16 bit drivers. However, there is rumoured to be an unpublished version of reference 3 for OS/2 Warp (1997) floating around the net somewhere. I will be taking parts of all the above and with luck provide something that can be used to learn what I did much quicker for a beginner.

You need some knowledge of C and enough ASM know-how to understand some basic source. For ASM I know how to do a HelloWorld executable but that is the extent. Also, the current version of Open Watcom needs to be installed and working. Later you might want to read up on privilege levels and a good explanation exists on EDM/2 by Holger Veit (what happened to him?).

I have modified Pike's Hello World example so it can be compiled with Open Watcom and does not require the OS/2 DDK [here]. It outlines the basic parts of a device driver, but really does nothing. During system boot it displays a message which indicates it has loaded and executed initialization. I have provided a test.exe which when run opens and closes the driver. The driver will beep on open and close, not very exciting but it does provide feedback that things are working correctly.

What is the most important thing to remember? Nothing comes fast and easy and the following information is from my continued learning about device drivers. I started with no knowledge and am still very limited. The source code for the driver and test program are HERE. Enjoy...

Note 1: I believe there are PDF versions of this book floating around, I have a hard cover copy. However, I did email Dr. Kogan asking if he would release it to the masses. He reply that he had wanted to do this sometime ago, but his co-author would not agree to it.

Note 2: I know there is an unpublished 3rd revision PDF floating around which covers Warp (approx 1997).

2. 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 device dependent module that provides the low-level I/O and interrupt support for a 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 eCS-OS/2, and for code to run in ring 0, it must be in a device driver. So, device drivers are trusted modules that have access to the kernel. For the hobbyist programmer this means you can really crash the system now!

Device drivers come in two flavours, block and character. Block devices are used for mass storage devices and character devices handle data one character at a time. Is this true? I have read that it is so, most of the time. However, in Gordon Letwin's Inside OS/2 he says the following:

''OS/2 device drivers are divided into two general categories: those for character mode devices and those for block mode devices. This terminology is traditional, but don't take it too literally because character mode operations can be done to block mode devices. The actual distinction is that character mode device drivers do I/O synchronously; that is, they do operations in first in, first out order. Block mode device drivers can be asynchronous; they can perform I/O requests in an order different from the one I which they received them.''

Device drivers operate in three different modes: INIT, Kernel (or task), and Interrupt. INIT mode is a special mode executed during at system boot with RING 3 privileges, however, it is allowed some RING 0 privileges (see OS/2 PDD reference). The Kernel mode is in effect when the device driver is called by the kernel in response to an I/O request. The Interrupt mode is in effect when the device driver's interrupt handler is called in response to an external interrupt. In this example I will only be concerned with INIT and Kernel modes.

I intend only to deal with 16-bit driver and not some of the 32-bit additions IBM added.

So, where does this all start? During system boot time the kernel finds a DEVICE= statement in the CONFIG.SYS file and then loads the device driver. Next, it reads the device driver header which is where I start this adventure.

3. The Header
The device driver consists of at least one code segment and one data segment, although more memory can be allocated if required. Additionally, the device driver is an EXE type program which is linked as a DLL. The header contains information used by the kernel during initialization. The data segment, which contains the Device Header, must appear as the very first data item. No data items or code can be placed before the Device Header. An OS/2 device driver which does not adhere to this rule will not load. While I do not intend to get ASM deep, the [devsegs.asm] source and #pragma data_seg ( "_HEADER", "DATA" ) in header.c keeps the segments in order.

The initialization thread opens the driver module and reads the first segment into low memory (below 1M). This segment will be the main data segment. The second segment is loaded into low memory and will be the main code segment. Any additional segments are read into high memory (above 1M).



The device header is defined in [devhdr.h] typedef struct DEVHEADER DEVHEADER; struct DEVHEADER { struct DEVHEADER FAR *next;     // next driver in chain uint16_t        DAWFlags;       // device attribute word NPVOID          StrategyEntry;  // offset to strategy routine NPVOID          IDCEntry;       // offset to IDC routine uint8_t         Name[8];        // driver name uint16_t        DAWProtCS;      // * Protect-mode CS of strategy entry pt   uint16_t         DAWProtDS;      // * Protect-mode DS   uint16_t         DAWRealCS;      // * Real-mode CS of strategy entry pt   uint16_t         DAWRealDS;      // * Real-mode DS   uint32_t         Capabilities;   // Capabilities bit strip }; This Hello World header is defined in [header.c] with the following values: DEVHEADER DDHeader = { -1L,                                    // Link to next header in chain DAW_CHARACTER|DAW_OPENCLOSE|DAW_LEVEL1, // device attribute word Strategy,                               // Entry point to strategy routine 0,                                      // Entry point to IDC routine {"Hello$ "},                            // Device driver name 0,0,0,0,                                // Reserved CAP_NULL                                // Capabilities bit strip (for level 3 DDs) }; The "next driver in chain" link is set to -1L to mark the end of DEVHEADER chain (see [devhdr.h]) because the example device driver contains only a single device. If a second device were to be defined in this driver then the field would point to it. Next, the Device Attribute Word which is used to define the operational characteristics of the device driver. This is set to DAW_CHARACTER | DAW_OPENCLOSE | DAW_LEVEL1 which are listed and explained in [devhdr.h]. The strategy routine entry point is next and is explained in section 4 of this article. The IDC entry point offset follows and is used if the device driver supports inter-device driver communications. The Hello World driver does not support IDC so this is set to 0. The Device driver name is next and must be 8 characters in length, notice how the Hello$ name is padded with spaces. The final field is the Capabilities Bit Strip word defines additional features on level 3 drivers (OS/2 v2.0 (support of memory above 16MB). See [devhdr.h] for Capabilities Bit Strip options, however, the Hello World driver does not utilize level 3 options.

A more detailed description of the header is located here.

4. The Strategy Section
When I started, the title "Strategy" kind of scared me. Like most code, the name tends to scare a hobbyist programmer until one sees what it really is! The Strategy section is just a large switch statement and from my point of view the heart of the device driver. Remember, I am not covering interrupt drivers. The device driver receives a request from the kernel on behalf of the calling application which are passed to the Strategy. Also, the Strategy is called at initialization with RP_INIT which will execute the INIT routine. In the Hello World example the [StratInit] function is called.

5. The INIT Mode
I would like to summarize what has been presented up to now. The kernel found a DEVICE statement during system boot, it loaded and then looked for the device header. It examines the header, finding the Strategy entry point ([strategy.c]) and the device name (Hello$). The kernel now calls the Strategy provided in the header with RP_INIT. To be more detailed and in Hello World context, it passes a request packet REQP_INIT (see [devreqp.h]) to the strategy entry point: On entry to [Strategy] REQP_INIT rp->command will be set to RP_INIT which will call [StratInit] to perform initialization. Again, it is important to remember during INIT the driver is actually at ring level 3 with some access ring level 0 functions. The following tables lists the API calls available at INIT: The initialization routine performs two important jobs. First, to save the value of the entry point for the device's [DevHlp] routines (REQP_INIT rp->in.devhlp). Second, and to set the end of data and code segments (REQP_INIT rp->out.finalcs and REQP_INIT rp->out.finalds). The data and code segments after these points will be discarded after all device driver headers in the driver have been initialized. If the data length is set to zero then the driver will be unloaded.

I found the best simple explanation of the [DevHlp] entry point on USENET by Holger Veit:

"The kernel itself IS the device helper., i.e. when registering a device driver you get a 16:16 pointer to a kernel routine that is named [DevHlp] and all so called device helper functions call this entry point indirectly (after setting the appropriate registers)."

The [DevHelp] entry point should be declared as: PFN Device_Help  = NULL; Although Resource Manager functions are not used in this Hello World example, you should plan for the future using Resource Management services. The PDD reference states: The PFN Device_Help variable must be initialized by your driver prior to calling any Resource Manager services. It is expected to contain the Device Help entry point provided in the OS/2 Init Request Packet your driver receives.

If the size of the segments remain zero, at the end of INIT the driver will unload.

6. The Kernel Mode
Need to add - MKG

7. Compiling and Testing the Driver
Compiling is easy. Ensure that Open Watcom is installed and working correctly. Unzip the hellowdevice.zip archive and then wmake. The result will be hello.sys and test.exe.

Here is the part where it all comes together! Place a statement in the config.sys (example: DEVICE=C:\hello.sys) and reboot. During boot you should see: Hello World Driver Installed. (C) ACP Soft 1996. M Greene  2007. All Rights Reserved. So, what just happened? The kernel found our DEVICE statement, loaded the driver, read the header, and then sent a RP_INIT to the Strategy Section. The Strategy Section received the RP_INIT and called the [StratInit] function. The [StratInit] function displayed the above message and returned. If the system did not hang or trap then hello.sys is loaded and ready.

As I stated, the driver does nothing spectacular. The [test.exe] executable interfaces with hello.sys to make some noise. When [test.exe] is run the following will be displayed with a couple beeps: About to beep.... DosOpen return 0 Sleep for 5 seconds.... About to beep.... DosClose return 0 Ok, here is what just happened:
 * [test.exe] prints "About to beep" and issues a DosOpen to Hello$
 * The Strategy Section receives a RP_OPEN and executes the [StratOpen] function
 * The [StratOpen] function issues a DosBeep and RPDONE then returns
 * Now back in [test.exe] DosOpen return is printed with the return code
 * [test.exe] prints Sleep for 5 seconds and sleeps for 5 seconds
 * Next [test.exe] issues a DosClose to Hello$
 * The Strategy Section receives a RP_CLOSE and executes the [StratClose] function
 * The [StratClose] function issues a DosBeep and RPDONE then returns
 * Back in [test.exe] DosClose return is printed with the return code
 * [test.exe] exits

8. Summary
Exciting, right??? Well maybe not, but it is a good and simple drive driver example.


 * Original Source: OS/2 Device Drivers for Dummies by Mike Greene (May 2007)