OS/2 Device Drivers for Dummies - The Beginning

From EDM2
Jump to: navigation, search

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 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 rumored 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 coauthor 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 flavors, 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 INIIT 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).

device segments

The device header is defined in devhdr.h

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:

	-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( ) funtion 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:

typedef struct _REQP_HEADER {
	uint8_t            length;        // Length of request packet
	uint8_t            unit;          // Unit code (B)
	uint8_t            command;       // Command code
	uint16_t           status;        // Status code
	uint32_t           res1;          // Flags
	struct REQP_HEADER FAR *next;     // Link to next request packet in queue

typedef struct {
	REQP_HEADER header;
	        uint8_t   res;            // Unused
	        uint32_t  devhlp;         // Address of Dev Help entry point
	        int8_t    *parms;         // Command-line arguments   PCHAR
	        uint8_t   drive;          // Drive number of first unit
	    } in;
	    struct  {
	        uint8_t   units;          // Number of supported units
	        uint16_t  finalcs;        // Offset to end of code
	        uint16_t  finalds;        // Offset of end of data
	        void      *bpb;           // BIOS parameter block   PVOID
	    } out;

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:

DosBeep Generate sound from speaker
DosCaseMap Perform case mapping
DosChgFilePtr Change (move) file read/write pointer
DosClose Close file handle
DosDelete Delete file
DosDevConfig Get device configuration
DosDevIOCtl I/O control for devices
DosFindClose Close find handle
DosFindFirst Find first matching file
DosFindNext Find next matching file
DosGetEnv Get address of process environment string
DosGetInfoSeg Get address of system variables segment
DosGetMessage Get system message with variable text
DosOpen Open file
DosPutMessage Output message text to indicated handle
DosQCurDir Query current directory
DosQCurDisk Query current disk
DosQFileInfo Query file information
DosQFileMode Query file mode
DosRead Read from file
DosSMRegisterDD Register session switch notification
DosWrite Synchronous write to file

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 <greenemk@cox.net> 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.