Structured Programming

As long as he's not distracted again, Jeff will continue his discussion of communication interrupts.


September 01, 1991
URL:http://drdobbs.com/architecture-and-design/structured-programming/184408625

Figure 1(a)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(b)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(c)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(c)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(b)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(c)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(d)


Copyright © 1991, Dr. Dobb's Journal

Figure 1(b)


Copyright © 1991, Dr. Dobb's Journal

SEP91: STRUCTURED PROGRAMMING

STRUCTURED PROGRAMMING

What is this Interrupt Really About?

Jeff Duntemann, KG7JF

Interrupts, like sorrows, come not as single spies, but in battalions. At 10:00 A.M. sharp, Joni's Airpark Catering Truck rolls into our office parking lot, playing the first three bars of "Dixie" so loud the ground shakes. This is fine by us; Joni brings things that are cold and wet, which in a Scottsdale summer is more than a convenience; it's survival.

Joni's arrival, furthermore, is more than just an interrupt. It's a whole hierarchy of interrupts. When her horn whistles Dixie, work stops. People pour into the parking lot and begin milling around, buying Cokes and muffins and waiting for... The Pizza Pride Girl.

Nobody goes back into their offices before the appearance of the Pizza Pride girl. It's part of the ritual, and there she is, bopping out of the little office that says Pizza Pride on the window, waist-length blonde hair blowing around in the hot wind. We're not sure what they do in the Pizza Pride office; probably broker tons of imitation cheese-flavored Pizza polymer to pizza parlors around the country. It doesn't matter. They have Pizza Pride Girl.

She's probably 20, with cheekbones up to here, exotic sculpted eyebrows and a shameful abundance of things that bored young men like to look at. And look they do (discreetly; this isn't any construction site), while she picks out her Coke and doughnuts. Some days its a silk camisole over black leather pants laced up to the waist on both sides: other days it's a polka-dot turn over skin-tight cyclist's pants. Some days coolie slippers, some days four-inch heels, some days barefoot. In a third-rate office park dominated by insurance agencies and windshield repair places, she represents the only truly random element in an otherwise totally predictable routine.

Some days Keith and I look down from our second-floor window over-looking the parking lot, and dig each other in the ribs watching the women watching the men watching the Pizza Pride Girl. It doesn't really help get the work done, but it does truly break up the day.

And sometimes, on the way out the door to get a Coke from Joni, we ask each other, well, are you really going down for a Coke or are you just going down to see what the Pizza Pride Girl will be wearing today?

In other words, what is this interruption really about?

Prioritizing Interrupts

Here we fold seamlessly into where we left our discussion last month, with a question the CPU asks the 8259 Programmable Interrupt Controller chip frequently. Recall that there's only one interrupt input pin on the CPU chip. One of the 8259's most important jobs is to allow several different hardware devices to share access to that one interrupt input pin. It has another job at least as important as (and much more complex than) the first: handling the situation that comes up when two or more different devices want to interrupt the CPU at the same time.

The CPU can only do one thing at a time. When an interrupt comes in, it begins executing the Interrupt Service Routine (ISR) for that interrupt. Completing the ISR takes a short (one hopes) time, but it still takes time. What happens if a second interrupt comes in while the CPU is still executing the ISR belonging to the first interrupt?

That depends on the priority level of the two interrupts.

When the 8259 chip is initialized at power-up time, it stack-ranks IRQ0-IRQ7 in a particular priority order. The default ordering puts IRQ0 at the highest priority, followed by IRQ1, and so on, with IRQ7 in the priority basement. This ordering can be changed on-the-fly by sending commands to the 8259, but on the PC this is almost never done. Assume unless told otherwise in PC work that IRQ0-7 are priority ordered with IRQ0 at the top and the others in order down from there.

Now, back to the question of a second interrupt appearing while the first is being serviced. If the second interrupt to come in has a higher priority than the first, the second interrupt will interrupt the first, and the second interrupt's ISR will begin running immediately. Otherwise, the second interrupt will be ignored.

