ACCESSING HARDWARE FROM 80386 PROTECTED MODE: PART II

A 4-gigabyte memory model and features that control 80386 paging make FAR pointers obsolete

Stephen Fried

Stephen is the vice president of MicroWay's R&D. He is well known in the field for his PC numeric and HF chemical laser contributions. You can reach him at MicroWay Inc., P.O. Box 79, Kingston, MA 02364.


Last month, in Part I, we saw that with a few tricks such as tiling a huge model, and the use of FAR pointers we could address up to 64 terabytes. However, I hope to convince you by the end of this article that the only use for FAR pointers in 80386 code is in operating system kernels. As a starting point, let's take a look at ports and interrupts.

The 80386 supports ports and interrupts in the same manner as the 8086, with the exception that interrupt vectors are only stored in the first 1024 bytes in real mode, while port reads and writes can result in exceptions if the user's code does not have the required protection level. From a practical standpoint, what this means in 32-bit protected mode is that the processor looks into the interrupt descriptor table (IDT) instead of the first kilobyte anytime an interrupt happens.

These two features make it possible for an operating system or environment to control what happens when a protected-mode program hits either an interrupt or attempts to write a port. This type of facility makes it possible to run DOS applications in "compatibility boxes" that simulate MS-DOS. In the case of DOS extenders, what happens during I/O operations is that port reads and writes are passed through, while interrupts get translated into MS-DOS compatible operations. These MS-DOS compatible operations are performed by DOS itself, which is activated by returning to real mode and examining a buffer area in the first 640K that has been left behind by the protected-mode portion of the DOS extender.

Interrupts

To place interrupts in context, let's examine how to write a short sequence that writes to the system diskette using the ROM BIOS INT 13H entry point. Digging out an eight-year-old copy of the IBM technical user's manual, we examine the parameters that have to be passed to the routine in the registers al, ah, cl, ch, dl, dh, and es: bx. Figure 1 shows the equivalent 8086 and 80386 versions of a section of a program that writes eight sectors at a time to a diskette.

Figure 1: 8086 and 80386 versions of a program fragment that write eight sectors at a time to a diskette

Real mode             32-bit protected mode
----------------------------------------------------------------

mov     ax,ds         mov     ax,ds          ; set up es:bx
mov     es,ax         mov     es,ax          ; es = ds
mov     bx,buffer     mov     ebx,buffer     ; point at buffer
mov     al,8          mov     al,8           ; # of sectors
mov     ah,3          mov     ah,3           ; write to diskette
mov     cl,8          mov     cl,8           ; sector # 1
mov     ch,39         mov     ch,39          ; track number 39
mov     dl,2          mov     dl,2           ; drive # 2
mov     dh,1          mov     dh,1           ; head #1
int     13H           int     13H            ; call the ROM BIOS

Of course, if we were writing this code in C, we would have used the NDP C version of int86 or int386, or the register-aliased variables introduced later, in conjunction with an asm statement. We chose the diskette routine as an example because it requires that a pointer to a buffer is passed to the ROM BIOS. Examining the code, we discover that both pieces of code are identical, except for the buffer pointers. In the protected-mode example, we use the pointer es:ebx instead of es:bx. To understand how the interface becomes 32-bit knowledgeable, it is necessary to examine what happens when an int 13H executes in protected mode.

The DOS extender has set up the IDT, so that when an int 13H occurs, the DOS extender gets interrupted and takes over. The extender first looks at the interrupt number, and if it employs a pointer of the type es:bx, it enters a special mode in which it moves the data pointed to by es:ebx into a buffer area it has reserved for itself in real memory. This makes it possible to locate our protected-mode track buffer anywhere in the 32-bit segment being addressed by ebx, and to use buffers larger than 64K. The extender then copies the first 64K from the protected buffer into the real buffer, sets es:bx so that it points at the real buffer, restores all the other registers so that they were the same as when the interrupt was invoked in protected mode, enters real mode and, finally, issues an int 13H to invoke the ROM BIOS diskette routine.

This technique works fine for buffers that are up to 64K in size. For buffers larger than 64K, the extender is forced to make multiple transfers. As a result, there is little benefit in using buffers larger than 64K when calling either a ROM BIOS or MS-DOS entry point from protected mode. In the process of benchmarking the NDP C and Fortran I/O run-time systems, we discovered that MS-DOS is the primary I/O bottleneck, and that a buffer size of 8-Kbytes gave excellent performance.

