07/16/07 |
|
|
BooksExcerpt from Writing OS/2 Device Drivers in C
This excerpt © 1995-1996 Van Nostrand Reinhold and John Wiley, Publishers. All Rights Reserved. This text may not be copied or sold without the express written consent of the publisher. The Anatomy of an OS/2 Device DriverOS/2 device drivers, like other multitasking device drivers, shield the application code that performs I/O from device-specific hardware requirements. The application program need not concern itself with the physical constraints of a particular I/O device, such as timing or I/O port addressing, as these are handled entirely by the device driver. If an I/O card address is moved or a different interrupt selected, the device driver can be recompiled (notice I did not say reassembled) without modifying or recompiling the application code. It should be noted that OS/2 device drivers can be configured during boot-up operation by placing adapter-specific parameters in the DEVICE= entry in CONFIG.SYS. The driver can retrieve the parameters and configure itself during the INIT section. Conceptually, OS/2 device drivers are similar to device drivers in other multitasking systems, but they have the added responsibility of handling processor-specific anomalies such as the segmented architecture and operating modes of the Intel processors. Application-to-Driver InterfaceOS/2 device drivers are called by the kernel on behalf of the application needing I/O service. The application program makes an I/O request call to the kernel, specifying the type of operation needed. The kernel verifies the request, translates the request into a valid device driver Request Packet and calls the device driver for service. The device driver handles all of the hardware details, such as register setup, interrupt handling, and error checking. When the request is complete, the device driver massages the data into a format recognizable by the application. It sends the data or status to the application and notifies the kernel that the request is complete. If the request cannot be handled immediately, the device driver may either block the requesting thread or return a 'request not done' to the kernel. Either method causes the device driver to relinquish the CPU, allowing other threads to run. If an error is detected, the device driver returns this information to the kernel with a 'request complete' status. The OS/2 device driver may also "queue up" requests to be handled later in a work queue. The OS/2 Device Helper (DevHlp) library contains several DevHlps for manipulating the device driver's work queue. DOS Device Drivers and OS/2 Device DriversDOS device drivers have no direct OS/2 counterpart. DOS device drivers are simple, single-task, polling device drivers. Even interrupt device drivers under DOS poll until interrupt processing is complete. DOS device drivers support only one request at a time, and simultaneous multiple requests from DOS will cause the system to crash. While the DOS device driver is a single-threaded polled routine, the OS/2 device driver must handle overlapping requests from different processes and threads. Because of this, the OS/2 device driver must be reentrant. The OS/2 device driver must also handle interrupts from the device and optionally from a timer handler. It must handle these operations in an efficient manner, allowing other threads to gain access to the CPU. Most importantly, it must do all of these reliably. The OS/2 device driver, because it operates at Ring 0, is the only program that has direct access to critical system functions, such as the interrupt system and system timer. The device driver, therefore, must be absolutely bug- free, as any error in the device driver will cause a fatal system crash. OS/2 2.1 device drivers no longer have to deal with the real-protect mode switching of OS/2 1.x, as all programs run in protect mode. OS/2 device drivers must have the capability to deinstall when requested, releasing any memory used by the device driver to the OS/2 kernel. OS/2 device drivers may also support device monitors, programs that wish to monitor data as it is passed to and from the device driver. OS/2 offers a wide range of device driver services to provide this functionality. Designing an OS/2 Device DriverDesigning an OS/2 device driver requires a thorough understanding of the role of a device driver, as well as a solid working knowledge of the OS/2 operating system and design philosophy. Debugging OS/2 device drivers can be difficult, even with the proper tools. The OS/2 device driver operates at Ring 0 with full access to the system hardware. However, it has almost no access to OS/2 support services, except for a handful of DevHlp routines. Many device driver failures occur in a real time context, such as in the midst of interrupt handling. It may be difficult or impossible to find a device driver problem using normal debugging techniques. In such cases, it is necessary to visualize the operation of the device driver and OS/2 at the time of the error to help locate the problem. Tools Necessary For Driver DevelopmentOne of the most important tools for device driver development is the device driver debugger. Generally, the best choice is the OS/2 2.1 kernel debugger or KDB. KDB uses a standard ASCII terminal attached to one of the serial COM ports via a null-modem cable. When OS/2 is started, KDB looks for a COM port to perform its I/O to the debugging terminal. For systems with only one COM port, KDB will use COM1. For systems with two COM ports, KDB will use COM2. The KDB is not simply a debugger, but is a replacement kernel that replaces the OS/2
standard system kernel called OS2KRNL. KDB has knowledge of internal OS/2 data structures
and provides a powerful command set for debugging OS/2 device drivers. Installing the
debugging kernel is easy. The attributes of the hidden file OS2KRNL are changed to
non-hidden and non-system, and the file is copied to OS2KRNL.OLD. The debug kernel is then
copied to OS2KRNL, and OS/2 is rebooted. KDB will issue a sign-on message to the debugging
terminal indicating that it is active. KDB can be entered by typing The Basics of Driver DesignThe device driver receives two basic types of requests: requests that can be completed immediately and those that cannot. It receives these requests via a standard data structure called a Request Packet (see Figure 5-1). Requests that can be completed immediately are handled as they come in, and sent back to the requestor. Requests that cannot be handled immediately (such as disk seeks) are queued up for later dispatch by the device driver. The device driver manipulates Request Packets using the DevHlp routines. To minimize head movement, disk device drivers usually sort pending requests for disk seeks in sector order. The OS/2 device driver plays an additional role in system performance and operation. When a device driver is called to perform a request that cannot be completed immediately, the device driver Blocks the requesting thread. This relinquishes the CPU and allows other threads to run. When the request is complete, usually as the result of an interrupt or error occurring, the thread is immediately UnBlocked and Run. The device driver then queries the request queue for any pending requests that may have come in while the thread was blocked. It is important to note that when an application calls a device driver, the application program's LDT is directly accessible by the device driver. Request PacketsThe first entry in the Request Packet Header is the Request Packet length, filled in by the kernel. The second parameter is the unit code. Applicable for block devices only, this field should be set by the device driver writer to zero for the first unit, one for the second, etc. The third field is the command code. The command code is filled in by the kernel. This is the code used by the switch routine in the Strategy section to decode the type of request from the kernel. The next field is the status word returned to the kernel. This field will contain the result of the device driver operation, along with the 'DONE' bit to notify the kernel that the request is complete (this is not always the case; the device driver may return without the 'done' bit set). To make things easier, a C language union should be used to access specific types of requests. The Request Packet structures are placed in an include file, which is included by the device driver mainline. Refer to the Standard OS/2 Device Driver Include File in Appendix C.
typedef struct ReqPacket {
UCHAR RPlength; // Request Packet length
UCHAR RPunit; // unit code for block DD only
UCHAR RPcommand; // command code
USHORT RPstatus; // status word
UCHAR RPreserved[4]; // reserved bytes
ULONG RPqlink; // queue linkage
UCHAR avail[19]; // command specific data
} REQPACKET;
Example 5-1. Request Packet. OS/2 Device Driver ArchitectureOS/2 device drivers come in two flavors, block and character. Block device drivers are used for mass storage devices such as disk and tape. Character device drivers are used for devices that handle data one character at a time, such as a modem. OS/2 device drivers are capable of supporting multiple devices, such as a serial communications adapter with four channels or a disk device driver which supports multiple drives. OS/2 device drivers receive requests from the OS/2 kernel on behalf of an application program thread. When the device driver is originally opened with a DosOpen API call, the kernel returns a handle to the thread that requested access to the device driver. This handle is used for subsequent access to the device driver. When an application makes a call to a device driver, the kernel intercepts the call and formats the device driver request into a standard Request Packet. The Request Packet contains data and pointers for use by the device driver to complete the request. In the case of a DosRead or DosWrite, for example, the Request Packet contains the verified and locked physical address of the caller's buffer. In the case of an IOCtl, the Request Packet contains the virtual address of a Data and Parameter Buffer. Depending on the type of request, the data in the Request Packet will change, but the Request Packet header length and format remain fixed. The kernel sends the Request Packet to the driver by passing it a 16:16 pointer to the Request Packet. Device drivers are loaded by the OS/2 loader at boot time, and the kernel keeps a linked list of the installed device drivers by name, using the link pointer in the Device Header. Before a device driver is used, it must be "DosOpen"ed from the application. The DosOpen specifies an ASCII-Z string with the device name as a parameter, which is the eight character ASCII name located in the Device Header. The kernel compares this name with its list of installed device drivers, and if it finds a match, it calls the OPEN section of the device driver Strategy routine to open the device. If the open was successful, the kernel returns to the application a handle to use for future device driver access. The device handles are usually assigned sequentially, starting with 3 (0, 1, and 2 are claimed by OS/2). However, the handle value should never be assumed.
typedef struct DeviceHdr
{
struct DeviceHdr far *DHnext; // ptr to next header, or FFFF
USHORT DHattribute; // device attribute word
OFF DHstrategy; // offset of strategy routine
OFF DHidc; // offset of IDC routine
UCHAR DHname[8]; // dev name (char) or #units (blk)
char reserved[8];
} DEVICEHDR;
DEVICEHDR devhdr =
{
(void far *) 0xFFFFFFFF, // link
(DAW_CHR | DAW_OPN | DAW_LEVEL1), // attribute
(OFF) STRAT, // &strategy
(OFF) 0, // &IDCroutine
"DEVICE1 ", // device name
};
Example 5-2. OS/2 device driver header. Device Driver ModesOS/2 2.1 device drivers operate in three different modes. The first, INIT mode, is a special mode entered at system boot time and executed at Ring 3. When the OS/2 system loader encounters a "DEVICE=" statement in the CONFIG.SYS file on boot-up, it loads the device driver .SYS file and calls the INIT function of the device driver. What makes this mode special is that the boot procedure is running in Ring 3 which normally has no I/O privileges, yet OS/2 allows Ring 0- type operations. The device driver is free to do port I/O and even turn interrupts off, but must ensure they are back on before exiting the INIT routine. The INIT routine can be used to initialize a Universal Asynchronous Receiver Transmitter (UART) or anything else necessary to ready a device. Ring 3 operation during INIT is necessary to protect the integrity of code that has already been loaded up to that point, and to make sure that the device driver itself does not corrupt the operating system during initialization. Ring 3 operation also allows the device driver initialization routine to call a limited number of system API routines to aid in the initialization process. For example, a device driver might use the API routines to read a disk file that contains data to initialize an adapter. The device driver also uses the API routines to display driver error and sign-on messages. The INIT code is only called once, during system boot. For this reason, the INIT code is usually located at the end of the code segment so it can be discarded after initialization. The second mode, called Kernel mode, is in effect when the device driver is called by the kernel as a result of an I/O request. The third mode, called Interrupt mode, is in effect when the device driver's interrupt handler is executing in response to an external interrupt, such as a character being received from a serial port. In general, the OS/2 device driver consists of a Strategy section, an INIT section, and optional interrupt and timer sections. The Strategy section receives requests from the kernel, in the form of Request Packet. The Strategy section verifies the request, and if it can be completed immediately, completes the request and sends the result back to the kernel. If the request cannot be completed immediately, the device driver optionally queues up the request to be completed at a later time and starts the I/O operation, if necessary. The kernel calls the Strategy routine directly by finding its offset address in the Device Header. The Device HeaderA simple OS/2 device driver consists of at least one code segment and one data segment, although more memory can be allocated if necessary. The first item of data that appears in the data segment must be the device driver header. The device driver header is a fixed length, linked list structure that contains information for use by the kernel during INIT and normal operation. The first entry in the header is a link pointer to the next device that the device driver supports. If no other devices are supported, the pointer is set to - 1L. A - 1L terminates the list of devices supported by this device driver. If the device driver supports multiple devices, such as a four-port serial board or multiple disk controller, the link is a far pointer to the next device header. When OS/2 loads device drivers at INIT time, it forms a linked list of all device driver device headers. The last device driver header will have a link address of -1L. When a DEVICE= statement is found in CONFIG.SYS, the last loaded device driver's link pointer is set to point to the new device driver's device header, and the new device driver's link pointer now terminates the list. The next entry in the device header is the Device Attribute Word (see Table 5- 1). The Device Attribute Word is used to define the operational characteristics of the device driver. The next entry is a one word offset to the device driver Strategy routine. Only the offset is necessary, because the device driver is written in the small model with a 64K code segment and a 64K data segment (this is not always true-in special cases, the device driver can allocate more code and data space if needed, and can even be written in the large model).
DEVICEHDR devhdr[2] =
{
{ (void far *) &.devhdr[1], // link to next dev
(DAW_CHR | DAW_OPN | DAW_LEVEL1), // attribute
(OFF) STRAT1, // &strategy
(OFF) 0, // &IDCroutine
"DEVICE1 ",
},
{(void far *) 0xFFFFFFFF, // link(no more devs)
(DAW_CHR | DAW_OPN | DAW_LEVEL1), // attribute
(OFF) STRAT2, // &strategy
(OFF) 0, // &IDCroutine
"DEVICE2 ",
}
};
Example 5-3. Device driver header, multiple devices. The next entry is an offset address to an IDC routine, if the device driver supports inter-device driver communications. (The DAW_IDC bit in the device attribute word must also be set, otherwise the AttachDD call from the other device driver will fail.) The last field is the device name, which must be eight characters in length. Names with less than eight characters must be space- padded. Remember, any mistake in coding the device driver header will cause an immediate crash and burn when booting. ------------------------------------------------- | Bit(s) | Description | ------------------------------------------------- | 15 | 1 if char driver, 0 if block | | ------------------------------------------------- | 14 | driver supports IDC | ------------------------------------------------- | 13 | non-IBM format block driver | ------------------------------------------------- | 12 | driver supports sharing | ------------------------------------------------- | 11 | driver supports removable media | ------------------------------------------------- | 10 | reserved, must be 0 | ------------------------------------------------- | 9-7 | driver function level | ------------------------------------------------- | 6-4 | reserved, must be 0 | ------------------------------------------------- | 3 | driver is clock device | ------------------------------------------------- | 2 | driver is a NUL device | ------------------------------------------------- | 1 | driver is new stdout device | ------------------------------------------------- | 0 | driver is new stdin device | ------------------------------------------------- Table 5-1. Device Attribute Word Capabilities Bit StripThe Capabilities Bit Strip word defines additional features supported on level 3 drivers only (see Table 5-2). Note that if the device driver is an ADD device driver, and sets bit 7 and 8 in the device attribute word as well as bit 3 in the capabilities bit strip, the Init request packet sent by the kernel will be formatted differently than the standard PDD Init request packet. Refer to the appropriate ADD documentation for a description of the ADD Init request packet format. ------------------------------------------------- | Bit(s) | Description | ------------------------------------------------- | 0 | set if driver supports DevIOCtl2| ------------------------------------------------- | 1 | 32-bit memory addressing | ------------------------------------------------- | 2 | driver supports parallel port(s)| ------------------------------------------------- | 3 | driver is an ADD driver | ------------------------------------------------- | 4 | CmdInitComplete strategy command| ------------------------------------------------- | 5-31 | reserved, must be 0 | ------------------------------------------------- Table 5-2. Capabilities Bit Strip. Providing a Low-Level InterfaceThe 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. Since our OS/2 device drivers are written in C, a mechanism must be provided for putting the code and data segments in the proper order, as well as providing a low-level interface to handle device and timer interrupts. Since the Device Header must be the first item that appears in the data segment, the C compiler must be prevented from inserting the normal C start-up code before the Device Header. Additionally, a method of detecting which device is being requested needs to be provided for device drivers that support multiple devices. These requirements are handled with a small assembly language stub that is linked in with the device driver (refer to Example 5-4). The __acrtused entry point prevents the C start-up code from being inserted before the device driver data segment. The segment-ordering directives ensure that the data segment precedes the code segment.
;
; C start-up routine, one device
;
EXTRN _main:near
PUBLIC _STRAT
PUBLIC __acrtused
_DATA segment word public 'DATA'
_DATA ends
CONST segment word public 'CONST'
CONST ends
_BSS segment word public 'BSS'
_BSS ends
DGROUP group CONST,_BSS,_DATA
_TEXT segment word public 'CODE'
assume cs:_TEXT,ds:DGROUP,es:NOTHING,ss:NOTHING
.286P
;
_STRAT proc far
__acrtused: ;no start-up code
;
push 0
jmp start ;signal device 0
;
start:
push es ;send Request Packet address
push bx
call _main ;call driver mainline
pop bx ;restore es:bx
pop es
add sp,2 ;clean up stack
mov word ptr es:[bx+3],ax ;send completion status
ret
;
_STRAT endp
;
_TEXT ends
end
Example 5-4. Start-up routine, one device. Note the _STRAT entry point. Remember that this is the address placed in the device driver's Device Header. The kernel, when making a request to the device driver, looks up this address in the Device Header and makes a far call to it. The assembly language routine then, in turn, calls the C mainline. Thus, the linkage from the kernel to the device driver is established. Note the "push 0" in the beginning of the _STRAT routine. This is to notify the device driver which device is being requested. Each device supported by the device driver requires its own separate Device Header. Note also that each Device Header contains an offset address to its own Strategy routine. Using the assembly language interface, the device number is pushed on the stack and passed to the device driver Strategy section for service. The device driver retrieves the parameter and determines which device was requested. One of the parameters to main is the int dev (see Example 5-7), the device number that was passed from the assembly language start-up routine. The assembly language start-up routine is modified to support multiple devices by adding entry points for each device's Strategy section. The modified source for this routine is shown in Example 5-5. The assembly language routine in Example 5-6 provides the interrupt handler and timer handler entry points. The interrupt handler entry point provides a convenient place to put a breakpoint before entering the C code of the main interrupt handler. The timer handler entry point provides a place to save and restore the CPU registers. Note that the interrupt handler does not need to save the register contents, as this is done by the OS/2 kernel. The timer handler, however, must save and restore register contents.
|
This site was last updated 07/16/07