For example: The COM1: serial port uses IRQ4. The timer tick interrupt uses IRQ0. If you are running a communications program and your IRQ4 ISR is currently executing, the timer tick interrupt will interrupt your communications ISR. IRQ0 has a higher priority than IRQ4. However, the parallel port uses IRQ7; and if the parallel port tries to interrupt the CPU while the serial port ISR is executing, the parallel port's interrupt request will be ignored until the serial port ISR completes execution. IRQ7 has a lower priority than IRQ4.

Similarly, if two interrupts come in at exactly the same time, (and no ISR is currently executing) the 8259 chooses the one with the higher priority and allows it to execute.

Priority of interrupts generally isn't a serious problem in PC work. One potential problem appears if you want to use both COM1 and COM2 at the same time. COM2, using as it does IRQ3, has a higher priority than COM1 using IRQ4. COM1 cannot interrupt the machine while a COM2 interrupt is being serviced. This is one excellent reason to keep your ISRs short and to the point.

The In-Service Register and EOI

The 8259 contains an 9-bit register called the In-Service Register. (Some folks call it the ISR, but doing so results in a bad case of colliding acronyms -- forgive me if I spell it out a lot.) There is one bit in the read-only In-Service Register for each of the eight IRQ lines supported by the 8259. Bit 0 belongs to IRQ0, bit 1 to IRQ1, and so on. When the ISR of an IRQ is currently executing, that IRQ's bit in the In-Service Register will be set to 1. When one of the bits is set to 1, the 8259 will ignore any IRQ whose priority level is lower than the IRQ whose bit is set.

Thus, during the time that your IRQ4 ISR is executing, bit 4 in the In-Service Register will be set to 1. But until that bit is cleared to 0, no interrupt from IRQ4-IRQ7 will be recognized by the 8259. Who clears it? Your program must, by sending a command to the 8259 chip. This command is called an End-Of-Interrupt (EOI) command.

There are two kinds of EOI commands that the 8259 understands. One is the nonspecific EOI (NSEOI) and the other is the specific EOI (SEOI). The specific EOI command tells the 8259 that a particular specified ISR has completed execution. The nonspecific EOI simply tells the 8259 that whatever ISR was most recently executing is now complete. Nonspecific EOI is the one most frequently used. Keep in mind that as long as interrupts are prioritized, the 8259 always knows what IRQ is currently executing: the one with the highest-priority 1-bit set in the In-Service Register. If IRQ0 and IRQ4 are both marked (with 1-bits) as being in ser vice, IRQ0 must be the one currently executing, because it has priority over IRQ4. So when a nonspecific EOI command comes in, the 8259 clears the set bit with the highest priority in the In-Service Register; in this case, bit 0, which belongs to IRQ0.

All you need to do to send a non-specific EOI command to the 8259 is write the value $20 to an 8259 register called Operation Control Word 2 (OCW2), which on the PC is located at I/O port $20: Port[$20]:= $20;.

A nonspecific EOI command must be sent to the 8259 at the conclusion of every ISR, or lower priority interrupts will simply be blocked.

A Real Interrupt-Driven Terminal Program

There's another aspect to the "What is this interrupt really about?" question that I haven't addressed yet: The UART can generate an interrupt based on any of several different occurrences, such as a character coming in, or readiness to send another character out. But if I go further down that road without providing some practice, the theory I've been laying out these past few columns will set into cement without being useful. So we'll come back to that question in the near future. For now, let's talk about the simplest possible interrupt-driven communications program, one that recognizes only a single interrupt trigger: an incoming character from the modem.

Listing One (page 142) is INTTERM.-PAS, the interrupt-driven successor to POLLTERM, which I presented a couple of columns ago. It does pretty much what POLLTERM does, with the exception that it doesn't poll the serial port for incoming characters. Instead, when a character appears from the modem, INTTERM grabs it from the port through the use of an interrupt service routine, and tucks it away until the program logic can deal with it.

INTTERM is a good, simple working example of how the PC's interrupt machinery operates. Once you digest that, you'll be much less likely to trip over the more complex serial port object I'm rolling for a future column.

INTTERM is not object oriented, and should compile under Turbo Pascal 5.0, 5.5, or 6.0.

The Polling Loop

