Writing OS/2 Device Drivers with WATCOM C
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