FAR Pointers

Two years ago, when we introduced NDP Fortran, the block move technique just described was the main technique used for accessing real mode memory from protected mode. We made this facility available to users with functions whose arguments included the selector of the destination segment. This worked fine, just as long as the memory being accessed was recognized and had a selector set up for it by Phar Lap. However, as our users became more sophisticated, this technique started to break down. More and more users wanted to access new memory-mapped gadgets no one had ever heard of before.

At about the same time, a number of people started talking about adopting FAR pointers to the 80386, but when we examined the problem we discovered that adding more segments was not a solution because it did not address the problem of how to create new segments to map in new devices.

About this time, Phar Lap added some calls that made it possible to control system paging. With these calls in hand, we developed a routine that enabled us to extend the size of a user's data segment while simultaneously mapping this new extension to any arbitrary, physical address.

To gain some perspective, imagine you have just bought a digitizer which is memory mapped and has a resolution of 1000 x 1000 pixels (that is, it takes up a megabyte of physical address space in your 386 AT's I/O channel). The ideal way to obtain access to this device is to pass its size and physical location to the operating system and to get back a 32-bit pointer to the device in the address space of the program. This is exactly what mapdev( ) does. The pointer that mapdev( ) returns is an ordinary 32-bit pointer to a piece of your program's data segment that was just magically created. It doesn't require the use of intrasegment transfers or FAR pointers, and you don't have to worry about using a block move to access the device or memory that was mapped in.

Another benefit of this technique is that it works up to 20 percent faster in an 80486 system than techniques that use intrasegment methods. The reason is that the 80486 has a preferred segment register, ds, for accessing data, and all NDP data accesses (by default) are made by using ds.

The choice of segment registers becomes a crucial issue when it comes to Weitek. The default technique used by NDP compilers for accessing Weitek is to pass the selector 3C into the segment register fs and to access Weitek through fs. Our research to date indicates that mapping Weitek, and everything else into ds, pays off with big dividends. Altogether, it is possible to make a 15 - 50 percent improvement in 486/Weitek speed by correctly aligning and mapping 486 code.

The 80486 and mapdev( ) put the final nails in the FAR pointer coffin. The next question is, "How do you port a 16-bit C application that uses FAR pointers to 386 protected mode?" FAR pointers are used in two different ways by 16-bit C programs: To identify 20-bit pointers when the program is using 16-bit pointers by default, and to access physical locations in the 20-bit 8086 address space. The first use, increasing pointer size, does not have a corresponding feature in the small memory model employed by any of the operating systems currently in use. As a result, this type of FAR pointer gets translated by the statement define FAR

This statement converts occurrences of FAR into a null string. When FAR pointers are used to access physical devices in the 20-bit address space of the 8086, the easiest solution is to use mapdev( ) to map the device into the protected data segment and use the resulting pointer instead. For instance, consider the program in Example 1 to clear the screen.

Example 1: Using mapdev( ) to map the screen into the protected data segment

char *mapdev();
main()
{
          int jcount = 0;
          char *scr_ptr;
          /* map the screen into the data segment */
          scr_ptr = mapdev(0xb8000, 4096);
          while (jcount (4096)
          {
                        *(scr_ptr + (jcount++)) = 0x20; /* write space*/
                        *(scr_ptr + (jcount++)) = 0x7c; /* and attribute*/
          }
}

For chores such as addressing the screen, note that mapdev( ) is a much better solution than the block move we introduced earlier. It makes it possible to read or write any region of physical memory by using 32-bit pointers created by the operating system, without invoking special functions or creating buffers that have to be moved in bulk.

At this point, you should have a good feel for what memory looks like. Figure 2 shows a map of a system running in protected mode under a DOS extender without virtual memory running (the virtual map is different).

The lowest megabyte looks just like any other real map with the exception that an area of the map between the DOS extender and its I/O buffer have been made available for protected-mode code and data if paging is enabled (the default). To simplify the map, we have drawn it with paging disabled, which places the start of the protected code and data segment at the bottom of the first megabyte. Note that the code and data of the protected-mode program itself looks very similar to an ordinary small model program with the exception that this segment can grow on the top. Just above the code and data is an area identified for expansion of the address space using the mapdev function, which also causes selectors OCH and 14H to grow. Finally, at the top of the map we find the Weitek segment.

Register-Aliased Variables

At this point, we have just about introduced all the interface tricks except those that let programs running in real mode access protected-mode programs, and vice versa. These types of accesses involve writing TSRs and passing data back and forth between real and protected mode, using real mode buffers. Phar Lap does a good job of explaining how to write these types of interfaces. Because of popular demand, we have written interfaces between the NDP runtime environment and Media Cybernetics' Halo and Novell's BTrieve. However, we have still not introduced the piece de resistance of interface tricks -- register-aliased variables.

Functions such as int86, int386, outp, outpw, inp, and inpw, are fine for prototyping I/O routines that interface devices directly but, in the end, any developer that is worth their salt will choose to rewrite these routines in assembly language. The reason is clear: Functions such as int86 involve the use of bulky structures and have to pass through as many as 40 lines of code every time they are called.

In searching for a technique to speed up this operation, we chose to make two simple extensions to the language:

    1. Make it possible to specify which register a register variable is stored in.

    2. Notify the asm statement about which variables were read and written on each "call" to asm.

These two tricks make it possible to declare 8-, 16- and 32-bit C variables that are stored in particular 80386 registers. To invoke an interrupt by using register-aliased variables, use C assignments to set up the registers and then kick off the function with an asm statement that contains the interrupt being used.

As we will see, what results is pure poetry and, frequently, much better code than can be produced by an expert assembly language programmer. A person writing an assembly routine does not know the state of all the registers (that is, which are free when a procedure is called) when the routine they are writing is called. As a result, they must save and restore all registers used (something the compiler does not have to do as the compiler has a complete knowledge, at all points in the program, of the machine for which it is generating code). In addition, the assembly routine must be called, possibly getting parameters off a stack, while the register-aliased routine appears inline. And, as you know, the fastest call is no call at all.

The ideal use of register-aliased variables is in advanced graphics adapter drivers in which register ports must be accessed inline and in sequence with reads and writes to the screen buffer. The difference between inline code and prototypes that use C to access the ports is very noticeable.

A Sound Generator

The example presented shortly demonstrates the principles and provides a sound( ) function that I will use as the basis for a Basic-like music interpreter. The program in Listing One (page 122) contains a general-purpose sound routine called note( ) that can be called with a pitch and duration. The program works by controlling the 8255 timer on the system motherboard and is similar to the program in the IBM-PC ROM BIOS. The traditional Microsoft C version of the program demonstrates the principles we have developed earlier, except for mapdev( ).

The register-aliased version of the same program is shown in Listing Two (page 122). Note that we have defined a couple of macros at the head which convert outp( ) from a function to a macro, and that we have created a new form of inp( ) that is also a macro. The benefit of these macros (over the functions) is that they are almost identical in form to those used by MS C, take up much less space than function calls in the generated code, and execute inline without the costly overhead of a call and return.

In examining the code, note that the source is virtually identical to Listing One, and that the only source lines that have in fact changed are those that invoke the two interrupts and port reads. The technique used to force a variable into a particular register is a simple extension of the regkeyword, followed by the register to be used. To use an 8- or 16-bit section of a 32-bit register, use the 32-bit register name, followed by short or char, using unsigned as necessary. Variables that have the same name as the register in which they reside are called "register aliased."

It is possible, however, to overload variables, and in this demonstration I have deliberately overloaded eax, declaring three variables (al, x, and y) to be stored in al, another in ax, and another in ah. (Note that ah is specified in a slightly different manner.) None of these five eax components are active at the same time. If a conflict exists, the compiler spills temporaries to the stack, thus signaling us to change our usage, if possible.

Listing Three (page 122) is the assembly language produced by the compiler for the procedure note( ). The code demonstrates the level of integration produced by the use of register-aliased variables. The register allocation summary at the bottom shows that all of the variables in the program, including the register-aliased variables, are allocated to registers. In addition, notice that the register-coloring algorithm used by the allocator placed six variables in eax (this includes the use of its components al, ah, and ax), in addition to using eax as an accumulator in three other locations. The inline assembly is integrated with the surrounding C code without a single push or pop, and does not require the use of structures or calls and returns to handle interrupts and port I/O.

In particular, examine the block that starts at label L13. This block is the one that polls the timer until it is time to turn off the note. The block starts off with ax being used as an argument for the timer service routine 01AH; the eax register is then used by the compiler as a temporary accumulator, which is followed by al being used to service both input and output operations. Of course, ecx, edx, esi, and edi (or their components) are also used throughout the 12 lines of code. In fact, the only register that does not find its way into this sequence is ebx.

The resulting code is about one-third the size of a similar routine that uses calls. However, it executes substantially faster than the mere size difference would indicate, as most of the instructions are self-contained in the processor's registers. The issue of keeping things in registers becomes even more important with the 80486 which, even though it has a built-in cache, has been RISCed to execute many register-register instructions in a single cycle. Taking advantage of these single cycle operations becomes a key issue with generating 80486 code. In addition, future generations of 486 systems will probably increase the importance of register allocation as improved processor speeds (50 to 100 MHz) outstrip the ability of inexpensive DRAM memory to keep up with the CPU.

Conclusion

To wrap things up, Listing Four (page 122) presents the play( ) function. The program calls note( ), using a case statement to parse the note being played or the command being executed. The syntax chosen was that in the IBM 2.0 Basic manual. The elements of the syntax supported are shown in Table 1.

Table 1: The syntax supported by the C version of the PLAY function

  a .. g[#,+,-,.,n] the notes with optional extenders
  o .. O n          octave, n = 0 .. 6
  >                      one octave
  <                      go down an octave
  l .. L n          set note length n = 1 .. 64
  p,P,r,R n              rests of length n = 1 .. 64
  t,T n              set tempo n = 32 .. 255

The command is only a partial implementation of the Basic version. Because the current implementation of note( ) polls the timer, play( ) will not run in the background. If you plan to use this function as part of a game, I recommend that you turn the routine into a compiler that generates a list of pitches and durations that get fed to a TSR interpreter that uses the timer tick to control duration.

One final problem is the fact that C does not support strings in strings. The addition of legato or staccato is left as a reader exercise.

_ACCESSING HARDWARE FROM 80386 PROTECTED MODE: PART II_ by Stephen Fried [LISTING ONE]



#include <dos.h>   /* i.e. just like MS C */
void note();
main()
{
   note(440,500);
}
void note(pitch, duration)
int pitch,duration;
{
      int u,v;
        union REGS regs;
        unsigned int_count,int_duration,count,int_pitch;
      int_pitch = 1190000/pitch;
        int_duration = (duration*1821)/10000;
        regs.x.ax = 0;                          /* call timer */
        int86(0x1a, &regs, &regs);
        int_count = regs.x.dx;  /* internal count = lowest 16-bits of time*/
        u = inp(0x61) | 3;    /* Turn on channel 2 of 8255 using port 61h */
        outp(0x61,u);         /* send byte to back */
        outp(0x43,0xb6);      /* set up I/O register */
        outp(0x42,(char) int_pitch);    /* send freq to latch */
        outp(0x42,(int_pitch >> 8));
        do {
                regs.x.ax = 0;       /* use timer to get end of duration */
                int86(0x1a, &regs, &regs);
                count = regs.x.dx;   /* use lowest 16-bits of count */
        } while (count < int_duration + int_count);
        v = inp(0x61) & 0xfc;        /* turn off the sound */
        outp(0x61,v);
}





[LISTING TWO]


#define outp(p,v) dx = p;al = v;asm(dx,al,"   out   dx,al")
#define inp(p,v)  dx = p;asm(dx,"   in   al,dx",al);v = al
   unsigned char x,y,page = 0; /* globals for pc_test */
void note();
main()
{
   note(440,500);
}

void note(pitch, duration)
int pitch,duration;
{
   reg$eax unsigned short ax;
   reg$eax unsigned char al,x,y;
   reg$edx unsigned short dx;
   reg$ah   unsigned char ah;
   /* this section was added for play */
     unsigned int_count,int_duration,count,int_pitch;
   if (duration == 0) return;
        int_duration = (duration*1821)/10000;
/*   We left the original interrupt as a comment for comparison purposes */
/*      regs.x.ax = 0;       call timer */
/*      int86(0x1a, &regs, &regs);  */
/*      int_count = regs.x.dx; internal count = lowest 16-bits of time*/
/*   the inline assembly language is line for line identical in function
   although there is an obvious difference in format. */
   ax = 0;
   asm(ax,"   int   01ah",dx);
   int_count = dx;

   if (pitch==0) goto time_it;
   int_pitch = 1190000/pitch;
/*   The port input is a little different using inline asm macros */
/*   x = inp(0x61) | 3;   the original code becomes */
   inp(0x61,x);   /* Turn on channel 2 of 8253 using port 61H */
   x = x | 3;   /* After read turn on lowest 2 bits */
+/*   The outp macro looks just like the outp function */
      outp(0x61,x);          /* send byte to back */
        outp(0x43,0xb6);       /* set up I/O register */
        outp(0x42,(char) int_pitch);    /* send freq to latch */
        outp(0x42,(int_pitch >> 8));
time_it:
   do {
      ax = 0;      /* use timer to wait for end */
      asm(ax,"   int   01ah",dx);
      count = dx;
   } while (count < int_duration + int_count);
   inp(0x61,y);   /* Turn off channel 2 */
   y = y & 0xfc;   /* use 1111 1100 to turn off lowest 2 bits only */
   outp(0x61,y);
}





[LISTING THREE]


   name sound4.c
   .387
   assume   cs:codeseg
   assume   ds:dataseg
codeseg   segment dword er use32 public 'code'
dataseg   segment dword rw use32 public 'data'

dataseg   ends
   align   4

_note   proc   near

   push      edi
   push      esi
   push      ebx
   mov      ebx,[esp]+16
   cmp      dword ptr [esp]+20,0
   jne      L17   short
   pop      ebx
   pop      esi
   pop      edi
   ret
   align   4
L17:
   mov      ecx,10000
   imul      eax,[esp]+20,1821
   cdq
   idiv      ecx
   mov      edi,eax
   mov      ax,0
   int      01ah
   movzx   esi,dx
   or      ebx,ebx
   jne      L16   short
   jmp      L13
   align   4
L16:
   mov      eax,1190000
   cdq
   idiv      ebx
   mov      ebx,eax
   mov      dx,97
   in      al,dx
   or      al,3
   mov      dx,97
   out      dx,al
   mov      dx,67
   mov      al,182
   out      dx,al
   mov      dx,66
   mov      al,bl
   out      dx,al
   mov      dx,66
   mov      eax,ebx
   shr      eax,byte ptr 8
   out      dx,al
   align   4
L14:
   align   4
L13:
   mov      ax,0
   int      01ah
   movzx   ecx,dx
   mov      eax,edi
   add      eax,esi
   cmp      eax,ecx
   ja      L13   short
   mov      dx,97
   in      al,dx
   and      al,252
   mov      dx,97
   out      dx,al
   align   4
L9:
   pop      ebx
   pop      esi
   pop      edi
   ret
   align   4
_note   endp
dataseg   segment dword rw use32 public 'data'

;_ax         ax      local
;_al         al      local
;_x         al      local
;_y         al      local
;_dx         dx      local
;_ah         ah      local
;_int_count   esi      local
;_int_duration   edi      local
;_count      ecx      local
;_int_pitch   ebx      local

;parameters
;_pitch      ebx      local
;_duration   [esp]+20   local
dataseg   ends
   end





[LISTING FOUR]


#include <stdio.h>
#include <dos1.h>
#include <ctype.h>
/*   ***WARNING*** if you change the scale so that it starts
   on middle C, instead of A, the resulting routine will
   exhibit not only the look and feel of the BASIC PLAY
   command, but its sound as well.
*/
#define aa   440      /* middle a = 440 */
#define as   469      /* a sharp */
#define bb   493
#define cc   523      /* middle c */
#define cs   556
#define dd   587
#define ds   624
#define ee   659
#define ff   698
#define fs   739
#define gg   783
#define gs   832

#define outp(p,v) dx = p;al = v;asm(dx,al,"   out   dx,al")
#define inp(p,v)  dx = p;asm(dx,"   in   al,dx",al);v = al
   unsigned char x,y,page = 0; /* globals for pc_test */
void note();
void look_ahead_and_toot();
void play();
int check_length();
int check_integer();
int gobble_dots();
      /* the buffer for the notes to be input */
int  pitch;
int  count = 0;         /* points to current location in string */
int  length = 4;         /* default is a quarter note */
int  tempo = 240;      /* = 120 beats per minute */
int  duration = 60;      /* = tempo/length =1/4 @ 120 bpm */
int  shift = 0;         /* current octave shift factor */
char c_note;         /* the current note character used for diag */

main()
{   /* Stereo version of Heart and Soul for two PCs
   lifted from a BASIC program Transcribed by
   Michael Benjamin Fried - Age 11 */
   /* bass line plays on first machine */
   play( "T150L8O4CCEEAACCDDFFO3GG>BBO4");
   play( "L8O4CCEEAACCDDFFO3GG>BBO4");

   /* while melody plays on a second */
   play( "O4L4CCC.P32C8B8A8B8C8D8P32");
   play( "EEE.P32E8D8C8D8E8F8G.C.>A8<G8F8E8D8CB8Ao3G8FFGGo4");
}
void play(in_string)
char in_string[];
{
int temp_duration;      /* gets set by L or change in l */
int temp_octave;      /* holds temporary octave */
char   n_note;
count = 0;
printf("note = %s \n",in_string);
while (c_note = in_string[count]){ /* loop till out of characters */
   n_note = in_string[count+1];  /* look ahead 1 char now */
   switch(c_note){         /* switch on current note */

      case 'A':      /* do a,a sharp and a flat */
      case 'a':
         pitch = aa;   /* set the default to A natural */
         if ((n_note == '#')||(n_note == '+')){
            pitch = as; /* it was A sharp */
            count++;
            }
         if (n_note == '-'){ /* it was A flat */
            pitch = gs; /* A flat == G sharp */
            count++;
            }
         look_ahead_and_toot(in_string); /* self explanatory */
         break;             /* line duration */

      case 'B':      /* B is just like A */
      case 'b':
         pitch = bb;
         if ((n_note == '#')||(n_note == '+')){
            pitch = cc; /* B sharp is actually C */
            count++;
            }
         if (n_note == '-'){
            pitch = as; /* B flat is A sharp */
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'C':         /* C is just like A */
      case 'c':
         pitch = cc;
         if ((n_note == '#')||(n_note == '+')){
            pitch = cs;
            count++;
            }
         if (n_note == '-'){ /* C flat is actually B */
            pitch = bb; /* and a perfectly legal note */
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'D':      /* D is like A */
      case 'd':
         pitch = dd;
         if ((n_note == '#')||(n_note == '+')){
            pitch = ds;
            count++;
            }
         if (n_note == '-'){
            pitch = cs; /* D flat is C sharp */
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'E':      /* E is like A */
      case 'e':
         pitch = ee;
         if ((n_note == '#')||(n_note == '+')){
            pitch = ff; /* E sharp is F */
            count++;
            }
         if (n_note == '-'){
            pitch = ds;
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'F':      /* F is like A */
      case 'f':
         pitch = ff;
         if ((n_note == '#')||(n_note == '+')){
            pitch = fs;
            count++;
            }
         if (n_note == '-'){
            pitch = ee;
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'G':      /* G is like A */
      case 'g':
         pitch = gg;
         if ((n_note == '#')||(pitch == '+')){
            pitch = gs;
            count++;
            }
         if (n_note == '-'){
            pitch = fs;
            count++;
            }
         look_ahead_and_toot(in_string);
         break;

      case 'L':      /* set length */
      case 'l':
         if(temp_duration = check_length(in_string)){
            duration = tempo/temp_duration;
            length = temp_duration;
            }
         break;
      case '>':      /* go up an octave */
         shift++;
         break;

      case '<':      /* go down an octave */
         shift--;
         break;

      case 'O':      /* chose an octave */
      case 'o':
         temp_octave = n_note - '0';
         if ((temp_octave < 0)||(temp_octave > 6)){
            printf("octave out of range");
            break;
            }
         switch(n_note){
            case '0':
               shift = -4;
               break;
            case '1':
               shift = -3;
               break;
            case '2':
               shift = -2;
               break;
            case '3':
               shift = -1;
               break;
            case '4':      /* default octave */
               shift = 0;
               break;
            case '5':
               shift = 1;
               break;
            case '6':
               shift = 2;
               break;
            }
         count++;   /* advance over digit */
         break;

      case 'P':      /* set pause/rest length */
      case 'p':      /* issue note of freq 0 to rest */
      case 'R':      /* computer scientists pause */
      case 'r':      /* but musicians rest! */
         pitch = 0;
         look_ahead_and_toot(in_string);
         break;

      case 'T':      /* set tempo */
      case 't':
         temp_duration = check_integer(in_string);
         if ((temp_duration < 32)||(temp_duration > 255))
            break;
         tempo = temp_duration*2;
         duration = tempo/length;
         break;

      case ' ':      /* spaces are gobbled up */
         break;

      default:      /* had a problem so issue error */
         printf("Syntax error in character %d \n",count);
         goto terminate;
      }
   count++;   /* advance pointer to next note */
   }
terminate:
}
/* The trickiest part of the syntax are the optional trailers that
   can follow each note. These include an optional integer that
   specifies a quarter (4) or eighth note (8) (or any integer
   between 1 and 64) and 1 or more optional dots, each of which
   increases the current duration by half. This section parses
   these trailers, and then uses the global variables that contain
   the tempo and octave to compute the duration and pitch, and
   then call note. Note that rests are handled as notes of 0 pitch.
*/

void look_ahead_and_toot(in_string)
char in_string[];
{
int temp_duration;
if (temp_duration = check_length(in_string)) /* if non zero have a temp */
   temp_duration = tempo/temp_duration;  /* compute new duration */
else
   temp_duration = duration;        /* if 0 play default duration */
/* check for dot, and if found call gobble_dots to increase temp_duration */
if (in_string[count+1] == '.')
   temp_duration = gobble_dots(temp_duration,in_string);
      /* range check octaves */
if (shift < -4)
   shift = -4;
if (shift > 2)
   shift = 2;
      /* shift to change octaves */
if (shift < 0)   /* negative shifts go down in frequency */
   pitch = pitch >> -shift;
else      /* positive shifts go up in frequency */
   pitch = pitch << shift;
   /* optional diagnostics for debugging */
printf("%c = %d duration = %d octave = d\n",
         c_note,pitch,temp_duration,shift+4);
   /* finally we are ready for a little toot */
note(pitch,temp_duration);
}
int gobble_dots(duration_in,in_string)
int duration_in;
char in_string[];
{
int duration_out;
int duration_increment;
duration_out = duration_in;
duration_increment = duration_in;
/* gobble as long as there are dots adding half the prior duration inc */
while (in_string[count+1] == '.'){
   duration_increment = duration_increment >> 1; /* divide it by 2 */
   duration_out = duration_out + duration_increment;
   count++;         /* advance string pointer */
   }
return(duration_out);
}
/* returns 1-64 in range 1-64 else returns 0 */
int check_length(in_string)
char in_string[];
{
int result = check_integer(in_string);
   if ((result < 1) || (result > 64))
      return(0);   /* out of range */
   else
      return(result); /* in range */
}
/* 0 1 to 999 if 1 - 999 found and advances count */
/* returns 0 otherwise */
int check_integer(in_string)
char in_string[];
{
int n_char,m_char,l_char;
n_char = in_string[count+1] - '0';
if ((n_char > 9) || (n_char < 0)) return (0); /* return if out of range */
count++;          /* we found a digit so advance count */
m_char = in_string[count+1] - '0';   /* check next integer */
if ((m_char > 9) || (m_char < 0))
   return (n_char);
else
   count++;      /*found second didit so advance again */
l_char = in_string[count+1] - '0';
if ((l_char > 9) || (l_char < 0)) /* check last possible digit */
   return (n_char*10+m_char); /* compute 2 digit result */
else {
   count++;      /* we found a third and last digit */
   return(n_char*100+m_char*10+l_char);
   }
}
/* optional main program works as an interactive interpreter */
char note_string[120];
/*
main()
{
do{
   puts("enter note ");
   fflush(stdin);
   gets(note_string);
   printf("\nnote string = %s \n",note_string);
   play();
   } while (strlen(note_string) > 0);
}
*/



[EXAMPLE 1]

char *mapdev();
main()
{
   int jcount = 0;
   char *scr_ptr;
   /* map the screen into the data segment */
   scr_ptr = mapdev(0xb8000,4096);
   while (jcount < 4096)
   {
      *(scr_ptr + (jcount++)) = 0x20; /* write space*/
      *(scr_ptr + (jcount++)) = 0x7c; /* and attribute*/
   }
}