I shouldn't mislead you by implying that there isn't any polling going on in INTTERM.PAS. INTTERM has a polling loop almost identical to the one in POLLTERM. The loop is the REPEAT..UNTIL structure at the end of the main program block. It polls the incoming character butfer. If a character is ready, it takes the character from the buffer and writes it to the screen. It then polls the keyboard, and if a keystroke is ready (and if that keystroke is not a predefined command), INTTERM writes the keystroke to the serial port. That's the polling loop in a nutshell.

The key difference between POLLTERM and INTTERM is where the incoming character comes from, POLLTERM actually polls the serial port hardware in its InStat function, looking at bit 0 of the UART's Line Status Register (LSR). If LSR bit 0 is 1, a character is ready to be picked up.

INTTERM also has an InStat function, but the serial port hardware is not involved. In fact, nowhere in the main program block, nor in any routine called by the main program block is an incoming character read from the serial port. INTTERM's interrupt service routine fetches the incoming character from the serial port and places it in a buffer. The polling loop only "talks to" that buffer--and that buffer is an interesting character all by itself.

Circular Buffers

The variable Buffer is defined as type CircularBuffer -- but there isn't anything unusual about its declaration as a simple character array. What makes CircularBuffer special (that is, circular) is not the way it is defined but the way it is used. Follow along on Figure 1 during the discussion below.

Two integer variables LastSaved and LastRead contain indexes into the Buffer array. When execution begins, both are set to 0--the index of the first element of the array. After interrupts are turned on and INTTERM begins listening for incoming characters, the ISR Incoming picks up any characters that arrive from the serial port, and writes them to the next position in Buffer. The next position is defined to be the position immediately after the index stored in the LastSaved variable.

The key to this whole notion of being circular lies in how that next position in the array is determined. This is the code (from Incoming) that does it:

  IF LastSaved >= BUFFSIZE THEN
    LastSaved := 0
  ELSE
    Inc(LastSaved);

BUFFSIZE is an integer constant that specifies the highest index of the CircularArray array type. (Here, 1023.) What happens in the test is that if LastSaved is found to be equal to the highest index in the array, LastSaved is reset to 0--otherwise it is simply incremented to the next index. In other words, once you increment LastSaved to the end of the array, it "wraps" around to the beginning again.

