Writing MS-DOS Device Drivers



December 01, 1990
URL:http://www.drdobbs.com/writing-ms-dos-device-drivers/184402277

December 1990/Writing MS-DOS Device Drivers

Marcus Johnson received his B.S. in math from the University of Florida in 1978 and is currently Senior System Engineer for Precision Software in Clearwater, Florida. You may reach him at 6258 99th Circle, Pinellas Park, Florida.

Introduction

This article describes, from my personal experience, the joys of writing MS-DOS device drivers in C. A device driver is the executable code through which the operating system can communicate with an I/O device.

Many of the device drivers you use on your MS-DOS system are already part of the operating system: the basic keyboard/screen (console) driver, the floppy and hard disk drivers, the serial (COM) port driver, and the printer port driver.

The drivers that I have written include a RAM disk driver and an ANSI console driver. They have been compiled under Microsoft C v4.0 and assembled under Microsoft MASM v5.1. The executable binaries were created with Microsoft Link v3.64. Certain modifications will have to be made in order for this to compile under Turbo C.

I wrote much of these drivers in C partly as an exercise and partly to make the code easier to write, understand, and extend. For sheer speed, assembly language is still better. But if you aren't that comfortable in assembly language, what better starting point than the relatively clean, documented, correct code produced by your compiler?

The significance of installable device drivers, such as provided under MS-DOS, is that you can interface a device to your system that was not originally part of it. The relative ease with which you can write a device driver has led to the proliferation of low-cost peripherals in the MS-DOS environment. Once written, these drivers are installed by simply creating (or adding to) an ASCII text file called config. sys in the root directory on your boot disk. For each device, the config.sys file contains a line that reads

device=filename [options]
where filename is the name of the file containing your device driver, and [options] are optional instructions for your device driver. Well-known examples of standard drivers include ansi.sys, the console driver that allows certain common ANSI escape sequences to be properly interpreted on the screen, and vdisk.sys, the disk driver that lets you keep files in RAM.

ansi.sys and vdisk.sys represent two of the three device driver types. ansi.sys is a driver of the first type, a character device driver. It is intended to handle a few bytes of data at a time, and can handle single bytes. vdisk.sys is a driver of the second type, a block device driver. It handles data in chunks whose units are called blocks or sectors.

The third type, a clock device driver, is actually a modified character device driver. It is easy to write. I have not provided an example since I do not have clock hardware to test it with.

Device Driver Format

Device drivers must rigorously follow a specific plan. Each must include a header, a strategy routine, an interrupt routine, and a set of command code routines. The device driver is typically a memory image file, like a .com file. The main difference between a device driver and a .com file is that the .com file starts at offset 0x0100 and the device driver starts at 0x0000.

The device header is the first part of the file. It contains the following fields:

The header for a character device driver is followed by an 8-byte logical name such as PRN, CON, or COM1. This is the name by which the device is known to the system. You use it exactly as you would any other named device. The header for a block device driver is followed by a byte containing the number of units controlled, followed by seven null bytes.

Note that there can be many device drivers within one file, with each driver pointing to the next. The last driver in the file uses 0xFFFF for the offset and segment of the link to the next driver. Thus, when there is only one device per file, as in my drivers, the link is simply a double word 0xFFFFFFFF.

The device attributes word contains the following fields:

Reserved bits should be zero. Bit 11 has meaning only to block devices and only under MS-DOS version 3 and up. Bits 0-4 only have meaning to character devices.

Bit 4 is an oddity. It is referenced in Ray Duncan's Advanced MS-DOS as both a reserved bit and as "special CON driver bit, INT 29H." Apparently, MS-DOS uses INT 29H to output characters via the CON driver. It was not until I set the bit and put a replacement for INT 29H in my code that my console device driver would work. (A quick tour of my system via DEBUG showed that the unadulterated INT 29H simply outputs a character in AL through the TTY function (OEH) of INT 10H.)

The strategy routine is a curiosity that, according to Duncan, has no real functionality under the single-user single-tasking MS-DOS we all know, but would have some utility in a multi-user multitasking environment. Its job is to store the request header address, which is in the register pair ES:BX on an I/O request.

This request header is the means by which MS-DOS communicates with your device driver. The first 13 bytes of each request header are the same. Later bytes differ depending on the nature of the command. The common portion of the request header contains the following fields:

The command code is used by the interrupt routine to determine which command to execute. The status word is used by the interrupt routine to give back status to MS-DOS. It contains the following fields:

The error codes returned are:

Command Code Routines

MS-DOS makes the driver initialization call (command code 0) only to install the device driver after the system is booted. It is never called again. Accordingly, it is a common practice among writers of device drivers to place it physically at the end of the device driver code, where it can be abandoned. Its function is to perform any hardware initialization needed. The request header for this command code includes the following additional fields:

The BIOS parameter block array contains 2-byte offsets to BIOS parameter blocks, one for each unit supported. The BIOS parameter block describes pertinent information to MS-DOS about each unit controlled. It contains the following fields:

The media descriptor byte describes to MS-DOS what kind of media is in use. The following codes are valid for IBM-format devices:

The media-check call (command code 1) is useful for block devices only. (Character devices should simply return DONE. I will not repeat this warning for other command codes that you use with only one type of device.) MS-DOS makes this call to determine whether or not the media has been changed. The request header for this command code includes the following additional fields:

If we're using a hard disk or a RAM disk, we know that the media cannot be changed, and we always return 1. If the media descriptor byte has changed (a copy of the BIOS parameter block can be found at offset 3 into block 0 of the media, if the format is IBM), or if the volume label has changed (checked under MS-DOS version 3 and up), then we know the media has changed, and we return -1. If the media descriptor byte and the volume label match, we don't really know (how many unlabelled disks, identically formatted, do you have?), and we return 0.

