Writing OS/2 Device Drivers with WATCOM C

From EDM2
Jump to: navigation, search

by Joseph Nord and Robert Rose

A common source of frustration among writers of OS/2 device drivers is the continued existence of the 16-bit assembler-based programming model. Device driver writers can't remove this frustration entirely, but they have been able to reduce it by writing in 16-bit C. We'd like to show you how the WATCOM compiler can make your job easier.

The Developer Connection Device Driver Kit for OS/2 Version 2 includes a physical device driver-virtual device driver (PDD-VDD) pair that controls the OPTi 82C929 MAD16 audio controller The drivers were developed using the WATCOM version 10.0 16- and 32-bit C compilers We installed the B level patches that are available via anonymous file transfer protocol (FTP) from WATCOM's server (ftp.watcom.on.ca).

Past Wrongs

The first thing you'll notice when compiling with WATCOM is warnings. The WATCOM compiler is strict in its interpretation of the ANSI C specification. After going through the trouble of correcting the errors, most developers find it well worth the effort. The biggest difference when compiling with WATCOM is in type checking; other compilers only flag a type error when two different types resolve to a different base type (as opposed to ANSI specification, where different intermediate types are enough to cause an error). For example:

Include file 'A.H':  
        typedef unsigned short USHORT;
        typedef USHORT SEL;
Include file 'B.H':  
        typedef unsigned short SEL;

Sample Code 1.

If the same C file includes these two headers, WATCOM generates an error because the two definitions of SEL pass through different intermediate types.

Another difference when using WATCOM is in the device header. Most existing C-based drivers have the strategy and IDC entry points defined as type USHORT. This, however, is not really accurate and WATCOM correctly flags the code as an error. The correct declaration defines the IDC and strategy entry points as pointers to functions.

typedef struct {
   LONG   next;                 // next driver in chain
   USHORT attrib;               // attribute
   void   (*strategy_entry)();  // offset of strategy routine
   void   (*idc_entry)();       // offset of IDC routine
   UCHAR  name8;                // device driver name
   ULONG  Reserved2;            // reserved (8 bytes)
   ULONG  capabilities;         // capabilities strip
} DEVHDR;

Sample Code 2.

Minimizing MASM

In an ideal world you would be able to write device drivers entirely in C. With WATCOM, we can't quite get there, but we can get pretty darn close. How? The WATCOM compiler provides a very powerful tool, the #pragma aux directive. This magical little #pragma lets you specify the calling convention for individual routines at a very detailed register level. In fact, it s flexible enough that you can essentially call anything from anywhere in WATCOM C code.

For example, when the kernel calls a device driver, it places the address of the request packet into the ES:BX register pair. Traditional C device drivers have always had a small assembly stub that pushes the ES and BX registers and then calls the C strategy handler that takes the pointer as a parameter off the stack. Using the WATCOM "aux" pragma, you can specify that the strategy routine parameters arrive in registers rather than on the stack. You don't need to worry about ES, BX, or registers at all; the compiler hides it from you completely.

Here is the strategy routine:

#pragma aux Strategy parm es bx;        // WATCOM pragma
void far Strategy (REQPACKET far *rpP)
{
  if (rpP->Command > (UCHAR) MaxStrategyFuncs)
    rpP->Status = RPDONE | RPERR | RPUNKCMND;
    else
  StrategyFuncsrpP->Command(rpP);
}

Sample Code 3.

Compiler Flags

Choosing compiler options is always time-consuming when you first use a new compiler. When building the audio driver, the following flags were used:

CFLAGS=-mc -3 -bt=os2 -d1 -oirs -s -wx -zl -zfp -zgp -zq -zu $(DEBUG)

where:

-bt=os2    = Build target OS is OS/2
-mc        = Memory model compact (one code, many data)
-3         = Enable use of 80386 instructions   Optimize for 386 
-d1        = Include line number info in object (necessary
             to produce assembler listing)
-o         = Optimization
             i = enable inline intrinsic functions
             r = optimize for 80486 and Pentium pipes
             s = space is preferred to time
-s         = Omit stack size checking from start of each function
-zl        = Place no library references into objects
-wx        = Warning level set to maximum
-zfp       = Prevent use of FS selector
-zgp       = Prevent use of GS selector
-zq        = Operate quietly
-zu        = Do not assume that SS contains segment of DGROUP

Linking

The linker determines segment order based on the order in which the segments are encountered (that is, first come, first served). The assembler source file shown in Sample Code 4 defines all segments used by the device driver It is fed to the linker as the first .OBJ, thus defining segment order for the entire device driver

; SEGS ASM - Define order of device driver segments
_DATA   SEGMENT WORD PUBLIC USE16  DATA      ; WATCOM data
_DATA   ENDS
_BSS    SEGMENT WORD PUBLIC USE16  BSS       ; Uninitialized data
_BSS    ENDS
CONST   SEGMENT WORD PUBLIC USE16  DATA 
CONST   ENDS
CONST2  SEGMENT WORD PUBLIC USE16  DATA 
CONST2  ENDS
ENDDATA SEGMENT WORD PUBLIC USE16  ENDDATA   ; end asm
ENDDATA ENDS

_TEXT   SEGMENT WORD PUBLIC USE16  CODE      ; WATCOM code
_TEXT   ENDS
CODE    SEGMENT WORD PUBLIC USE16  CODE      ; DHCALLS lib
CODE    ENDS

; Tell linker to treat each of these individual
; segments as part of a larger whole (the group)
DGROUP  GROUP   _DATA, _BSS, CONST, CONST2, ENDDATA
CGROUP  GROUP   _TEXT, CODE
END

Sample Code 4. Source of SEGS asm in the MAD16 device driver

To support discarding initialization data, care must be taken to ensure that all data needed after initialization is placed before the label that defines the start of initialization data. The compiler places all uninitialized data into the BSS segment and all BSS data comes after all variables in the _DATA segment.

The marker for the start of initialization data must be somewhere in the _DATA segment. This dictates that all data in the BSS segment is discarded after initialization In the MAD16 driver, we expressly initialized all data segment variables (often to zero), forcing them into the _DATA segment where they can be retained after initialization.

Using the 16-bit toolkit linker, LINK.EXE, the following options were used:

LFLAGS=/BATCH /NOE /MAP /NOD

SEGS obj HEADER obj DATA obj STRATEGY obj UTILA obj IDC_PDD obj +
   IDC_VDD obj MADUTIL obj WSSUTIL obj UTIL obj TRACE obj +
   INIT obj MAD16 obj WSS obj SNDBLAST obj DDSTRING obj DDPRINTF obj +
   END obj
   mad16 add
   mad16 map
   DOSCALLS+OS2286+DHCALLS LIB
   mad16 def;

Sample Code 5.

Conclusion

As you have seen, there is no magic in using the WATCOM compiler for OS/2 physical device driver development. Your MAKEFILE and assembler code will change, but in most cases the changes provide a positive effect to the resulting code. In a future issue of The Developer Connection News, we will demonstrate how to write OS/2 virtual device drivers with the WATCOM 32-bit compiler and show the advantages of using the single compiler set for both PDD and VDD development.

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation