Additions:
AUTHOR: Steven J. Mastrianni
Deletions:
AUTHOR: Steven J. Mastrianni
Additions:
OS/2 device drivers continue to be a limiting factor in the acceptance and use of OS/2. DOS drivers abound, but OS/2 drivers are scarce as hen's teeth -- for a variety of reasons. OS/2 drivers are more complicated than DOS drivers. They've got to handle context switching and priorities and accommodate dual-mode operation (real versus protected) -- issues foreign to many DOS programmers. In this article, I'll describe how to build an asynchronous RS-232 terminal driver for OS/2 in C, complete with interrupt handler and timer support (the code you'll need to build this driver is available on BIX). Once you've seen how that's done, you'll have the basic understanding you need to write OS/2 drivers for other types of devices.
Deletions:
OS/2 device drivers continue to be a limiting factor in the acceptance and use of OS/2. DOS drivers abound, but OS/2 drivers are scarce as hen's teeth -- for a variety of reasons. OS/2 drivers are more complicated than DOS drivers. They've got to handle context switching and priorities and accommodate dual-mode operation (real versus protected) -- issues foreign to many DOS programmers. In this article, I'll describe how to build an asynchronous RS-232 terminal driver for OS/2 in C, complete with interrupt handler and timer support (the code you'll need to build this driver is available on BIX). Once you've
seen how that's done, you'll have the basic understanding you need to write OS/2 drivers for other types of devices.
Additions:
The device-driver header is a fixed-length, link-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 the driver supports. If no other devices are supported, the pointer is set to -1L. This terminates the list of devices supported by this driver. If the 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.
The next entry in the device header is the attribute word, followed by a one-word offset to the driver Strategy section. Only the offset is necessary, because the driver is written in the small model with a 64-kilobyte code segment and a 64-KB data segment (this is not always true; in special cases, the driver can allocate more code and data space if needed).
The succeeding entry is an offset address to an interdriver communications routine if the driver supports IDC. (The DAW_IDC bit in the device attribute word must also be set; otherwise, the AttachDD call from the other driver will fail.)
The last field is the device name, which must be eight characters in length. Names with fewer than eight characters must be padded with blanks. Remember, any mistake in coding the device-driver header will cause an immediate crash and burn when booting.
Providing a Register Interface to the C Driver OS/2 device drivers are normally written in C, using the small model, which means 64 KB of data and 64 KB of code (code and data space may be increased in special cases). The driver .SYS file must load the data segment before the code segment. When you write an OS/2 driver in C, you must provide a mechanism for putting the code and data segments in the proper order, and you must also provide a low-level interface
to handle device and timer interrupts. Because the device header must be the first item that appears in the data segment, you have to prevent the C compiler from inserting the C start-up code before the device header. You may also have to provide a method of detecting which device is being requested for drivers that support multiple devices. The small assembly language program in listing 4 takes care of these requirements. The _acrtused entry point prevents the C start-up code from being inserted before the driver data segment. The segment-ordering directives ensure that the data segment precedes the code segment.
Note the _STRAT entry point. How does this get called? Remember, this is the address that is placed in the driver's data-segment device header. The kernel, when making a request to the driver, looks up this address in the device header and makes a far call to it. The assembly language routine then calls the C mainline. Thus, the linkage from the kernel to the driver is established.
Why is there a push 0 at the beginning of the _STRAT routine? That's the device number. Each device supported by the device driver requires a separate device header, and each device header contains an offset address to its own Strategy section. Using the assembly language interface, the routine pushes the device number on the stack and passes it to the driver Strategy section for service.
==The Strategy Section==
The Strategy section is nothing more than a big switch statement. Common driver requests, such as DosWrite and DosRead, have standard function and return codes. The driver may ignore any or all of these requests by returning a Done status to the kernel. This tells the kernel that the request has been completed. The status returned to the kernel can also include error information that the kernel returns to the calling program.
Note that in the case of a standard driver function, the kernel will map the error value returned from the driver to one of the standard return codes. It is therefore impossible to pass any special return codes to the application via a standard driver request. If you attempt to do so, the kernel will intercept the special return code and map it to one of the standard return codes. The only way to return a special code to the application is by means of an IOCtl request. IOCtls are
used for special driver-defined operations (e.g., port I/O). IOCtls are accessed when the application issues a DosDevIOCtl call with the driver's handle. This flexibility allows the driver writer to customize the device driver to fit any device. For instance, if you had a serial driver that monitored bus traffic and reported the
occurrence of one or more special characters, you could use an IOCtl read and pass back the character in the return code.
Listing 5 shows the skeleton of a Strategy section. Note the switch on the request-packet command. Several standard driver functions have command codes predefined in OS/2. The driver writer can act on or ignore any of the requests to the driver. Although it would not make sense, the driver could ignore the Open command, issued by the kernel in response to a DosOpen call. Or, more logically, the driver can refuse to be deinstalled by rejecting a Deinstall request.
The INIT call is made only once, during system loading in response to a DEVICE= in CONFIG.SYS. The call is made in the INIT mode from ring 3, but with I/O privileges. The INIT routine is where you would insert the code to initialize your device, such as configuring a UART or sending a disk to track 0.
The very first thing you must do in the initialization code is to save the DevHlp entry-point address in the driver's data segment. This is the only time the address is valid. It must be saved, or it is lost forever. The address of the DevHlp entry point is passed in the INIT request packet. The initialization code performs two other functions. First, it issues the sign-on message to the screen that the driver is attempting to load. Second, it finds the segment address of the last data and last code item, and it sends them back to OS/2. OS/2 uses the code- and data-segment values to size memory. If a driver fails installation, it must send back zeroes for the CS and DS registers so that OS/2 can use the memory space it occupied.
One of the most common techniques in OS/2 driver design is for the Strategy section to request service from the device and wait for a device or timer interrupt to signal completion of the request. The fragment in listing 6 shows an implementation of this scheme for the Read function of my sample serial communications driver. In this case, the Strategy section starts the I/O and issues a Block DevHlp call, which blocks the calling thread. When the device interrupt signals that the operation is done, the interrupt section runs the blocked thread, completing the request. To protect against the request's never being completed (e.g., in the case of a down device), the Block call can contain a time-out parameter. If the time expires before the completion interrupt occurs, the Strategy section can end the proper error back to the kernel.
Deletions:
The device-driver header is a fixed-length, link-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 the driver supports. If no other devices are supported, the
pointer is set to -1L. This terminates the list of devices supported
by this driver. If the 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.
The next entry in the device header is the attribute word, followed by
a one-word offset to the driver Strategy section. Only the offset is
necessary, because the driver is written in the small model with a 64-
kilobyte code segment and a 64-KB data segment (this is not always
true; in special cases, the driver can allocate more code and data
space if needed).
The succeeding entry is an offset address to an interdriver
communications routine if the driver supports IDC. (The DAW_IDC bit in
the device attribute word must also be set; otherwise, the AttachDD
call from the other driver will fail.)
The last field is the device name, which must be eight characters in
length. Names with fewer than eight characters must be padded with
blanks. Remember, any mistake in coding the device-driver header will
cause an immediate crash and burn when booting.
Providing a Register Interface to the C Driver
OS/2 device drivers are normally written in C, using the small model,
which means 64 KB of data and 64 KB of code (code and data space may
be increased in special cases). The driver .SYS file must load the
data segment before the code segment. When you write an OS/2 driver in
C, you must provide a mechanism for putting the code and data segments
in the proper order, and you must also provide a low-level interface
to handle device and timer interrupts. Because the device header must
be the first item that appears in the data segment, you have to
prevent the C compiler from inserting the C start-up code before the
device header. You may also have to provide a method of detecting
which device is being requested for drivers that support multiple
devices. The small assembly language program in listing 4 takes care
of these requirements. The _acrtused entry point prevents the C start-
up code from being inserted before the driver data segment. The
segment-ordering directives ensure that the data segment precedes the
code segment.
Note the _STRAT entry point. How does this get called? Remember, this
is the address that is placed in the driver's data-segment device
header. The kernel, when making a request to the driver, looks up this
address in the device header and makes a far call to it. The assembly
language routine then calls the C mainline. Thus, the linkage from the
kernel to the driver is established.
Why is there a push 0 at the beginning of the _STRAT routine? That's
the device number. Each device supported by the device driver requires
a separate device header, and each device header contains an offset
address to its own Strategy section. Using the assembly language
interface, the routine pushes the device number on the stack and
passes it to the driver Strategy section for service.
The Strategy Section
The Strategy section is nothing more than a big switch statement.
Common driver requests, such as DosWrite and DosRead, have standard
function and return codes. The driver may ignore any or all of these
requests by returning a Done status to the kernel. This tells the
kernel that the request has been completed. The status returned to the
kernel can also include error information that the kernel returns to
the calling program.
Note that in the case of a standard driver function, the kernel will
map the error value returned from the driver to one of the standard
return codes. It is therefore impossible to pass any special return
codes to the application via a standard driver request. If you attempt
to do so, the kernel will intercept the special return code and map it
to one of the standard return codes. The only way to return a special
code to the application is by means of an IOCtl request. IOCtls are
used for special driver-defined operations (e.g., port I/O). IOCtls
are accessed when the application issues a DosDevIOCtl call with the
driver's handle. This flexibility allows the driver writer to
customize the device driver to fit any device. For instance, if you
had a serial driver that monitored bus traffic and reported the
occurrence of one or more special characters, you could use an IOCtl
read and pass back the character in the return code.
Listing 5 shows the skeleton of a Strategy section. Note the switch on
the request-packet command. Several standard driver functions have
command codes predefined in OS/2. The driver writer can act on or
ignore any of the requests to the driver. Although it would not make
sense, the driver could ignore the Open command, issued by the kernel
in response to a DosOpen call. Or, more logically, the driver can
refuse to be deinstalled by rejecting a Deinstall request.
The INIT call is made only once, during system loading in response to
a DEVICE= in CONFIG.SYS. The call is made in the INIT mode from ring
3, but with I/O privileges. The INIT routine is where you would insert
the code to initialize your device, such as configuring a UART or
sending a disk to track 0.
The very first thing you must do in the initialization code is to save
the DevHlp entry-point address in the driver's data segment. This is
the only time the address is valid. It must be saved, or it is lost
forever. The address of the DevHlp entry point is passed in the INIT
request packet. The initialization code performs two other functions.
First, it issues the sign-on message to the screen that the driver is
attempting to load. Second, it finds the segment address of the last
data and last code item, and it sends them back to OS/2. OS/2 uses the
code- and data-segment values to size memory. If a driver fails
installation, it must send back zeroes for the CS and DS registers so
that OS/2 can use the memory space it occupied.
One of the most common techniques in OS/2 driver design is for the
Strategy section to request service from the device and wait for a
device or timer interrupt to signal completion of the request. The
fragment in listing 6 shows an implementation of this scheme for the
Read function of my sample serial communications driver. In this case,
the Strategy section starts the I/O and issues a Block DevHlp call,
which blocks the calling thread. When the device interrupt signals
that the operation is done, the interrupt section runs the blocked
thread, completing the request. To protect against the request's never
being completed (e.g., in the case of a down device), the Block call
can contain a time-out parameter. If the time expires before the
completion interrupt occurs, the Strategy section can send the proper
error back to the kernel.
Additions:
Another way to time-out a device is to use the SetTimer DevHlp routine. You can attach a timer handler to the OS/2 system clock and have the handler run the blocked thread after a specified number of ticks.
The commands allowed by the Strategy section are up to the device driver writer. You can process only the commands you wish to act on and let the others simply pass by sending a Done status back to the kernel. You may instead wish to trap the illegal function calls and return an ERROR_BAD_COMMAND message to the kernel. Keep in mind, however, that the kernel frequently issues its own commands to the driver without your knowledge. For example, when the user of the application that opened the driver types a Control-C, the kernel checks the application's list of open drivers and issues a Close request to each one. In general, I've found it easier to ignore all the requests I'm not waiting for and just flag them as done.
In the simplest of drivers, the Strategy section can only contain an Open, Close, and Read or Write request. In a complicated driver, such as a disk driver, the Strategy section may contain over two dozen standard driver functions and several additional IOCtl calls. IOCtl calls are actually Strategy functions, but they are broken down one step further to provide more detailed or device-specific operations. For instance, a driver might send a list of parameters to an I/O port to initialize it and return the input value of a status port with the status of the initialization.
==A Sampler of Standard Driver Functions==
**INIT (code 0x00)**. This function is called by the kernel during driver installation at boot time. The INIT section should initialize your device, such as setting the baud rate, parity, stop bits, and so forth on a serial port or checking to see if the device is installed by issuing a status request to the device controller. This INIT function is called in a special mode in ring 3 with some ring 0 capabilities.
The driver may turn off interrupts, but they must be turned back on before returning to the kernel. The INIT code may perform direct port I/O without protection violations. Usually, the driver writer will allocate buffers and data storage during initialization, to be sure the driver will work when installed. Because the initialization is being performed in ring 3, the system can check to make sure the buffer and storage allocations are valid and the segments are owned by the driver. If not, the driver can remove itself from memory, freeing up any previously allocated space for other system components or another driver. Because initialization is done only once during system boot-up, it is not critical to optimize the section. Do all your initializations here, as it may be time-prohibitive or even impossible to do initialization during normal driver operation.
**Media Check (code 0x01)**. This function is called by the kernel prior to disk access, and it is therefore valid only for block devices. The kernel passes the driver the media ID byte corresponding to the type of disk it expects to find in the selected drive.
**BuildBPB (code 0x02)**. When the block driver gets a Build Bios Parameter Block call, it must return a pointer to the BPB that describes the mass-storage device.
**Read (code 0x04)**. The application calls the Read section by issuing a DosRead with the handle obtained during the DosOpen. The Read routine may return one character at a time, but more often it returns a buffer full of data. How the Read function works is up to the driver writer. The driver returns the count of characters read and stores the received data in the data segment of the application. Read returns a standard driver return code.
**Nondestructive Read (code 0x05)**. In response to this request, the driver must get the first character in the driver buffer and return it to the caller. If no character is present, the driver must return immediately with the proper error bits and Done bit set.
**Input Status (code 0x06)**. The driver must clear the Busy bit in the request packet if one or more characters are in the driver's buffer, or set it if no characters are present. This is a Peek function to determine the presence of data.
==Do You Really Need a Device Driver?==
Deletions:
Another way to time-out a device is to use the SetTimer DevHlp
routine. You can attach a timer handler to the OS/2 system clock and
have the handler run the blocked thread after a specified number of
ticks.
The commands allowed by the Strategy section are up to the device
driver writer. You can process only the commands you wish to act on
and let the others simply pass by sending a Done status back to the
kernel. You may instead wish to trap the illegal function calls and
return an ERROR_BAD_COMMAND message to the kernel. Keep in mind,
however, that the kernel frequently issues its own commands to the
driver without your knowledge. For example, when the user of the
application that opened the driver types a Control-C, the kernel
checks the application's list of open drivers and issues a Close
request to each one. In general, I've found it easier to ignore all
the requests I'm not waiting for and just flag them as done.
In the simplest of drivers, the Strategy section can only contain an
Open, Close, and Read or Write request. In a complicated driver, such
as a disk driver, the Strategy section may contain over two dozen
standard driver functions and several additional IOCtl calls. IOCtl
calls are actually Strategy functions, but they are broken down one
step further to provide more detailed or device-specific operations.
For instance, a driver might send a list of parameters to an I/O port
to initialize it and return the input value of a status port with the
status of the initialization.
A Sampler of Standard Driver Functions
INIT (code 0x00). This function is called by the kernel during driver
installation at boot time. The INIT section should initialize your
device, such as setting the baud rate, parity, stop bits, and so forth
on a serial port or checking to see if the device is installed by
issuing a status request to the device controller. This INIT function
is called in a special mode in ring 3 with some ring 0 capabilities.
The driver may turn off interrupts, but they must be turned back on
before returning to the kernel. The INIT code may perform direct port
I/O without protection violations. Usually, the driver writer will
allocate buffers and data storage during initialization, to be sure
the driver will work when installed. Because the initialization is
being performed in ring 3, the system can check to make sure the
buffer and storage allocations are valid and the segments are owned by
the driver. If not, the driver can remove itself from memory, freeing
up any previously allocated space for other system components or
another driver. Because initialization is done only once during system
boot-up, it is not critical to optimize the section. Do all your
initializations here, as it may be time-prohibitive or even impossible
to do initialization during normal driver operation.
Media Check (code 0x01). This function is called by the kernel prior
to disk access, and it is therefore valid only for block devices. The
kernel passes the driver the media ID byte corresponding to the type
of disk it expects to find in the selected drive.
BuildBPB (code 0x02). When the block driver gets a Build Bios
Parameter Block call, it must return a pointer to the BPB that
describes the mass-storage device.
Read (code 0x04). The application calls the Read section by issuing a
DosRead with the handle obtained during the DosOpen. The Read routine
may return one character at a time, but more often it returns a buffer
full of data. How the Read function works is up to the driver writer.
The driver returns the count of characters read and stores the
received data in the data segment of the application. Read returns a
standard driver return code.
Nondestructive Read (code 0x05). In response to this request, the
driver must get the first character in the driver buffer and return it
to the caller. If no character is present, the driver must return
immediately with the proper error bits and Done bit set.
Input Status (code 0x06). The driver must clear the Busy bit in the
request packet if one or more characters are in the driver's buffer,
or set it if no characters are present. This is a Peek function to
determine the presence of data.
**Do You Really Need a Device Driver?**