In Figure 1(a), four characters have come in, and have been placed into Buffer by the ISR Incoming. (I've reduced the size of BUFFER to 16 here to keep the figure manageable.) LastSaved "points" to the last character it saved -- hence its name. The other index pointer, LastRead, is defined as holding the index of the last character read from the buffer by the polling loop, hence its name. Here, LastRead has yet to move off zero. Once the polling loop begins to poll, however, LastRead will pick up one character on each pass through the REPEAT..UNTIL loop. Each time InChar picks a character out of the buffer, LastRead is incremented:

  IF LastRead >= BUFFSIZE THEN
     LastRead := 0
  ELSE
     Inc(LastRead)

Looks familiar? LastRead and LastSaved are treated identically by the program. Both are incremented along the Buffer array, and both wrap back to 0 once they reach the end of the array.

By the time represented in Figure 1(b), LastRead has picked four characters out of Buffer. LastSaved, however, has gotten way ahead, having placed a full 16 characters into Buffer at the behest of Incoming. When the next interrupt happens, Incoming will wrap LastSaved around to 0, so that it can begin its dash through Buffer again.

Note that as soon as it wraps to 0, LastSaved begins "reusing" locations in Buffer where it placed characters earlier; Figure 1(c). This is OK -- because LastRead managed to get them out of the buffer and displayed to the screen before the original characters were overwritten. In Figure 1(c), LastRead is finally beginning to "catch up" to LastSaved. In Figure 1(b), LastRead was a full 12 characters behind LastSaved. In Figure 1(c), LastRead has come up from behind and is only five characters behind LastSaved.

Because both index pointers LastSaved and LastRead are wrapped around to 0 when they reach the end of the Buffer array, we can think (metaphorically) of the two ends of Buffer brought up and butted nose-to-tail, making the buffer into a ring, as shown in Figure 1(d). As characters are placed into the buffer and read from the buffer, the two index pointers zip around and around the buffer in the direction shown by the array.

Catching Up

The notion of "catching up" is important in a circular buffer. By definition, the circular buffer is empty when the two pointers are equal. Think about it: If the last character read (at LastRead) is the same as the last character saved (at LastSaved), then there are no more characters waiting to be read. The buffer is never "cleared" by being filled with blanks or anything. Whether or not it is empty and what characters it "contains" is dictated solely by the relative position of the two pointers. The contents of a circular buffer are defined as those characters between LastRead and LastSaved, moving index-forward from LastRead.

As Figure 1(b) begins to demonstrate, LastSaved can only get so far ahead of LastRead before information stored in the buffer is overwritten and lost. So over a period of time, the program had better be capable of reading characters from the buffer just as fast as the interrupt service routine can put them there. However, the buffer allows the modem to deliver data in rapid spurts, perhaps faster (for a few seconds) than the polling loop can grab. Or, the buffer allows the polling loop to go off and do other things for a few seconds (like closing or opening a disk file) and not have to worry about missing incoming characters.

On a fast machine, POLLTERM can run as fast as 2400 baud without missing any characters. But run any faster -- or begin to do anything more involved than throw incoming characters at the display -- and the power of INTTERM becomes mandatory.

Wrapping With AND

Experienced Pascal hackers should be assured that I am aware of a faster way to wrap an index -- by using the AND operator -- but the whole idea with INTTERM is to create an interrupt-driven program that the ordinary Pascal programmer can understand. AND will do the same thing as the IF statement just shown, like so:

  LastSaved := Inc(LastSaved) AND
                        BUFFSIZE;

This, however, is nowhere near as easy to understand. For those who want to work it out, keep in mind that using AND like this works only for values of BUFFSIZE that are some power of two decremented by one; for example, 15, 63, 255, and so on.

The ISR Itself

Functionally, the interrupt service routine itself (Incoming in INTTERM) is simple. In truth, it's only four statements long.

The first thing most common ISRs must do is turn interrupts back on again at the CPU. When the 86-family of processors recognize an interrupt, they set a flag indicating that an interrupt is in progress. Until this flag is cleared again, another interrupt will not be recognized by the CPU. Now, in most cases there is no reason why one ISR cannot be interrupted by another, and in fact it happens all the time. The timer tick interrupt on IRQ0 interrupts everybody (given its top priority) and this is an accepted way of life. So early on in a garden-variety ISR, execute a CLI instruction (opcode $FB) to clear the flag and reenable CPU interrupts. (This has nothing to do with the EOI command that must be sent to the 8259 PIC -- we'll get to that shortly.)

After reenabling CPU interrupts, the ISR increments the LastSaved pointer as explained earlier. Once LastSaved points to the next location in Buffer, the incoming character is read from the Receive Buffer Register (RBR) and written into Buffer at LastSaved. That's the core task of the ISR: to grab a character from RBR and write it into Buffer.

Finally, with its work done, the ISR must tell the 8259 that it's all finished, and to clear the appropriate bit in the In-Service Register. This is the purpose of the line

  Port[OCW2] := $20;

If you don't do this, you won't get another serial port interrupt, nor any interrupt with a lower priority. Don't forget EOI!

Turbo Pascal Interrupt Procedures

The Incoming ISR is written entirely in Turbo Pascal. There's no assembly language involved. The qualifier INTERRUPT identifies an otherwise unremarkable procedure as an interrupt procedure. The differences are almost all beneath the surface, in the way the compiler generates entry and exit code for the procedure.

One thing is for sure: Do not try to execute an interrupt procedure as though it were a normal procedure. Odd things will happen, up to and including hard system crashes.

Nothing in its definition attaches an interrupt procedure to any particular interrupt vector. This has to be done manually, using Turbo Pascal's GetIntVec and SetIntVec procedures, as shown in the SetupSerialPort procedure:

  GetIntVec(ComInt, OldVector);
  ExitProc := @IntTermExitProc;
  SetIntVec(ComInt,@Incoming);

You could conceivably just peek and poke addresses into the interrupt vector table itself, but this is a bad idea. What would happen if an interrupt occurred on a vector while you're half-finished changing it? Nothing good, surely. Turbo Pascal's library procedures use the "safe" routines for altering vectors available through DOS.

Another point to notice from the snippet shown above is the use of an exit procedure in connection with INTTERM. The exit procedure, IntTermExitProc, is there to restore certain essential conditions in the event that a runtime error dumps execution from INTTERM back to DOS. Exit procedures are always executed before the Turbo Pascal runtime code relinquishes control to DOS, either normally or in the event of a runtime error. You don't have to call the exit procedure explicitly; the Turbo Pascal runtime code hands control to it automatically at the appropriate time. Note the $F directives: Exit procedures must always be declared as FAR, because they must be callable from the runtime library's code segment rather than any particular program or unit's code segment.

The first priority within the exit procedure is to put the original vector back into the interrupt vector used by INTTERM. The vector that was in the table when INTTERM took control from DOS is saved in the variable OldVector, and Oldvector is stuffed back into the interrupt vector table by the exit procedure. Additionally, the communications line is brought down, and interrupts are disabled both at the UART and at the 8259 PIC.

An exit procedure you build into your Pascal communications software may do more than this, but it should never do less.

What Not to Do in an Interrupt Procedure

And finally, the big question of How To Stay Alive inside an interrupt routine. Most people have a vague awareness that interrupt service routines are truly alien territory, where things that nobody would bat an eye about in an ordinary program can put your session six feet under. The problem is called reentrancy, and it centers around the possibility of interrupting a piece of code, and then executing that piece of code again from within the interrupt service routine. Some code is reentrant (meaning it can be interrupted and executed again) and some is not.

The biggest hazard is DOS. Once interrupted, DOS should not be executed a second time unless some very arcane precautions are taken. (I won't attempt to enumerate them here. That's a subject for an entire book, not a column.) So don't make DOS calls from within an ISR, or (obviously) call any code that makes DOS calls. Read/Readln and Write/Writeln use DOS, as do all disk access routines. The heap is another thing that is not generally reentrant. Do not call any of the heap management routines. However, I've managed to access data stored on the heap from within an ISR by dereferencing pointers. This data was allocated outside the ISR, however. Do not allocate or free memory from within an ISR.

In general, limit your ISR's activities to simple stuffing of buffers and manipulation of global variables, as INTTERM does here. Reentrancy is only one of your problems; putting too much stuff into an ISR makes your ISR run slowly, which is a hazard all by itself. The bulkier your ISR, the less quickly your com software can be made to run.

Torment It!

On the other hand, it might well be educational to carefully back up your hard disk and do a lot of stupid things to INTTERM. Make DOS calls or allocate heap memory from within the ISR. Forget to issue EOI. Call Incoming directly. Then watch what happens.

I've found that you learn less when things go well than when they blow up in your face. And the evil savor of the forbidden can be very satisfying -- especially considering that mayhem is less likely with software than with, say, lighting off M80s inside beer cans.

That's it on interrupts for a column or so. Study them in the meantime. We'll be back to them, after covering some additional design issues and taking a stab at the goblin we know as Turbo Vision.


_STRUCTURED PROGRAMMING COLUMN_
by Jeff Duntemann


[LISTING ONE]


{------------------------------------------------------------------------}
{                           INTTERM                                      }
{                             by Jeff Duntemann                          }
{                             Turbo Pascal V5.0 or later                 }
{                             Last update 6/2/91                         }
{ This is an interrupt-driven "dumb terminal" program for the PC. It     }
{ the use of Turbo Pascal's INTERRUPT procedures, and in a lesser fashion}
{ the use of serial port hardware. It can be set to use either COM1: or  }
{ COM2: by setting the COMPORT constant to 1 (for COM1) or 2 (for COM2)  }
{ as appropriate and recompiling.                                        }
{------------------------------------------------------------------------}

PROGRAM INTTERM;

USES DOS,CRT;

CONST
  COMPORT  = 2;                     {  1 = COM1:  2 = COM2: }
  COMINT   = 13-COMPORT;            { 12 = COM1: (IRQ4) 11 = COM2: (IRQ3) }
  COMBASE  = $2F8;
  PORTBASE = COMBASE OR (COMPORT SHL 8);  { $3F8 for COM1: $2F8 for COM2: }

  { 8250 control registers, masks, etc. }
  RBR      = PORTBASE;        { 8250 Receive Buffer Register     }
  THR      = PORTBASE;        { 8250 Transmit Holding Register   }
  LCR      = PORTBASE + 3;    { 8250 Line Control Register       }
  IER      = PORTBASE + 1;    { 8250 Interrupt Enable Register   }
  MCR      = PORTBASE + 4;    { 8250 Modem Control Register      }
  LSR      = PORTBASE + 5;    { 8250 Line Status Register        }
  DLL      = PORTBASE;        { 8250 Divisor Latch LSB           }
  DLM      = PORTBASE + 1;    { 8250 Divisor Latch MSB           }
  DLAB     = $80;             { 8250 Divisor Latch Access Bit    }

  BAUD300  = 384;     { Value for 300 baud operation        }
  BAUD1200 = 96;      { Value for 1200 baud operation       }
  NOPARITY = 0;       { Comm format value for no parity     }
  BITS8    = $03;     { Comm format value for 8 bits        }
  DTR      = $01;     { Value for Data Terminal Ready       }
  RTS      = $02;     { value for Ready To Send             }
  OUT2     = $08;     { Bit that enables adapter interrupts }
  BUFFSIZE = 1023;

  { 8259 control registers, masks, etc. }
  OCW1     = $21;     { 8259 Operation Control Word 1  }
  OCW2     = $20;     { 8259 Operation Control Word 2  }
                      { The 8259 mask bit is calculated depending on }
                      { which serial port is used...   }
                      { $10 for COM1: (IRQ4); $08 for COM2: (IRQ3): }
  IRQBIT   = $20 SHR COMPORT;

TYPE
  CircularBuffer = ARRAY[0..BUFFSIZE] OF Char;  { Input buffer }
VAR
  Quit       : Boolean;           { Flag for exiting the program }
  HiBaud     : Boolean;           { True if 1200 baud is being used }
  KeyChar    : Char;              { Character from keyboard }
  CommChar   : Char;              { Character from the comm port }
  Divisor    : Word;              { Divisor value for setting baud rate }
  Clearit    : Byte;              { Dummy variable }
  Buffer     : CircularBuffer;    { Our incoming character buffer }
  LastRead,                       { Index of the last character read }
  LastSaved  : Integer;           { Index of the last character stored }
  NoShow     : SET OF Char;       { Don't show characters set }
  OldVector  : Pointer;           { Global storage slot for the old }
                                  { interrupt vector }
PROCEDURE EnableInterrupts;
INLINE($FB);
{->>>>Incoming (Interrupt Service Routine)<<<<--------------------------------}
{ This is the ISR (interrupt Service Routine) for comm ports. DO NOT call this}
{ routine directly; you'll crash hard. The only way Incoming takes control is }
{ when a character coming in from modem triggers a hardware interrupt from    }
{ serial port chip, the 8250 UART. Note that the register pseudo-parameters   }
{ are not needed here, and you could omit them. However, omitting them doesn't}
{ really get you any more speed or reliability.                               }
{--------------------------------------------------------------------------}

PROCEDURE Incoming(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : Word);
INTERRUPT;
BEGIN
  { First we have to enable interrupts *at the CPU* during the ISR: }
  EnableInterrupts;
  { The first "real work" we do is either wrap or increment the index of the }
  { last character saved. If index is "topped out" at buffer size (here,     }
  { 1023) we force it to zero. This makes the 1024-byte buffer "circular,"   }
  { in that  once the index hits the end, it rolls over to beginning again.  }
  IF LastSaved >= BUFFSIZE THEN LastSaved := 0 ELSE Inc(LastSaved);

  { Next, we read the actual incoming character from the serial port's}
  { one-byte holding buffer: }
  Buffer[LastSaved] := Char(Port[RBR]);

  { Finally, we must send a control byte to the 8259 interrupt  }
  { controller, telling it that the interrupt is finished:      }
  Port[OCW2] := $20;                    { Send EOI byte to 8259 }
END;

{$F+}
PROCEDURE IntTermExitProc;
BEGIN
  Port[IER] := 0;                      { Disable interrupts at 8250 }
  Port[OCW1] := Port[OCW1] OR IRQBIT;    { Disable comm int at 8259 }
  Port[MCR] := 0;                       { Bring the comm line down  }
  SetIntVec(COMINT,OldVector);    { Restore previously saved vector }
END;
{$F-}

PROCEDURE SetupSerialPort;
BEGIN
  LastRead  := 0;  { Initialize the circular buffer pointers }
  LastSaved := 0;

  Port[IER] := 0;  { Disable 8250 interrupts while setting them up }

  GetIntVec(ComInt,OldVector);             { Save old interrupt vector }
  ExitProc := @IntTermExitProc;            { Hook exit proc into chain }
  SetIntVec(ComInt,@Incoming);     { Put ISR address into vector table }

  Port[LCR] := Port[LCR] OR DLAB;     { Set up 8250 to set baud rate   }
  Port[DLL] := Lo(Divisor);           { Set baud rate divisor          }
  Port[DLM] := Hi(Divisor);
  Port[LCR] := BITS8 OR NOPARITY;         { Set word length and parity }
  Port[MCR] := DTR OR RTS OR OUT2;        { Enable adapter, DTR, & RTS }
  Port[OCW1] := Port[OCW1] AND (NOT IRQBIT);  { Turn on 8259 comm ints }
  Clearit := Port[RBR];                   { Clear any garbage from RBR }
  Clearit := Port[LSR];                   { Clear any garbage from LSR }
  Port[IER] := $01;      { Enable 8250 interrupt on received character }
END;

FUNCTION InStat : Boolean;
BEGIN
  IF LastSaved <> LastRead THEN InStat := True
    ELSE InStat := False;
END;

FUNCTION InChar : Char;    { Bring in the next character }
BEGIN
  IF LastRead >= BUFFSIZE THEN LastRead := 0
    ELSE Inc(LastRead);
  InChar := Buffer[LastRead];
END;

PROCEDURE OutChar(Ch : Char);   { Send a character to the comm port }
BEGIN
  Port[THR] := Byte(Ch)     { Put character ito Transmit Holding Register }
END;

PROCEDURE ShowHelp;
BEGIN
  Writeln('>>>IntTerm by Jeff Duntemann >>>>>>>>>>>>>>>>>>>>>>>>>>>');
  Writeln('   Defaults to 1200 Baud; to run at 300 Baud');
  Writeln('   invoke with "300" after "INTTERM" on the command line.');
  Writeln;
  Writeln('   Commands:');
  Writeln('   ALT-X:  Exits to DOS');
  Writeln('   CTRL-Z: Clears the screen');
  Writeln('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
END;

{>>>>>INTTERM MAIN PROGRAM<<<<<}
BEGIN
  HiBaud := True;               { Defaults to 1200 baud; if "300" is }
  Divisor := BAUD1200;          { entered after "INTTERM" on the command }
  IF ParamCount > 0 THEN        { line, then 300 baud is used instead.   }
    IF ParamStr(1) = '300' THEN
      BEGIN
        HiBaud := False;
        Divisor := BAUD300
      END;
  DirectVideo := True;
  NoShow := [#0,#127];          { Don't display NUL or RUBOUT }
  SetupSerialPort;              { Set up serial port & turn on interrupts }
  ClrScr;
  Writeln('>>>INTTERM by Jeff Duntemann');
  Quit := False;        { Exit INTTERM when Quit goes to True }
  REPEAT
    IF InStat THEN      { If a character comes in from the modem }
      BEGIN
        CommChar := InChar;                        { Go get character  }
        CommChar := Char(Byte(CommChar) AND $7F);  { Mask off high bit }
        IF NOT (CommChar IN NoShow) THEN           { If we can show it,}
          Write(CommChar)                          {  then show it! }
      END;
    IF KeyPressed THEN  { If a character is typed at the keyboard }
      BEGIN
        KeyChar := ReadKey;       { First, read the keystroke }
        IF KeyChar = Chr(0) THEN  { We have an extended scan code here }
          BEGIN
            KeyChar := ReadKey;   { Read second half of extended code  }
            CASE Ord(KeyChar) OF
             59 : ShowHelp;       { F1 : Display help screen }
             45 : Quit := True;   { Alt-X: Exit IntTerm      }
            END { CASE }
          END
        ELSE
        CASE Ord(KeyChar) OF
         26 : ClrScr;             { Ctrl-Z: Clear the screen }
         ELSE OutChar(KeyChar)
        END;  { CASE }
      END
  UNTIL Quit
END.


Copyright © 1991, Dr. Dobb's Journal

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