The build-BIOS-parameter-block call (command code 2) is useful only to block device drivers. MS-DOS makes this call when the media has been legally changed. (Either the media check call has returned "media changed" or it returned "don't know," and there are no buffers to be written to the media.) The routine returns a BIOS parameter block describing the media. Under MS-DOS version 3 and up, it also reads the volume label and saves it. The request header for this command code includes the following additional fields:

MS-DOS performs the I/O-control-read-call (command code 3) only if the I/O-control bit is set in the device attributes word. It allows application programs to access control information from the driver (what baud rate, etc.). The request header for this command code includes the following additional fields:

The read call (command code 4) transfers data from the device to a memory buffer. If an error occurs, the handler must return an error code and report the number of bytes or blocks successfully transferred. The request header for this command code includes the following additional fields:

The non-destructive-read call (command code 5) is valid only for character devices. Its purpose is to allow MS-DOS to look ahead one character without removing the character from the input buffer. The request header for this command code includes the following additional field:

The input-status call (command code 6) is valid only for character devices. Its purpose is to tell MS-DOS whether or not there are characters in the input buffer. It does so by setting the busy bit in the returned status to indicate if the buffer is empty. An unbuffered character device should return a clear busy bit; otherwise, MS-DOS will hang up, waiting for data in a nonexistent buffer! This call uses no additional fields.

The flush-input-buffers call (command code 7) is valid only for character devices. If the device supports buffered input, it should discard the characters in the buffer. This call uses no additional fields.

The write call (command code 8) transfers data from the specified memory buffer to the device. If an error occurs, it must return an error code and report the number of bytes or blocks successfully transferred. The request header for this command code includes the following additional fields:

The write-with-verify call (command code 9) is identical to the write call, except that a read-after-write verify is performed, if possible.

The output-status call (command code 10) is used only on character devices. Its purpose is to inform MS-DOS whether the next write request will have to wait for the previous request to complete by returning the busy bit set. This call uses no additional fields.

The flush-output-buffers call (command code 11) is used only on character devices. If the output is buffered, the driver should discard the data in the buffer. This call uses no additional fields.

MS-DOS makes the I/O-control-write call (command code 12) only if the I/O-control bit is set in the device attributes word. It allows application programs to pass control information to the driver (what baud rate, etc.). The request header for this command code includes the following additional fields:

The open call (command code 13) is available only for MS-DOS version 3 and up. MS-DOS makes this call only if the open/close/removable media bit is set in the device attributes word. This call can be used to tell a character device to send an initializing control string, as to a printer. It can be used on block devices to control local buffering schemes. Note that the predefined handles for the CON, AUX, and PRN devices are always open. This call uses no additional fields.

The close call (command code 14) is available only for MS-DOS version 3 and up. MS-DOS makes this call only if the open/close/removable media bit is set in the device attributes word. This call can be used to tell a character device to send a terminating control string, as to a printer. It can be used on block devices to control local buffering schemes. Note that the predefined handles for the CON, AUX, and PRN devices are never closed. This call uses no additional fields.

The removable-media call (command code 15) is available only for MS-DOS version 3 and up, and only for block devices where the open/close/removable media bit is set in the device attributes word. If the media is removable, the function returns the busy bit set. This call uses no additional fields.

The output-until-busy call (command code 16) is available only for MS-DOS version 3 and up, and is called only if the output-until-busy bit is set in the device attributes word. It only pertains to character devices. This call is an optimization designed for use with print spoolers. It causes data to be written from the specified buffer to the device until the device is busy. It is not an error, therefore, for the driver to report back fewer bytes written than were specified. The request header for this command code includes the following fields after the standard request header:

Designing a Device Driver

Designing a device driver is a relatively simple task, since so much of the design is dictated to you. You know that you must have a strategy routine and an interrupt routine that must perform certain well-defined functions. The only real design decisions are how you choose to implement these functions. What tasks must be performed in order to implement the functions? What approaches will you use? Note that some calls only exist under MS-DOS versions 3 and up, or act differently under those versions. Will you use those calls, will you restrict yourself from using them, or (tricky, but best) will you write code that finds out the MS-DOS version and acts accordingly?

Coding the device driver is an entirely different matter, and, except maybe for debugging, the most challenging. Those of us who write C code for a living are not normally concerned with the underlying implementation of our code in machine language. We might employ some tricks we have learned about how C is typically implemented — using shifts to divide or multiply by a power of 2, for example — to get us a bit more speed, but by and large we ignore the machine interface.

In the world of the device driver, you are forced to think about what you're really doing at the machine level. If you look at my code, you'll find that I hardly ever pass parameters from one function to another. I don't use local variables. Everything's done with global variables. Look at w_putc in the console driver — it just cries out to be broken down into smaller functions. But it isn't, although it was originally written that way. The reason? You have no stack to speak of, perhaps 40 or 50 bytes. C passes parameters on the stack, two bytes for each word. C also keeps local variables on the stack, two bytes for each word again. Each function call eats up at least four bytes of stack as well. (My C compiler insists on starting every function by pushing the BP register, preparatory to building a stack frame for the local variables, whether or not there are any local variables.). All these contributions add up.

What I ended up doing was learning more assembly language then I ever meant to. In the early stages, I used the -Fc flag in my compilations to generate a merged assembly/C listing. That allowed me to examine the code that the compiler generated from the C I had written. In particular, I had to learn about how far pointers are implemented to come up with the (char far * far *) cast used in the ansi_init code to (correctly) load the INT 29 vector. I learned a few more things, too, but I will discuss those a little later.

Unfortunately, when you're working in a high-level language, you sometimes "can't get there from here." How do you get the compiler to load certain specific registers and then make a BIOS call? What statement generates a return-from-interrupt opcode? You need to preserve the machine state by pushing all the locally-used registers, and then popping them back off the stack when you're done. What function will do that? If your compiler allows in-line assembler code, great. But that's cheating, it isn't standard C. Thus the assembler interface.

