Mark Nelson is a programmer for 21CenNet in Richardson, Texas. 21CenNet is a supplier of hardware and software for mobile computing. You can reach Mark on CompuServe at 73650,312.
DOS extenders for the Intel 80286 (or 16-bit extenders) represent an affordable and effective bridge between conventional MS-DOS programming and protected-mode environments, such as OS/2 or UNIX. The process of making a conventional MS-DOS program work with a DOS extender consists for the most part of cleaning up memory accesses and storage mechanisms. Special care must be given to code that directly accesses hardware, such as a video card, but in general the process is straightforward. (For more information on DOS extenders see the sidebar, "A Quick Look at DOS Extenders.")
This article discusses an aspect of using DOS extenders that is somewhat murky developing and using bimodal interrupt handlers. In this case, I develop a bimodal serial interrupt handler that works with the Phar Lap 286|DOS-Extender. The techniques used here apply to several other popular DOS extenders, with minor implementation specific changes.
The Limitations of DOS Extenders
While there is a lot of activity going on behind the scenes in a DOS Extender, as an application programmer you probably won't have to be too concerned about it. Particularly if you program in a higher-level language, a good DOS extender won't step on your toes too often. You should expect to be able to use most system resources in just the same way as before.However, there are a few things that MS-DOS doesn't provide that people need to use on a regular basis. One service that I need frequently is access to an interrupt-driven serial port. Both the PC BIOS and MS-DOS talk to the serial ports on the ISA and MCA busses only in polled mode. While this might be adequate for printer output in a single-tasking environment, it is not up to more taxing applications. High-speed input or output in a multitasking environment require additional systems-level code.
The traditional approach to solving high-speed communications problems under MS-DOS is to write an interrupt service routine (ISR) in either assembly language or C, and use it to collect input data as it comes in. The data is then stored in a buffer and read by the application program at its leisure. This approach has become fairly routine and has been incorporated into the body of MS-DOS programming folklore.
The Traditional Approach Breaks Down
Two things have conspired to render the traditional approach to RS-232 interrupt handling inadequate. First, modem speeds have been steadily rising over the past few years. Modems running at 9,600 baud with V.42bis data compression are now fairly common, requiring serial link rates to push up to 38,400 bits per second. At the same time, various types of code bloat make it necessary for programmers to routinely use 16-bit DOS extenders in their programs.An interrupt handler written under a DOS extender would normally be just like any other piece of protected-mode code. However, the DOS extender has to contend with the possibility that an interrupt may occur while the processor is in real mode. For example, a new character could come in on the serial port while MS-DOS is in the middle of reading the disk buffer discussed earlier. When this happens, the DOS extender has to intercept the interrupt, then switch the processor back to protected mode. The application program processes the interrupt in protected mode, then returns control to the DOS extender. The DOS extender finally has to switch the processor back to real mode and return control to the interrupted process.
While this may seem like an awful lot of work to do, nothing about it is impossibly difficult. In fact, protected-mode applications regularly handle interrupts in this fashion for things like the keyboard or the timer chip. But when you try to do this on an 80286 system with high-speed serial input, you find that things no longer work properly.
The problem is simply a matter of timing. At modern speeds of 19,200 baud, a new character arrives at the PC's input port approximately once every 500 microseconds. On most 80286 systems, it takes roughly 500 microseconds to switch modes, either from protected to real or vice versa.
It doesn't take an advanced math degree to see the problem here. If a new character is ready to be read in from the UART, and the processor is in real mode, it first must be switched to protected mode. This process takes about 500 microseconds. The interrupt handler then reads in the character, and returns to real mode, which takes another 500 microseconds. Unfortunately, during the one millisecond it has taken to process the first character, two more have arrived, and we will have lost one of them. This is all due to the cycle time of one millisecond per interrupt.
An 80386 processor usually doesn't have to deal with this problem. It has slightly more sophisticated circuitry for mode switching, giving it a round-trip time of less than 100 microseconds. This 10:1 speed advantage over the 80286 means that 80386 DOS-extender programmers can ignore this problem, and code all their interrupt handlers in pure protected mode.
What to Do?
There are two potential solutions to this problem. First, you could ignore the 80286 audience and elect to only support 80386 machines with your DOS-extended application. While this solves your problem it may cut back on your potential audience. A more palatable alternative is to begin developing bimodal interrupt handlers for your time-critical events.A bimodal interrupt handler performs the same chores that a regular interrupt handler does, but with an interesting twist. The bimodal handler will process the interrupt in whatever mode the processor is in when the interrupt occurs. Thus, if the interrupt occurs in real mode, the handler doesn't have to switch to protected mode, saving the one-millisecond round-trip penalty.
While we refer to the bimodal handler as if it were a single piece of code, in actuality it is usually two pieces of code that perform an identical function. There are two actual copies of the code in the machine, and although they are very similar to one another, there are some small differences.
Implementation
The interrupt handler in the program shown here receives incoming characters from the 8250 UART on an IBM PC. This means the interrupt service routine has to read incoming characters from the UART, store them in a buffer, and update the buffer pointers. The main program can then read the characters in from the buffer at its leisure.In this demonstration program, the code to accomplish this task is written in C, and takes up only a few lines of code (see Listing 1) . It resembles what you might find in a standard program running under conventional MS-DOS.
As you can see from this listing, the incoming character is read in from the UART, then stored in a buffer. The head pointer for the buffer is then incremented in preparation for the next character. Note that all of the data items being manipulated here are part of the Port data structure.
The Port data structure used here takes on a special importance under a DOS extender. In the bimodal interrupt handler, both the protected- and real-mode interrupt handlers must be able to manipulate the data in this structure. This has one immediate implication the data structure must be in the low 640K memory area. If the data structure was loaded into high memory, the real-mode ISR would not be able to access it.
The other important piece of code that must be in low memory will be the real-mode ISR. Once again, if an interrupt occurred in real mode, and the real-mode ISR was in high memory, you couldn't execute the code in the ISR! So, when writing this bimodal interrupt handler, first you need to hurdle the obstacle of finding a way to force both the Port data structure and the real-mode ISR to be loaded into low memory (and stay there).
Loading Real-Mode Code
The standard compilers and linkers used under MS-DOS aren't equipped to deal with the concept of real- and protected-mode code. To them, all types of code are the same. The concept of having two different flavors of code in the same program just doesn't make sense. Because of this, no good way exists to tell the compiler, linker, or MS-DOS that some special modules in a program absolutely have to be loaded into real memory.Phar Lap deals with this problem by separating the real- and protected-mode code into two different modules. The Phar Lap 286 | DOS Extender links all of the protected-mode code into a standard .exe file and the real-mode code into a Dynamic Link Library (DLL). When the .exe file runs, Phar Lap code loads the DLL into memory, making sure that all of its segments are loaded into the 640K DOS memory area. This runtime loading and binding lets Phar Lap get around a missing feature of the MS-DOS program linker/loader system.
The DLL loading mechanism allows data to be loaded in along with the code. While there really isn't any such thing as real-mode data, placing the real-mode ISR data structures in a real-mode DLL guarantees that the data will be placed in the lower 640K bytes of the machine, making it accessible to both protected and real-mode code. Code inside a DLL will not be able to access data in the main program, so we have to place the port structure in the DLL.
The linker resolves protected-mode access to this data without causing too much trouble. When declaring external definitions for this code, the far keyword must be used, since the data will not show up in the normal data segment that the compiler expects. Other than that, the protected-mode portions of the program can treat data loaded as part of a DLL in exactly the same way as data loaded in the main .exe file.
Down to Specifics
To demonstrate just how a bimodal interrupt handler works under the Phar Lap 286|DOS Extender, I have written a very basic terminal program called TERM286.C shown in Listing 2, Listing 3, and Listing 4. A quick look at Listing 3 and Listing 4 shows that the two interrupt service routines, real_isr and prot_isr, are almost identical. To aid in testing this program, I added a pair of counters to the port structure that keeps track of how many real-mode and protected-mode interrupts have been processed. The only difference between the two versions of the ISR lies in which one of the counters each increments.In a normal communications program, you replace the default interrupt handler with your own using standard MS-DOS function calls. Interrupt 21H functions 25H and 35H are used to get and set the interrupt handlers for a port. However, MS-DOS doesn't have any protocol for installing bimodal handlers, so Phar Lap had to replace this with a function call of their own. In Listing 4, the function SetInterruptVectors makes the following function call:
DosSetReal ProtVec( Port.interrupt_number, (PIHANDLER) port_isr, real_isr_ptr, (PIHANDLER far *) &Port.old_prot_vector, (REALPTR far *) &Port.old_real_vector );This cryptically-named function takes the place of the MS-DOS get vector and set vector functions (Int21H Functions 25H and 35H). It also adds support for bimodal handlers. The first argument to this function is simply the interrupt number, used exactly as it would have been in a DOS function call. The second two arguments are the addresses of the real- and protected-made ISRs. Finally, the next two parameters are pointers to a memory area where the old values of those two ISRs can be saved. We keep those in the port structure so they can be restored when the program exits.One thing to note in this function call is that instead of passing the address of real_isr directly to the DosSetRealProtVec function, I pass it a variable named real_isr_ptr. This variable was defined a few lines earlier in OpenComPort with the function call
real_isr_ptr= DosProtToReal ((PVOID) real_isr);Here you see an instance of one source of perpetual confusion to programmers using DOS extenders keeping track of which pointers are real-mode pointers and which are protected-mode pointers. In this particular instance, the linker makes sure that all addresses used in the program are protected-mode addresses, even if they refer to data in the real-mode DLL. Because I am guaranteed protected-mode addresses, I can directly access any piece of code or data in the program without having to do anything unusual. This direct access allows me to look directly at the port structure without having to perform any conversions. However, the DosSetRealProtVec function wants the address of the real-mode ISR to be a real-mode address, and I have to call a Phar Lap function to make the conversion.The DosSetRealProtVec function call makes up the core of Phar Lap's support for bimodal interrupt handlers. Once called, DosSetRealProtVec routes any interrupt that occurs in real mode to the real_isr function, and any interrupt that occurs in protected mode to prot_isr. TERM286.C has some added code in the main body of the program to verify that this routing is working properly. Pressing the CTRL-Z key while the terminal emulator is running will dump out the contents of the port status buffer. An example of this is shown in Figure 1.
Under normal MS-DOS operations, you will find that many of the interrupts occur in real mode. This is to be expected, since any call to MS-DOS or the BIOS causes the processor to switch to real mode. And TERM286.C does all of its input and output to the screen and keyboard via the C runtime library, which uses MS-DOS and the BIOS. Interestingly, if you run the same program in an DOS box under MS-Windows in 386 mode, you will see all of your interrupts occurring in protected mode.
Putting It All Together
Writing the code for this program is a relatively straightforward task. However, compiling and linking the modules under the Phar Lap 286|DOS-Extender can be difficult, particularly for the uninitiated. Most MS-DOS programmers are not used to building a DLL and linking it with another application. Some of the specific details can be somewhat bewildering.First, you need to create the DLL a multi-stage process. You begin by compiling REAL_ISR.C to object form for use as if it were part of a real-mode application. Listing 5 and Listing 6 show this as the first executable lines in the batch files for the Microsoft and Borland C compilers.
Once REAL_ISR. C has been compiled to object form, use the standard MS-DOS linkers to create a DLL. Both the Microsoft and Borland linkers know how to create DLL files, given the appropriate switches. Both of these linkers require the use of a .def file to create a DLL. In this case, the .def file contains two very important pieces of information: the names of a routine and a data structure you will want to export. The real_isr routine and the Port data structure both have to be made available to any routine that loads the DLL. Both must be defined in the .def file. The Microsoft .def file is shown in Listing 7. The Borland .def file does away with the EXETYPE line.
After building the DLL, the Borland version of the DLL needs to be marked as a real-mode DLL with a call to bcc286, as shown in Listing 5. Finally, both versions of this process have to call implib, the import library manager. implib creates a lib file that tells the linker everything it needs to know about linking to the DLL. Without a valid lib file, the linker wouldn't know how to access the code in the DLL.
Finally, the last line of the two .bat files invokes the protected-mode version of the compiler to compile TERM286.C, and link it in protected mode to REAL_ISR.LIB. If all goes according to plan, TERM286.EXE is now ready to run.
Enhancements
TERM286.C is a very simple program, but will work adequately under the Phar Lap 286|DOS-Extender. One improvement that could be made to this program would be to start using a protected-mode library for all of the terminal I/O. By writing to the screen in protected mode, the code would perform fewer mode switches, resulting in significantly less overhead.A second performance enhancement would be to begin using interrupt-driven output on the COM port. The polled-mode output used in TERM286.C works effectively, but ties up the processor when it could be doing more useful work elsewhere.
Suggested Reading
In the event that servicing COM-port interrupts puts you on unfamiliar ground, these articles describe the basic steps needed to create and use an interrupt handler under MS-DOS:Nelson, Mark. October 1990. "Servicing COM Port Interrupts." Tech Specialist.
Nelson, Mark. April 1991. "What To Do When You're Past COM2." Tech Specialist.