I broke the assembly code for the drivers into two files, main.asm and vars.inc, plus raw.asm for the console driver and bpb.inc and rdisk.asm for the block driver. raw.asm performs functions that you just can't do in standard C. It handles all the BIOS calls, the reading and writing of I/O ports, the interrupt handlers. bpb.inc defines the standard BIOS parameter block for the RAM disk. rdisk.asm sets up the boot block, file allocation table blocks, and the first directory block, complete with clever volume label. main.asm handles the startup code. Except for the device header, it is pretty much identical for both drivers. vars.inc sets up the global variables used.

vars.inc allocates the variables because my C compiler wants to put them in a segment that gets loaded higher in memory than the program code. This behavior defeats the practice of putting the initialization code physically last and passing its own address back as the end of the driver. Also, the assembly language routines and the C routines could never agree (as I discovered by examining the code with DEBUG) as to where the variables were in memory until I put them in the assembly language .CODE segment portion.

Other Lessons

In putting the code together, I learned about a few more switches for the compiler that I had never used before, by examining the merged assembly/source files. I didn't want to use any code from the compiler's library. I had no idea what the library code did internally, and I couldn't risk putting unknown code into the drivers. Nor could I afford the additional stack usage. Yet there were calls to a stack-checking routine in every C function. Fortunately, there is a command-line switch to disable such stack probes.

A more serious problem was that my C code was incorrectly pulling fields out of the request header, which I had set up as a structure. The problem was that the compiler aligns the structure fields on int boundaries to minimize access time to the fields. Unfortunately, I don't have access to the source code for MS-DOS to make its request header similarly aligned. I did discover, however, that there is yet another command-line switch to force tight packing of structures.

One final trick I had to play was to fool the linker into not loading the C library functions. Even though no reference is made in the source code, the compiler adds to the object file a reference to a routine called _acrtused. As it turns out, this is the startup code that processes the DOS command line, initializes the data area for memory allocation, and calls main. I could not get rid of the references in the C object, so I named the interrupt routine in main.asm _acrtused and made it a public name.

Creating the final executable was simple. Using the Microsoft linker, I simply made sure that main.obj is the first file in the command line and that init.obj is the last. Object modules are linked together in the order they are found in the command line. The linker complains of no stack segment, as I expected, but this is a warning, not an error. Finally, the executable main.exe is converted to main.bin by exe2bin. The file is now ready for calling in your config.sys file.

Debugging the device driver is not simple. In its final form, it is ill-suited for standard debugging tools. Its first bytes, containing the link to the next device in the driver, are not executable. I found that the best way to debug the driver was to test each of the interrupt functions as they were written, attaching stubs to them for testing. Once each of the functions was debugged, I was ready to tie them into the main.asm interrupt routine.

As Duncan recommends, I copied the test version onto a floppy and booted from there. For the first three evenings of test, everything I did gave the same result: the drive would be accessed, then everything would get real quiet, with the A: drive light shining steadily. Finally, as I explained earlier, I looked at the code with DEBUG and discovered the discrepancies between where the strategy routine was placing the pointer to the request header and where the C routines were looking for it. That problem solved, I booted successfully, and the drivers tested out to my specs.

I am deeply indebted to the following sources of knowledge while producing this article: Ray Duncan's Advanced MS-DOS and Peter Norton's The Programmer's Guide to the IBM PC. These volumes are an indispensable part of my library, and in great danger of falling apart from use.

Listing 1

Listing 2

Listing 3

Listing 4

Listing 5

Listing 6

Listing 7

Listing 8

December 1990/Writing MS-DOS Device Drivers/Listing 1

Listing 1 (block.c) Main Interrupt Routine

#include   <dos.h>
#include   "block.h"

/*
 * normalize()
 *
 * normalize() guarantees that the offset portion of a far
 * pointer is as small as possible. A complete 20-bit address on
 * the processor can be calculated as
 *
 *     (segment * 16) + offset
 *
 * thus, the offset can be kept to a value between 0 and 15. I
 * use the FP_SEG and FP_OFF macro's in Microsoft's dos.h to
 * manipulate the segment and offset of the far pointer. If your
 * compiler doesn't support such a facility, see the  _rawscroll
 * routine in RAW.ASM, where I do it in assembly language.
 *
 * The whole point of this is to allow a lot of pointer
 * incrementing, using just the offset, without worrying about
 * wrapping around.
 */

static void normalize(p)
int far     **p;
   {
   offset       = FP_OFF(*p);
   FP_SEG(*p)   = FP_SEG(*p) + (offset >> 4);
   FP_OFF(*p)   = offset & 017;
   }

/*
 * interrupt()
 *
 * interrupt() takes care of the commands as they come in from
 * the request header. Because of the size of the RAM disk
 * buffer, the driver initialization could not be appended to the
 * back of the driver, and is in-line like everything else.
 */

void    interrupt()
   {
   command      = rh->command;
   start        = rh->b18.io.start;
   count        = rh->b18.io.count;
   transfer     = (int far *) rh->b14.transfer;
   switch (command)
       {
       case    0:       /* driver initialization */
          source            = ram_disk;
          FP_SEG(source)    = FP_SEG(source) + 0x1000;
          normalize(&source);
          rh->b14.transfer  = (char far *) source;
          rh->b18.bpb       = bpb_tab;
          rh->data          = 1;
          rh->status        = DONE;
          break;
       case    1:       /* media check */
          rh->b14.media_change_code   = 1;     /* disk has
                                 * not been changed */
          rh->status      = DONE;
          break;
       case    2:      /* build parameter block */
          rh->b18.bpb = &bpb;
          break;
       case    4:      /*  read */
       case    8:      /*  write */
       case    9:      /*  write with verify */

          If (start > MAX_BLK  ½½ count > MAX_BLK ½½
              start + count > MAX_BLK)
              {
              rh->status = BLK_NOT_FOUND ½ ERROR;
              break;
              }
          If (command == 4)
              {
              source = ram_disk;
              normalize(&source);
              source += (BLK_SIZE / sizeof(int)) * start;
              dest   = transfer;
              }
          else
              {
              source = transfer;
              dest   = ram_disk;
              normalize(&dest);
              dest   += (BLK_SIZE / sizeof(int)) * start;
              }
          normalize(&dest);
          normalize(&source);
          for (k1 = 0; k1 < count; k1++)
              for (k2 = 0; k2 < BLK_SIZE / sizeof(int); k2++)
                  *dest++ = *source++;
          rh->status = DONE;
          break;
       case    15:    /*  removable media check */
          rh->status = DONE | BUSY;
          break;
       case    5:     /*  non-destructive read */
       case    6:     /*  input status */
       case    7:     /*  flush input buffers */
       case    10:    /*  output status */
       case    11:    /*  flush output buffers */
       case    13:    /*  device open */
       case    14:    /*  device done */
          rh->status = DONE;
          break;
       case    3:     /*  ioctl read */
       case    12:    /*  ioctl write */
       default;
          rh->status = UNKNOWN_COMMAND | ERROR | DONE;
          break;
       }
    }
December 1990/Writing MS-DOS Device Drivers/Listing 2

Listing 2 (block.h) Common References for Block Device Driver

/*
 * status bits for the return code
 */

#define UNKNOWN_COMMAND 3
#define ERROR       0x8000
#define DONE        0x0100
#define BUSY        0x0200
#define BLK_NOT_FOUND 8

#define MAX_BLK     256 /*  256 blocks       */
#define BLK_SIZE    256 /*  256 bytes/block  */
/*------------- global variables -------------*/

              /* the transfer address specified in
                 the request header */
extern int far *transfer;
              /* the count specified in the request header */
extern int count;
              /* counter */
extern int k1;
              /* counter */
extern int k2;
              /* offset for normalization */
extern unsigned offset;
              /* source pointer */
extern int far *source;
              /* destination pointer */
extern int far *dest;
              /* command specified in request header */
extern char command;
              /* start block specified in request header */
extern int start;

extern  struct parm_block    /* parameter block   */
   {
   unsigned  bps;        /* bytes per block                   */
   char      spau;       /* blocks per allocation unit        */
   unsigned  nrs;        /* number of reserved blocks         */
   char      nfat;       /* number of file allocation tables  */
   unsigned  rent;       /* number of root directory entries  */
   unsigned  tns;        /* total number of blocks    */
   char      mdb;        /* media descriptor byte     */
   unsigned  nsfat;      /* number of blocks per FAT  */
   }   bpb,
       bpb_tab [ ];

/*
 * pointer to the request header
 */

extern struct   request_header
   {
   char         rlength;
   char         unit;
   char         command;
   unsigned     status;
   char         reserved   [ 8 ];
   char         data;
   union
       {
       char far        *transfer;
       char            media_change_code;
       }   b14;
   union
       {
       struct parm_block far   *bpb;
       struct
          {
          unsigned        count;
          unsigned        start;
          } io;
       }   b18;
   } far    *rh;

extern int ram_disk[ ];
December 1990/Writing MS-DOS Device Drivers/Listing 3

Listing 3 (char.c) Main Interrupt Routine; also Keyboard Read Routine

#include     "char.h"

/*-------- Prototypes ---------*/

              /* handle init call */
extern void char_init (void);
              /* look up key code for reassignment */
extern char *k_seek (void);
              /* read the keyboard */
extern void rawread (void);
              /* see if char is available at the keyboard */
extern int rawstat (void);
              /* write byte into ring buffer */
extern void r_write (char);
              /* write character to screen */
extern void w_putc (void);

/*
 * rd_getc()
 *
 * rd_getc() reads a character from the keyboard, hanging until
 * there is one. If the character has been reassigned, copy the
 * reassignment buffer into the ring buffer. Otherwise, write
 * the character itself (with leading nul byte for extended
 * keys) into the ring buffer
 */

void   rd_getc()
   {
   if (r_index == w_index)
      {
      rawread();
      if (k_seek())
         {
         for (k = 0; k <*len; k++)
             r_write(*ptr++);
         }
      else
         {
         if (keycheck & 0177400)
             r_write(0);
         r_write(((char) keycheck) & 0000377);
         }
      }
   }

/*
 * interrupt()
 *
 * interrupt() takes care of the commands as they come in from
 * the request header. Of all the commands, only the device
 * initialization call is a separate function; this reduces
 * stack overhead. char_init() is a separate function, alone in
 * its own module, so that it can report its own address as the
 * end of the driver.
 */

void  interrupt()
   {
   count = rh->count;
   transfer = rh->transfer;
   switch (rh->command)
      {
      case   0:    /* initialization */
         char_init();
         break;
      case   4:    /* read */
         while (count)
            {
            rd_getc();
            *transfer++ = r_buf[ r_index++ ] & 000377;
            r_index &= RLIMIT;
            count--;
            }
         rh->status = DONE;
         break;
      case   5:      /* non-destr uctive read */
         if (r_index == w_index)
            {
            if (!rawstat())
                {
                rh->status = BUSY | DONE;
                break;
                }
            rd_getc();
            }
         rh->status = DONE;
         rh->data = r_buf[ r_index ];
         break;
      case   7:    /* flush input buffers */
         r_index = w_index = 0;
         while (rawstat())
            rawread();
         rh->status = DONE;
         break;
      case   8:      /* write */
      case   9:      /* write with verify */
         while (count)
            {
            outchar = *transfer++;
            w_putc();
            count--;
            }
      case   1:     /* media check */
      case   2:     /* build parameter block */
      case   6:     /* input status */
      case   10:    /* output status */
      case   11:    /* flush output buffers */
         rh->status = DONE;
         break;
      case   3:      /* ioctl read */
      default:
         rh->status = UNKNOWN_COMMAND | ERROR | DONE;
         break;
      }
   }
December 1990/Writing MS-DOS Device Drivers/Listing 4

Listing 4 (init.c) Code for Initializing the Device Driver

#include   "char.h"

extern void int29  (void);

void  char_init()
   {
   *((char far * far *) 0x0000A4) = (char far *) int29;
   rh->transfer = (char far *) char_init;
   rh->status = DONE;
   }
December 1990/Writing MS-DOS Device Drivers/Listing 5

Listing 5 (key.c) Routines for Manipulating the Key Reassignment Buffers

#include  "char.h"
/*
 * k_seek()
 *
 * k_ seek() finds a buffer based on the global variable
 * 'keycheck'. the first match returns a pointer to the
 * replacement string; the variable 'len' is also set to
 * point to the length field. If no match, then it returns
 * a null pointer
 */

char    *k_seek()
   {
   for (kp = &kbuffer[ 0 ], k = 0; k < NKEYS; k++, kp++)
       {
       if (kp->keystroke == keycheck)
          {
          len = &(kp->length);
          ptr = kp->buffer;
          return ptr;
          }
      }
   return ((char *) 0);
   }

/*
 * k_alloc()
 *
 * k_alloc() searches for an unallocated key buffer.
 * It does so by searching for a zero keystroke field.
 * Simple.
 */

char   *k_alloc()
   {
   keycheck = 0;
   return k_seek();
   }
December 1990/Writing MS-DOS Device Drivers/Listing 6

Listing 6 (ring.c) Ring Buffer Routines

#include  "char.h"

/*
 * r_write()
 *
 * r_write() puts a byte in the buffer. when is the buffer full?
 * when writing 1 more byte would set the read and write indices
 * equal to each other (which means the buffer is empty!!). does
 * nothing but return if it can't write the byte without
 * overflowing the buffer... if this was a real multi-tasking
 * system, we could sleep until somebody reads a byte, which
 * would allow us to do our write, but it isn't, so...
 */

void   r_write(c)
char   c;
   {
   if (((w_index + 1) & RLIMIT) == r_index)
       return;
   r_buf[ w_index++ ] = c;
   w_index &= RLIMIT;      /* wrap the index around */
   }

/*
 * r_puti()
 *
 * r_puti() converts a small (0 - 99) decimal number into two
 * ASCII digits and put them in the ring buffer
 */

void   r_puti(c)
char   c;
   {
   r_write((c / 10) + '0');
   r_write((c % 10) + '0');
   }
December 1990/Writing MS-DOS Device Drivers/Listing 7

Listing 7 (write.c) Routines Used to Write to the Screen

#includde   "char.h"

/*--------- external function prototypes: ---------*/

             /* look for unused key buffer */
extern char *k_alloc (void);
             /* look for key buffer */
extern char *k_seek (void);
             /* write decimal integer to ring buffer */
extern void r_puti (char);
             /* write byte to ring buffer */
extern void r_write (char);
             /* clear selected part of the screen */
extern void rawclear (void);
             /* set the video mode */
extern void rawmode (void);
             /* move the cursor */
extern void rawmv (void);
             /* scroll the screen up */
extern void rawscroll (void);
             /* output character as raw tty */
extern void rawtty (void);
             /* output character to screen */
extern void rawwrite (void);

/*
 * delimiters used for quoted characters as
 * parameters of escape sequences
 */

#define DELIM1 '\"
#define DELIM2 '"'

/*
 * characters that require special handling
 */

#define BEL '\007'
#define BS  '\010'
#define NL  '\012'
#define CR  '\015'
#define ESC '\033'

/*
 * color codes
 */

#define BLUE    (01)
#define GREEN   (02)
#define RED (04)
#define CYAN    (BLUE ½ GREEN)
#define MAGENTA (BLUE ½ RED)
#define YELLOW  (GREEN ½ RED)
#clefine WHITE  (BLUE ½ GREEN ½ RED)

/*
 * macro's for turning on and off attributes or designating
 * a color as foreground or background
 */

#define ON(x)    (attrib ½= (char) (x))
#define OFF(x)   (attrib &= (char) (~(x)))
#define FORE(x)  (attrib ½= (char) (x))
#define BACK(x)  (attrib ½= (char) ((x) << 4))

/*
 * we don't want to use the standard 'c' isdigit(); it's
 * either implemented as a function (we don't want to use
 * osmebody else's functions that might have unpleasant side
 * effects) or a macro invoking an array of values that
 * dictate what lexical properties a given character possesses
 * (a waste of precious memory)
 */

#define isdigit(x) (((x) >= '0') && ((x) <= '9'))

/*
 * w_write()
 *
 * w_write() keeps track of actually getting stuff on the screen
 * and moving the cursor around
 */

void   w_write()
   {
   switch (outchar)
      {
      case     CR:

          /* just set the column to 0 for a carriage return */

          curr.loc.col = 0;

          /* and fall through to the backspace handler */

      case    BS:

          /* decrement the current column unless at the left
           * margin */

          if (curr.loc.col)
             --curr.loc.col;

          /* move the cursor and that's it... */

          rawmv();
          break;

      default:

          /* first, write the character without
             moving the cursor */

          rawwrite();

          /* then, if we're not on the right margin, bump the
           * cursor right and that's it */

          if ((curr.loc.col + 1) <= max.loc.col)
              {
              ++curr.loc.col;
              rawrmv();
              break;
              }

          /* but if we were at the right margin, check the wrap
           * flag; if it's clear, just return. if not, execute
           * a carriage return (just set the column to zero -
           * we'll do a rawmv() call later), set the current
           * character to newline, and fall into the newline
           * routine */

          if (!wrap)
              break;
          curr.loc.col = 0;
          outchar = NL;
      case     NL:

          /* if we're not at the bottom of the screen, just bump
           * the line down and call rawmv() */

          if (++curr.loc.line < 25)
              {
              rawmv();
              break;
              }

          /* but if we were at the bottom (or somehow below!),
           * make sure we're on the bottom line. If we're in
           * one of the CGA 80x25 text modes, do our fancy
           * assembly language scroll routine, else just let
           * the BIOS handle it */

          curr.loc.line = 24;
          if (video_mode == 2 || video_mode == 3)
              {
              rawscroll();
              break;
              }

      case     BEL:

          /* do a raw tty output; it handles the cursor movement
           * too */

          rawtty();
          break;
      }
   }

/*
 * w_buffer()
 *
 * w_buffer() writes a byte into the escape buffer. silently
 * overwrites the last byte in the buffer if we get that far. it
 * was either that or trash the new byte
 */

void  w_buffer(c)
char  c;
   {
   if (char_cnt == BUF_LEN)
      esc_buf[ BUF_LEN - 1 ] = c;
   else
      esc_buf[ char_cnt++ ] = c;
   }
/*
 * w_cursor()
 *
 * w_cursor() handles the cursor left, right, up and down
 * functions. bumps the value by the value of the 1st parameter
 * in the escape sequence (if there isn't one, we put a 1 in for
 * it) in the direction specified by the delta, until it hits the
 * specified limit. then execute the cursor move...
 */

void   w_cursor()
   {
   if (!char_cnt)
      esc_buf[ 0 ] = 1;
   while (*cur_val != limit)
      {
      *cur_val += delta;
      esc_buf[ 0 ]--;
      if (!esc_buf[ 0 ])
          break;
      }
   rawmv();
   }

/*

 * w_putc()
 *
 * w_putc() updates the parameters that might have changed since
 * last time, then runs the character through the escape sequence
 * state machine
 */

void   w_putc()
   {

   /* update parameters */

   max.loc.col = SCREEN_WIDTH - 1;
   cur_page = CURRENT_PAGE;
   curr.position = (PAGE_TABLE [ cur_page ]).position;
   if (curr.loc.col > max.loc.col)
      curr.loc.col = max.loc.col;
   if ((video_mode = CURRENT_MODE) == 7)
      video_address = MONOCHROME + SCREEN_OFFSET;
   else
      video_address = GRAPHIC + SCREEN_OFFSET;

   /* process the escape sequence state */

   switch (state)
      {

      case    HAVE_ESC:
          /* if we have an escape, we want a left bracket.
           * if we get it, change the state and return,
           * else reset back to the RAW state and fall
           * through */

          if (outchar == '[')
              {
              state = HAVE_LBRACE;
              break;
              }
          state = RAW;

      case     RAW:

          /* if it's an escape, change the stae, else output the
           * character */

          if (outchar == ESC)
              state = HAVE_ESC;
          else
              w_write();
          break;

      case     IN_NUMBER:

          /* if it's another digit, roll it into the value. else
           * the state falls back to HAVE_LBRACE, and we fall
           * through */

          if (isdigit(outchar))
              {
              tmp.value *= 10;
              tmp.value += outchar - '0';
              break;
              }
          else
              {
              state = HAVE_LBRACE;
              w_buffer(tmp.value);
              }

      case     HAVE_LBRACE:

          /* if we have a string delimiter, change the state and
           * save the delimiter */
             if (outchar == DELIM1 || outchar == DELIM2)
                {
                state = IN_STRING;
                tmp.delim = outchar;
                break;
                }

             /* else if it's 'punctuation', ignore it */

             if (outchar == ';' || outchar == '=' ||
                       outchar == '?')
                break;

             /* else if it's a digit, start a number and
                change the state */

             if (isdigit(outchar))
                {
                state = IN_NUMBER;
                tmp.value = outchar - '0';
                break;
                }

             /* else it terminates the escape sequence, and
              * identifies its purpose */

             switch (outchar)
                {

                case     'A':           /* cursor up */
                    limit = 0;
                    delta = (char) -1;
                    cur_val = &curr.loc.line;
                    w_cursor();
                    break;
                case     'B':           /* cursor down */
                    limit = 24;
                    delta = 1;
                    cur_val= &curr.loc.line;
                    w_cursor();
                    break;

                case     'C':           /* cursor right */
                    limit = max.loc.col;
                    delta = 1;
                    cur_val = &curr.loc.col;
                    w_cursor();
                    break;

                case     'D':           /* cursor left */
                    limit = 0;
                    delta = (char) -1;
                    cur_val = &curr.loc.col;
                    w_cursor();
                    break;

                case     'H':
                case     'R':
                case     'f':

                    /* set cursor position: make sure there
                     * are at least 2 parameters stored,
                     * correct any out-of-range parameters,
                     * and execute the move. if the
                     * character was 'R', fall through into
                     * the report position sequence */

                    switch (char_cnt)
                       {
                       case  0:
                           w_buffer(1);
                       case    1:
                           w_buffer(1);
                       default:
                           break;

                    /* set graphic rendition - just do all
                     * the parameters and set/reset the
                     * appropriate bits in the attribute
                     * byte */

                    while (char_cnt)
                       {
                       switch (esc_buff[ --char_cnt ])
                           {
                           case    0:
                              attrib = 0007; break;
                           case  1:
                              ON(010); break;
                           case    4:
                              OFF(07); ON(01); break;
                           case    5:
                              ON(0200); break;
                           case    7:
                              OFF(07); 0N(0160); break;
                           case    8:
                              0FF(0167); break;
                           case    30:
                              OFF(07);  break;
                           case    31:
                              OFF(07); FORE(RED); break;
                           case    32:
                              OFF(07); FORE(GREEN); break;
                           case    33:
                              OFF(07); FORE(YELLOW); break;
                           case    34:
                              OFF(07); FORE(BLUE); break;
                           case    35:
                              OFF(07); FORE(MAGENTA); break;
                           case    36:
                              OFF(07); FORE(CYAN); break;
                           case    37:
                              OFF(07); FORE(WHITE); break;
                           case    40:
                              OFF(0160);  break;
                           case    41:
                              OFF(0160); BACK(RED); break;
                           case    42:
                              OFF(0160); BACK(GREEN); break;
                           case    43:
                              OFF(0160); BACK(YELLOW); break;
                           case    44:
                              OFF(0160); BACK(BLUE); break;
                           case    45:
                              OFF(0160); BACK(MAGENTA); break;
                           case    46:
                              OFF(0160); BACK(CYAN); break;
                           case    47:
                              OFF(0160); BACK(WHITE); break;
                           default:
                              break;
                           }
                        }
                    break;
                case     'p':
                    if (esc_buf[ 0 ])
                        {

      /* if the first parameter is not nul, then we're
         redefining a 'normal' key. Clear the msb of
         keyc1heck to indicate this */
    
                        keycheck = (esc_buf[ 0 ]) &
                           0000377;

      /* check first to see if we've already allocated
         a buffer to this key; then if not, see if we have
         an unused buffer to hand out */

                        if (k_seek() ½½ k_alloc()))
                           }

                        if (!esc_buf[ 0 ])
                           curr.loc.line = 1;
                        else if (esc_buf[ 0 ] > 25)
                           curr.loc.line = 25;
                        else
                           curr.loc.line = esc_buf[ 0 ];

                        if (!esc_buf[ 1 ])
                           curr.loc.col = 1;
                        else if (esc_buf[ 1 ] > max.loc.col + 1)
                           curr.loc.col = max.loc.col + 1;
                        else
                           curr.loc.col = esc_buf[ 1 ];

                        curr.loc.line--;
                        curr.loc.col--;
                        rawmv();

                        if (outchar != 'R')
                           break;

                    case     'n':
                        /* output the position; format is
                         * "\033[%.2d;%.2dR\015" */

                        r_write(ESC);
                        r_write('[');
                        r_puti(curr.loc.line + 1);
                        r_write(';');
                        r_puti(curr.loc.col + 1);
                        r_write('R');
                        r_write(CR);
                        break;

                    case     'J':
                        /* rawclear clears the screen from
                         * (curr.loc.line, curr.loc.col) to
                         * (max.loc.line, max.loc.col); so for
                         * clear screen, set the current
                         * position to the upper left hand
                         * corner of the screen, and the max
                         * line to the bottom of the screen */

                        curr.loc.line = curr.loc.col = 0;
                        max.loc.line = 24;
                        rawclear();
                        break;

                    case     'K':
                        /* and clear to end of line is even
                         * simpler - just set the max line equal
                         * to the same line we're on */

                        max.loc.line = curr.loc.line;
                        rawclear();
                        break;

                    case     'h':
                    case     'l':
                        /* set and reset mode do the same thing
                         * unless the mode is 7. easy */

                        if (!char_cnt)
                           w_buffer(2);
                        if (esc_buf[ 0 ] > 7)
                           break;
                        if (esc_buf[ 0 ] == 7)
                           wrap = (char) (outchar == 'h');
                        else
                           rawmode();
                        break;

                    case     'm':
                           {
                           k = 1;
                           kp->keystroke =
                              (esc_buf[ 0 ])
                              & 0000377;
                           }
                        else
                           break;
                        }
                    else
                        {

      /* first byte was nul - an extended key. indicate
         by setting msb of keycheck to $FF */

                        keycheck = (esc_buf[ 1 ]) |
                           0177400;
                        if (k_seek() ½½ k_alloc())
                           {
                           k = 2;
                           kp->keystroke =
                              (esc_buf[ 1 ])
                              ½ 0177400; 
                           }
                        else
                           break;
                        }55

      /* copy the parameters into the buffer, counting as we go */

                        for (*len= 0; (k < char_cnt) &&
                           (k < KEY_BUFLEN); ++*len)
                           *ptr++ = esc_buf[ k++ ];

                        break;

                    case     's':

                        /* save current position */

                        saved.position = curr.position;
                        break;

                    case     'u':

                        /* restore current position */

                        curr.position = saved.position;
                        rawmv();
                        break;

                    default:

                        /* anything else? discard the parameters
                         * and output the final character to the
                         * screen */

                        w_write();
                        break;
                    }

                /* finally, clear the buffer by resetting the count
                   and fall back to the RAW state */

                char_cnt = 0;
                state = RAW;
                break;

            case    IN_STRING:

                /* finally, the IN_STRING case - if the character
                 * isn't the delimiter we saved, then put it into
                 * the buffer as it was received */

                if (outchar == tmp.delim)
                        state = HAVE_LBRACE;
                    else
                        w_buffer(outchar);
                    break;
                }
            }
December 1990/Writing MS-DOS Device Drivers/Listing 8

Listing 8 (char.h) Common Reference for Character Device Driver

/* This is used for the escape buffer. This is how many
 * bytes of parameters the escape() routine can save in one
 * sequence. Tune it as you see fit. It needs to be at
 * least long enough to hold the two bytes of an extended
 * key (such as F1), plus the replacement string: */

#define BUF_LEN     80

/* length of the definition field; tune as you see fit.
 * How long a string do you want? */

#define KEY_BUFLEN  21

/* number of re-assignments you can define;
 * tune as you see fit. Don't use it much?
 * make it less. Redefining the entire keyboard?
 * then make it more */

#define NKEYS       20
/*
 * parameters for the ring buffer. If you want to
 * change the size, just change RLOG - the math demands
 * that the size of the buffer be a power of 2. Makes
 * things nice and efficient that way.
 */

#define RLOG    6
#define RLEN    (2 << (RLOG - 1))
#define RLIMIT  (RLEN - 1)

/*
 * macros for reading the system RAM where these neat
 * things are stored
 */

#define CURRENT_MODE   (*(char far *)0x0449)
#define SCREEN_WIDTH   (*(char far *)0x044A)
#define SCREEN_OFFSET  (*(unsigned far *)0x044E)
#define PAGE_TABLE ((POSITION far *)0x0450)
#define CURRENT_PAGE   (*(char far *)0x0462)

/*
 * base addresses for the video memory for the
 * monochrome adaptor (MONOCHROME) and the CGA (GRAPHIC)
 */

#define MONOCHROME  ((char far *)  0x000B0000)
#define GRAPHIC     ((char far *)  0x000B8000)

/*
 * status bits for the return code
 */
 
#define UNKNOWN_COMMAND 03
#define ERROR       0x8000
#define DONE        0x0100
#define BUSY        0x0200

/*
 * the states of the escape sequence:
 *
 * RAW:  no escape sequence begun yet, or previous
 *       sequence has been terminated
 *
 * HAVE_ESC:  an escape has been received; now awaiting

 *            the left bracket
 *
 * HAVE_LBRACE:  an escape followed by a left bracket
 *               have been received; now waiting for a
 *               parameter or terminating character
 *
 * IN_STRING:  a parameter beginning with a delimiter has
 *             been started; until the same delimiter is
 *             received, characters will be placed in the
 *             escape buffer as is
 *
 * IN_NUMBER:  a numeric parameter has been started; each
 *             subsequent digit is 'added' to the number
 *             in the escape buffer
 */

#define RAW    0
#define HAVE_ESC     1
#define HAVE_LBRACE  2
#define IN_STRING    3
#define IN_NUMBER    4

/* typedef for cursor positioning; this union reflects the way
 * that they are stored internally. At the assembly language
 * level, 16-bit registers can be loaded directly with the
 * 16-bit position so that the high and low halves are
 * correctly loaded for BIOS calls
 */

typedef union
    {
    short  position;
    struct
        {
        char  col;
        char  line;
        } loc;
    } POSITION;
/*
 * typedef for the reassignment buffer. keystroke is the key
 * being replaced; length is the number of bytes that the
 * keystroke 'generates'; buffer holds the data that replaces
 * the keystroke.
 */

typedef struct
    {
    int keystroke;
    char  length;
    char  buffer [ KEY_BUFLEN ];
    } KEY;


/*----------- global variables -----------*/

                 /* the current character being output */
extern char outchar;
                 /* the current video mode */
extern char video_mode;
                 /* the current screen attribute */
extern char attrib;
                 /* a count of how many parameter bytes
                  * have been read into the escape buffer
                  */
extern char char_cnt;
                 /* the code being checked for in
                  * reassignment routines; if null, it is
                  * used to find an ununsed buffer; if
                  * the value is non zero and positive,
                  * it is used to look up a regular key;
                  * if non-zero and negative, it is used
                  * to look up an extended key */
extern int keycheck;
                 /* the parameter buffer for ansi escape
                  * sequences */
extern char esc_buf [ BUF_LEN ];
                 /* the current position */
extern POSITION curr;
                 /* the maximum position. Actually, the
                  * maximum line number is not used as
                  * a maximum, but is used simply to
                  * tell the clear-screen and
                  * clear-to-end-of-line code how much screen to
                  * clear */
extern POSITION max;
                 /* the current video page number */
extern char cur_page;
                 /* the current video address. this is
                  * the base address plus the offset to
                  * the current page */
extern char far *video_address;
                 /* the transfer address specified in the
                  * request header */
extern char far *transfer;
                 /* the count specified in the request
                  * header */
extern int count;

/*
 * pointer to the request headerd
 */

extern struct
    {
    char      rlength;
    char      units;
    char      command;
    unsigned  status;
    char      reserved [ 8 ];
    char      data;
    char far  *transfer;
    unsigned  count;
    } far   *rh;
extern int k;          /* generic int */

                   /* pointer to the length field
                    * for the selected buffer */
extern char *len;
                   /* pointer to the definition
                    * field for the selected buffer */
extern char *ptr;

extern KEY kbuffer[NKEYS];  /* the buffers */
extern KEY *kp;         /* pointer to a buffer */

                    /* r_buf[ r_index ] is the
                     * next byte to be read  */
extern unsigned r_index;
                    /* r_buf[ w_index ] is where
                     * the next-byte can be written */
extern unsigned w_index;

extern char r_buf [ RLEN ];     /* ring buffer */

/*
 * a temporary variable for storing either the delimiter while
 * the escape sequence is in the IN_STRING state, or the value
 * being computed if the escape sequence is in the IN_NUMBER
 * state. a convenient way of doubling the utility of a byte
 * of storage while keeping track of just what we're doing
 * with it.
 */

extern union
    {
    char  delim;
    char  value;
    }     tmp;
extern char    state;      /* the escape sequence state */
extern POSITION    saved;  /* the saved cursor position */
extern char  wrap;         /* wrap flag: wrap on if set */
extern char  *cur_val;     /* line or column parameter being
                         manipulated by w_cursor() */
extern char  delta;        /* incr/decr to cur_val */
extern char  limit;        /* limit of *cur_val */

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.