The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Twenty Two (Part 1)

Table of Content

Chapter Twenty Three  

CHAPTER TWENTY TWO:
THE PC SERIAL PORTS (Part 2)
22.2 - The UCR Standard Library Serial Communications Support Routines
22.3 - Programming the 8250 (Examples from the Standard Library)

22.2 The UCR Standard Library Serial Communications Support Routines

Although programming the 8250 SCC doesn't seem like a real big problem, invariably it is a difficult chore (and tedious) to write all the software necessary to get the serial communication system working. This is especially true when using interrupt driven serial I/O. Fortunately, you do not have to write this software from scratch, the UCR Standard library provides 21 support routines that trivialize the use of the serial ports on the PC. About the only drawback to these routines is that they were written specifically for COM1:, although it isn't too much work to modify them to work with COM2:. The following table lists the available routines:

Standard Library Serial Port Support
Name Inputs Outputs Description
ComBaud AX: bps (baud rate) = 110, 150, 300, 600, 1200, 2400, 4800, 9600, or 19200 - Sets the communication rate for the serial port. ComBaud only supports the specified speeds. If ax contains some other value on entry, ComBaud ignores the value.
ComStop AX: 1 or 2 - Sets the number of stop bits. The ax register contains the number of stop bits to use (1 or 2).
ComSize AX: word size (5, 6, 7, or 8) - Sets the number of data bits. The ax register contains the number of bits to transmit for each byte on the serial line.
ComParity AX: Parity selector. If bit zero is zero, parity off, if bit zero is one, bits one and two are:

00 - odd parity

01 - even parity

10 - parity stuck at 0

11 - parity stuck at 1
- Sets the parity (if any) for the serial communications.
ComRead

-

AL- Character read from port. Waits until a character is available from in the data register and returns that character. Used for polled I/O on the serial port. Do not use if you've activated the serial interrupts (see ComInitIntr).
ComWrite AL- Character to write.

-

Waits until the transmitter holding register is empty, then writes the character in al to the output register. Used for polled I/O on the serial port. Do not use with interrupts activated.
ComTstIn - AL=0 if no character,

AL=1 if char avail.
Test to see if a character is available at the serial port. Use only for polling I/O, do not use with interrupts activated.
ComTstOut - AL=0 if transmitter busy, AL=1 if not busy. Test to see if it is okay to write a character to the output register. Use with polled I/O only, do not use with interrupts active.
ComGetLSR - AL= Current LSR value. Returns the current LSR value in the al register. See the section on the LSR for more details.
ComGetMSR - AL= Current MSR Value. Returns the current MSR value in the al register. See the section on the MSR for more details.
ComGetMCR

-

AL= Current MCR Value. Returns the current MCR value in the al register. See the section on the MCR for more details.
ComSetMCR AL = new MCR Value

-

Stores the value in al into the MCR register. See the section on the MCR for more details.
ComGetLCR

-

AL= Current LCR Value. Returns the current LCR value in the al register. See the section on the LCR for more details.
ComSetLCR AL = new LCR Value

-

Stores the value in al into the LCR register. See the section on the LCR for more details.
ComGetIIR - AL= Current IIR Value. Returns the current IIR value in the al register. See the section on the IIR for more details.
ComGetIER - AL= Current IER Value. Returns the current IER value in the al register. See the section on the IER for more details.
ComSetIER AL = new IER Value

-

Stores the value in al into the IER register. See the section on the IER for more details.
ComInitIntr - - Initializes the system to support interrupt driven serial I/O. See details below.
ComDisIntr - - Resets the system back to polled serial I/O
ComIn - - Reads a character from the serial port when operating with interrupt driven I/O.
ComOut - - Writes a character to the serial port using interrupt driven I/O.

The interrupt driven I/O features of the Standard Library routines deserve further explanation. When you call the ComInitIntr routine, it patches the COM1: interrupt vectors (int 0Ch), enables IRQ 4 in the 8259A PIC, and enables read and write interrupts on the 8250 SCC. One thing this call does not do that you should is patch the break and critical error exception vectors (int 23h and int 24h) to handle any program aborts that come along. When your program quits, either normally or via one of the above exceptions, it must call ComDisIntr to disable the interrupts. Otherwise, the next time a character arrives at the serial port the machine may crash since it will attempt to jump to an interrupt service routine that might not be there anymore.

The ComIn and ComOut routines handle interrupt driven serial I/O. The Standard Library provides a reasonable input and output buffer (similar to the keyboard's type ahead buffer), so you do not have to worry about losing characters unless your program is really, really slow or rarely reads any data from the serial port.

Between the ComInitIntr and ComDisIntr calls, you should not call any other serial support routines except ComIn and ComOut. The other routines are intended for polled I/O or initialization. Obviously, you should do any necessary initialization before enabling interrupts, and there is no need to do polled I/O while the interrupts are operational. Note that there is no equivalent to ComTstIn and ComTstOut while operating in interrupt mode. These routines are easy to write, instructions appear in the next section.

22.3 Programming the 8250 (Examples from the Standard Library)

The UCR Standard Library Serial Communication routines provide an excellent example of how to program the 8250 SCC directly, since they use nearly all the features of that chip on the PC. Therefore, this section will list each of the routines and describe exactly what that routine is doing. By studying this code, you can learn about all the details associated with the SCC and discover how to extend or otherwise modify the Standard Library routines.

; Useful equates:

BIOSvars        =       40h             ;BIOS segment address.
Com1Adrs        =       0               ;Offset in BIOS vars to COM1: address.
Com2Adrs        =       2               ;Offset in BIOS vars to COM2: address.

BufSize         =       256             ;# of bytes in buffers.


; Serial port equates. If you want to support COM2: rather than COM1:, simply
; change the following equates to 2F8h, 2F9h, ...

ComPort         =       3F8h
ComIER          =       3F9h
ComIIR          =       3FAh
ComLCR          =       3FBh
ComMCR          =       3FCh
ComLSR          =       3FDh
ComMSR          =       3FEh


; Variables, etc. This code assumes that DS=CS. That is, all the variables
; are in the code segment.
;
; Pointer to interrupt vector for int 0Ch in the interrupt vector table.
; Note: change these values to 0Bh*4 and 0Bh*4 + 2 if you want to support
; the COM2: pot.

int0Cofs        equ     es:[0Ch*4]
int0Cseg        equ     es:[0Ch*4 + 2]

OldInt0c        dword   ?

; Input buffer for incoming character (interrupt operation only). See the
; chapter on data structures and the description of circular queus for
; details on how this buffer works. It operates in a fashion not unlike
; the keyboard's type ahead buffer.

InHead          word    InpBuf
InTail          word    InpBuf
InpBuf          byte    Bufsize dup (?)
InpBufEnd       equ     this byte

; Output buffer for characters waiting to transmit.

OutHead         word    OutBuf
OutTail         word    OutBuf
OutBuf          byte    BufSize dup (?)
OutBufEnd       equ     this byte

; The i8259a variable holds a copy of the PIC's IER so we can restore it
; upon removing our interrupt service routines from memory.

i8259a          byte    0       ;8259a interrupt enable register.

; The TestBuffer variable tells us whether we have to buffer up characters
; or if we can store the next character directly into the 8250's output
; register (See the ComOut routine for details).

TestBuffer      db      0

The first set of routines provided by the Standard Library let you initialize the 8250 SCC. These routines provide "programmer friendly" interfaces to the baud rate divisor and line control registers. They let you set the baud rate, data size, number of stop bits, and parity options on the SCC.

The ComBaud routine sets the 8250's transfer rate (in bits per second). This routine provides a nice "programmer's interface" to the 8250 SCC. Rather than having to compute the baud rate divisor value yourself, you can simply load ax with the bps value you want and simply call this routine. Of course, one problem is that you must choose a bps value that this routine supports or it will ignore the baud rate change request. Fortunately, this routine supports all the common bps rates; if you need some other value, it is easy to modify this code to allow those other rates.

This code consists of two parts. The first part compares the value in ax against the set of valid bps values. If it finds a match, it loads ax with the corresponding 16 bit divisor constant. The second part of this code switches on the baud rate divisor registers and stores the value in ax into these registers. Finally, it switches the first two 8250 I/O registers back to the data and interrupt enable registers.

Note: This routine calls a few routines, notably ComSetLCR and ComGetLCR, that we will define a little later. These routines do the obvious functions, they read and write the LCR register (preserving registers, as appropriate).

ComBaud         proc
                push    ax
                push    dx
                cmp     ax, 9600
                ja      Set19200
                je      Set9600
                cmp     ax, 2400
                ja      Set4800
                je      Set2400
                cmp     ax, 600
                ja      Set1200
                je      Set600
                cmp     ax, 150
                ja      Set300
                je      Set150
                mov     ax, 1047                ;Default to 110 bps.
                jmp     SetPort

Set150:         mov     ax, 768                 ;Divisor value for 150 bps.
                jmp     SetPort

Set300:         mov     ax, 384                 ;Divisor value for 300 bps.
                jmp     SetPort

Set600:         mov     ax, 192                 ;Divisor value for 600 bps.
                jmp     SetPort

Set1200:        mov     ax, 96                  ;Divisor value for 1200 bps.
                jmp     SetPort

Set2400:        mov     ax, 48                  ;Divisor value for 2400 bps.
                jmp     SetPort

Set4800:        mov     ax, 24                  ;Divisor value for 4800 bps.
                jmp     SetPort

Set9600:        mov     ax, 12                  ;Divisor value for 9600 bps.
                jmp     short SetPort

Set19200:       mov     ax, 6                   ;Divisor value for 19.2 kbps.
SetPort:        mov     dx, ax                  ;Save baud value.
                call    GetLCRCom               ;Fetch LCR value.
                push    ax                      ;Save old divisor bit value.
                or      al, 80h                 ;Set divisor select bit.
                call    SetLCRCom               ;Write LCR value back.
                mov     ax, dx                  ;Get baud rate divisor value.
                mov     dx, ComPort             ;Point at L.O. byte of divisor reg.
                out     dx, al                  ;Output L.O. byte of divisor.
                inc     dx                      ;Point at the H.O. byte.
                mov     al, ah                  ;Put H.O. byte in AL.
                out     dx, al                  ;Output H.O. byte of divisor.
                pop     ax                      ;Retrieve old LCR value.
                call    SetLCRCom1              ;Restore divisor bit value.
                pop     dx
                pop     ax
                ret
ComBaud         endp

The ComStop routine programs the LCR to provide the specified number of stop bits. On entry, ax should contain either one or two (the number of stop bits you desire). This code converts that to zero or one and writes the resulting L.O. bit to the stop bit field of the LCR. Note that this code ignores the other bits in the ax register. This code reads the LCR, masks out the stop bit field, and then inserts the value the caller specifies into that field. Note the usage of the shl ax, 2 instruction; this requires an 80286 or later processor.

comStop         proc
                push    ax
                push    dx
                dec     ax              ;Convert 1 or 2 to 0 or 1.
                and     al, 1           ;Strip other bits.
                shl     ax, 2           ;position into bit #2.
                mov     ah, al          ;Save our output value.
                call    ComGetLCR       ;Read LCR value.
                and     al, 11111011b   ;Mask out Stop Bits bit.
                or      al, ah          ;Merge in new # of stop bits.
                call    ComSetLCR       ;Write result back to LCR.
                pop     dx
                pop     ax
                ret
comStop         endp

The ComSize routine sets the word size for data transmission. As usual, this code provides a "programmer friendly" interface to the 8250 SCC. On enter, you specify the number of bits (5, 6, 7, or 8) in the ax register, you do not have to worry an appropriate bit pattern for the 8250's LCR register. This routine will compute the appropriate bit pattern for you. If the value in the ax register is not appropriate, this code defaults to an eight bit word size.

ComSize         proc
                push    ax
                push    dx
                sub     al, 5           ;Map 5..8 -> 00b, 01b, 10b, 11b
                cmp     al, 3
                jbe     Okay
                mov     al, 3           ;Default to eight bits.
Okay:           mov     ah, al          ;Save new bit size.
                call    ComGetLCR       ;Read current LCR value.
                and     al, 11111100b   ;Mask out old word size.
                or      al, ah          ;Merge in new word size.
                call    ComSetLCR       ;Write new LCR value back.
                pop     dx
                pop     ax
                ret
comsize         endp

The ComParity routine initializes the parity options on the 8250. Unfortunately, there is little possibility of a "programmer friendly" interface to this routine, So this code requires that you pass one of the following values in the ax register:

ComParity Input Parameters
Value in AX Description
0 Disable parity.
1 Enable odd parity checking.
3 Enable even parity checking.
5 Enable stuck parity bit with value one.
7 Enable stuck parity bit with value zero.
comparity       proc
                push    ax
                push    dx

                shl     al, 3                   ;Move to final position in LCR.
                and     al, 00111000b           ;Mask out other data.
                mov     ah, al                  ;Save for later.
                call    ComGetLCR               ;Get current LCR value.
                and     al, 11000111b           ;Mask out existing parity bits.
                or      al, ah                  ;Merge in new bits.
                call    ComSetLCR               ;Write results back to the LCR.
                pop     dx
                pop     ax
                ret
comparity       endp

The next set of serial communication routines provide polled I/O support. These routines let you easily read characters from the serial port, write characters to the serial port, and check to see if there is data available at the input port or see if it is okay to write data to the output port. Under no circumstances should you use these routines when you've activated the serial interrupt system. Doing so may confuse the system and produce incorrect data or loss of data.

The ComRead routine is comparable to getc - it waits until data is available at the serial port, reads that data, and returns it in the al register. This routine begins by making sure we can access the Receive Data register (by clearing the baud rate divisor latch bit in the LCR).

ComRead         proc
                push    dx
                call    GetLCRCom
                push    ax              ;Save divisor latch access bit.
                and     al, 7fh         ;Select normal ports.
                call    SetLCRCom       ;Write LCR to turn off divisor reg.
WaitForChar:    call    GetLSRCom       ;Get data available bit from LSR.
                test    al, 1           ;Data Available?
                jz      WaitForChar     ;Loop until data available.
                mov     dx, comPort     ;Read the data from the input port.
                in      al, dx
                mov     dl, al          ;Save character
                pop     ax              ;Restore divisor access bit.
                call    SetLCRCom       ;Write it back to LCR.
                mov     al, dl          ;Restore output character.
                pop     dx
                ret
ComRead         endp

The ComWrite routine outputs the character in al to the serial port. It first waits until the transmitter holding register is empty, then it writes the output data to the output register.

ComWrite        proc
                push    dx
                push    ax
                mov     dl, al          ;Save character to output
                call    GetLCRCom       ;Switch to output register.
                push    ax              ;Save divisor latch access bit.
                and     al, 7fh         ;Select normal input/output ports
                call    SetLCRCom       ; rather than divisor register.
WaitForXmtr:    call    GetLSRCom       ;Read LSR for xmit empty bit.
                test    al, 00100000b   ;Xmtr buffer empty?
                jz      WaitForXmtr     ;Loop until empty.
                mov     al, dl          ;Get output character.
                mov     dx, ComPort     ;Store it in the ouput port to
                out     dx, al          ; get it on its way.
                pop     ax              ;Restore divisor access bit.
                call    SetLCRCom
                pop     ax
                pop     dx
                ret
ComWrite        endp

The ComTstIn and ComTstOut routines let you check to see if a character is available at the input port (ComTstIn) or if it is okay to send a character to the output port (ComTstOut). ComTstIn returns zero or one in al if data is not available or is available, respectively. ComTstOut returns zero or one in al if the transmitter register is full or empty, respectively.

ComTstIn        proc
                call    GetComLSR
                and     ax, 1           ;Keep only data available bit.
                ret
ComTstIn        endp


ComTstOut       proc
                push    dx
                call    ComGetLSR       ;Get the line status.
                test    al, 00100000b   ;Mask Xmitr empty bit.
                mov     al, 0           ;Assume not empty.
                jz      toc1            ;Branch if not empty.
                inc     ax              ;Set to one if it is empty.
toc1:           ret
ComTstOut       endp

The next set of routines the Standard Library supplies load and store the various registers on the 8250 SCC. Although these are all trivial routines, they allow the programmer to access these register by name without having to know the address. Furthermore, these routines all preserve the value in the dx register, saving some code in the calling program if the dx register is already in use.

The following routines let you read ("Get") the value in the LSR, MSR, LCR, MCR, IIR, and IER registers, returning said value in the al register. They let you write ("Set") the value in al to any of the LCR, MCR, and IER registers. Since these routines are so simple and straight-forward, there is no need to discuss each routine individually. Note that you should avoid calling these routines outside an SCC ISR while in interrupt mode, since doing so can affect the interrupt system on the 8250 SCC.

ComGetLSR       proc                    ;Returns the LSR value in the AL reg.
                push    dx
                mov     dx, comLSR      ;Select LSR register.
                in      al, dx          ;Read and return the LSR value.
                pop     dx
                ret
ComGetLSR       endp



ComGetMSR       proc                    ;Returns the MSR value in the AL reg.
                push    dx
                mov     dx, comMSR      ;Select MSR register.
                in      al, dx          ;Read and return MSR value.
                pop     dx
                ret
ComGetMSR       endp



ComSetMCR       proc                    ;Stores AL's value to the MCR reg.
                push    dx
                mov     dx, comMCR      ;Point at MCR register.
                out     dx, al          ;Output value in AL to MCR.
                pop     dx
                ret
ComSetMCR       endp



ComGetMCR       proc                    ;Stores value in AL into MCR reg.
                push    dx
                mov     dx, comMCR      ;Select MCR register.
                in      al, dx          ;Read value from MCR register into AL.
                pop     dx
                ret
ComGetMCR       endp



ComGetLCR       proc                    ;Return the LCR value in the AL reg.
                push    dx
                mov     dx, comLCR      ;Point at LCR register.
                in      al, dx          ;Read and return LCR value.
                pop     dx
                ret
ComGetLCR       endp



ComSetLCR       proc                    ;Write a new value to the LCR.
                push    dx
                mov     dx, comLCR      ;Point at LCR register.
                out     dx, al          ;Write value in AL to the LCR.
                pop     dx
                ret
ComSetLCR       endp



ComGetIIR       proc                    ;Return the value in the IIR.
                push    dx
                mov     dx, comIIR      ;Select IIR register.
                in      al, dx          ;Read IIR value into AL and return.
                pop     dx
                ret
ComGetIIR       endp



ComGetIER       proc                    ;Return IER value in AL.
                push    dx
                call    ComGetLCR       ;Need to select IER register by saving
                push    ax              ; the LCR value and then clearing the
                and     al, 7fh         ; baud rate divisor latch bit.
                call    ComSetLCR
                mov     dx, comIER      ;Address the IER.
                in      al, dx          ;Read current IER value.
                mov     dl, al          ;Save for now
                pop     ax              ;Retrieve old LCR value (divisor latch).
                call    ComSetLCR       ;Restore divisor latch
                mov     al, dl          ;Restore IER value
                pop     dx
                ret
ComGetIER       endp


ComSetIER       proc                    ;Writes value in AL to the IER.
                push    dx
                push    ax              ;Save AX's value.
                mov     ah, al          ;Save IER value to output.
                call    ComGetLCR       ;Get and save divsor access
                push    ax              ; bit.
                and     al, 7fh         ;Clear divisor access bit.
                call    ComSetLCR
                mov     al, ah          ;Retrieve new IER value.
                mov     dx, comIER      ;Select IER register
                out     dx, al          ;Output IER value.
                pop     ax              ;Restore divisor latch bit.
                call    ComSetLCR
                pop     ax
                pop     dx
                ret
ComSetIER       endp

The last set of serial support routines appearing in the Standard Library provide support for interrupt driven I/O. There are five routines in this section of the code: ComInitIntr, ComDisIntr, ComIntISR, ComIn, and ComOut. The ComInitIntr initializes the serial port interrupt system. It saves the old int 0Ch interrupt vector, initializes the vector to point at the ComIntISR interrupt service routine, and properly initializes the 8259A PIC and 8250 SCC for interrupt based operation. ComDisIntr undoes everything the ComDisIntr routine sets up; you need to call this routine to disable interrupts before your program quits. ComOut and ComIn transfer data to and from the buffers described in the variables section; the ComIntISR routine is responsible for removing data from the transmit queue and sending over the serial line as well as buffering up incoming data from the serial line.

The ComInitIntr routine initializes the 8250 SCC and 8259A PIC for interrupt based serial I/O. It also initializes the int 0Ch vector to point at the ComIntISR routine. One thing this code does not do is to provide break and critical error exception handlers. Remember, if the user hits ctrl-C (or ctrl-Break) or selects abort on an I/O error, the default exception handlers simply return to DOS without restoring the int 0Ch vector. It is important that your program provide exception handlers that will call ComDisIntr before allowing the system to return control to DOS. Otherwise the system may crash when DOS loads the next program into memory.

ComInitIntr     proc
                pushf                   ;Save interrupt disable flag.
                push    es
                push    ax
                push    dx

; Turn off the interrupts while we're doing this.

                cli

; Save old interrupt vector. Obviously, you must change the following code
; to save and set up the int 0Bh vector if you want to access COM2: rather
; than the COM1: port.

                xor     ax, ax                  ;Point at interrupt vectors
                mov     es, ax
                mov     ax, Int0Cofs
                mov     word ptr OldIInt0C, ax
                mov     ax, Int0Cseg
                mov     word ptr OldInt0C+2, ax

; Point int 0ch vector at our interrupt service routine (see note above
; concerning switching to COM2:).

                mov     ax, cs
                mov     Int0Cseg, ax
                mov     ax, offset ComIntISR
                mov     Int0Cofs, ax

; Clear any pending interrupts:

                call    ComGetLSR               ;Clear Receiver line status
                call    ComGetMSR               ;Clear CTS/DSR/RI Interrupts
                call    ComGetIIR               ;Clear xmtr empty interrupt
                mov     dx, ComPort
                in      al, dx                  ;Clear data available intr.

; Clear divisor latch access bit. WHILE OPERATING IN INTERRUPT MODE, THE
; DIVISOR ACCESS LATCH BIT MUST ALWAYS BE ZERO. If for some horrible reason
; you need to change the baud rate in the middle of a transmission (or while
; the interrupts are enabled) clear the interrupt flag, do your dirty work,
; clear the divisor latch bit, and finally restore interrupts.

                call    ComGetLCR               ;Get LCR.
                and     al, 7fh                 ;Clear divisor latch bit.
                call    ComSetLCR               ;Write new LCR value back.


; Enable the receiver and transmitter interrupts. Note that this code
; ignores error and modem status change interrupts.

                mov     al, 3           ;Enable rcv/xmit interrupts
                call    SetIERCom

; Must set the OUT2 line for interrupts to work.
; Also sets DTR and RTS active.

                mov     al, 00001011b
                call    ComSetMCR

; Activate the COM1 (int 0ch) bit in the 8259A interrupt controller chip.
; Note: you must change the following code to clear bit three (rather than
; four) to use this code with the COM2: port.

                in      al, 21h         ;Get 8259A interrupt enable value.
                mov     i8259a, al      ;Save interrupt enable bits.
                and     al, 0efh        ;Bit 4=IRQ 4 = INT 0Ch
                out     21h, al         ;Enable interrupts.

                pop     dx
                pop     ax
                pop     es
                popf                    ;Restore interrupt disable flag.
                ret
ComInitIntr     endp

The ComDisIntr routine disables serial interrupts. It restores the original value of the 8259A interrupt enable register, it restores the int 0Ch interrupt vector, and it masks interrupts on the 8250 SCC. Note that this code assumes that you have not changed the interrupt enable bits in the 8259 PIC since calling ComInitIntr. It restores the 8259A's interrupt enable register with the value from the 8259A interrupt enable register when you originally called ComInitIntr.

It would be a complete disaster to call this routine without first calling ComInitIntr. Doing so would patch the int 0Ch vector with garbage and, likewise, restore the 8259A interrupt enable register with a garbage value. Make sure you've called ComInitIntr before calling this routine. Generally, you should call ComInitIntr once, at the beginning of your program, and call ComDisIntr once, either at the end of your program or within the break or critical error exception routines.

ComDisIntr      proc
                pushf
                push    es
                push    dx
                push    ax

                cli                     ;Don't allow interrupts while messing
                xor     ax, ax          ; with the interrupt vectors.
                mov     es, ax          ;Point ES at interrupt vector table.

; First, turn off the interrupt source at the 8250 chip:

                call    ComGetMCR       ;Get the OUT 2 (interrupt enable) bit.
                and     al, 3           ;Mask out OUT 2 bit (masks ints)
                call    ComSetMCR       ;Write result to MCR.

; Now restore the IRQ 4 bit in the 8259A PIC. Note that you must modify this
; code to restore the IRQ 3 bit if you want to support COM2: instead of COM1:

                in      al, 21h         ;Get current 8259a IER value
                and     al, 0efh        ;Clear IRQ 4 bit (change for COM2:!)
                mov     ah, i8259a      ;Get our saved value
                and     ah, 1000b       ;Mask out com1: bit (IRQ 4).
                or      al, ah          ;Put bit back in.
                out     21h, al

; Restore the interrupt vector:

                mov     ax, word ptr OldInt0C
                mov     Int0Cofs, ax
                mov     ax, word ptr OldInt0C+2
                mov     Int0Cseg, ax

                pop     ax
                pop     dx
                pop     es
                popf
                ret
ComDisIntr      endp

The following code implements the interrupt service routine for the 8250 SCC. When an interrupt occurs, this code reads the 8250 IIR to determine the source of the interrupt. The Standard Library routines only provide direct support for data available interrupts and transmitter holding register empty interrupts. If this code detects an error or status change interrupt, it clears the interrupt status but takes no other action. If it detects a receive or transmit interrupt, it transfers control to the appropriate handler.

The receiver interrupt handler is very easy to implement. All this code needs to do is read the character from the Receive Register and add this character to the input buffer. The only catch is that this code must ignore any incoming characters if the input buffer is full. An application can access this data using the ComIn routine that removes data from the input buffer.

The transmit handler is somewhat more complex. The 8250 SCC interrupts the 80x86 when it is able to accept more data for transmission. However, the fact that the 8250 is ready for more data doesn't guarantee there is data ready for transmission. The application produces data at its own rate, not necessarily at the rate that 8250 SCC wants it. Therefore, it is quite possible for the 8250 to say "give me more data" but the application has not produced any. Obviously, we should not transmit anything at that point. Instead, we have to wait for the application to produce more data before transmission resumes.

Unfortunately, this complicates the driver for the transmission code somewhat. With the receiver, the interrupt always indicates that the ISR can move data from the 8250 to the buffer. The application can remove this data at any time and the process is always the same: wait for a non-empty receive buffer and then remove the first item from the buffer. Unfortunately, we cannot simply do the converse operation when transmitting data. That is, we can't simply store data in the transmit buffer and leave it up to the ISR to remove this data. The problem is that the 8250 only interrupts the system once when the transmitter holding register is empty. If there is no data to transmit at that point, the ISR must return without writing anything to the transmit register. Since there is no data in the transmit buffer, there will be no additional transmitter interrupts generated, even when there is data added to the transmit buffer. Therefore, the ISR and the routine responsible for adding data to the output buffer (ComOut) must coordinate their activities. If the buffer is empty and the transmitter is not currently transmitting anything, the ComOut routine must write its data directly to the 8250. If the 8250 is currently transmitting data, ComOut must append its data to the end of the output buffer. The ComIntISR and ComOut use a flag, TestBuffer, to determine whether ComOut should write directly to the serial port or append its data to the output buffer. See the following code and the code for ComOut for all the details.

ComIntISR       proc    far
                push    ax
                push    bx
                push    dx
TryAnother:     mov     dx, ComIIR
                in      al, dx          ;Get interrupt id value.
                test    al, 1           ;Any interrupts left?
                jnz     IntRtn          ;Quit if no interrupt pending.
                cmp     al, 100b        ;Since only xmit/rcv ints are
                jnz     ReadCom1        ; active, this checks for rcv int.
                cmp     al, 10b         ;This checks for xmit empty intr.
                jnz     WriteCom1

; Bogus interrupt? We shouldn't ever fall into this code because we have
; not enabled the error or status change interrupts. However, it is possible
; that the application code has gone in and tweakd the IER on the 8250.
; Therefore, we need to supply a default interrupt handler for these conditions.
; The following code just reads all the appropriate registers to clear any
; pending interrupts.

                call    ComGetLSR       ;Clear receiver line status
                call    ComGetMSR       ;Clear modem status.
                jmp     TryAnother      ;Check for lower priority intr.

; When there are no more pending interrupts on the 8250, drop down and
; and return from this ISR.

IntRtn:         mov     al, 20h         ;Acknowledge interrupt to the
                out     20h, al         ; 8259A interrupt controller.
                pop     dx
                pop     bx
                pop     ax
                iret

; Handle incoming data here:
; (Warning: This is a critical region. Interrupts MUST BE OFF while executing
; this code. By default, interrupts are off in an ISR. DO NOT TURN THEM ON
; if you modify this code).

ReadCom1:       mov     dx, ComPort     ;Point at data input register.
                in      al, dx          ;Get the input char

                mov     bx, InHead      ;Insert the character into the
                mov     [bx], al        ; serial input buffer.

                inc     bx              ;Increment buffer ptr.
                cmp     bx, offset InpBufEnd
                jb      NoInpWrap
                mov     bx, offset InpBuf
NoInpWrap:      cmp     bx, InTail      ;If the buffer is full, ignore this
                je      TryAnother      ; input character.
                mov     InHead, bx
                jmp     TryAnother      ;Go handle other 8250 interrupts.



; Handle outgoing data here (This is also a critical region):

WriteCom1:      mov     bx, OutTail     ;See if the buffer is empty.
                cmp     bx, OutHead
                jne     OutputChar      ;If not, output the next char.

; If head and tail are equal, simply set the TestBuffer variable to zero
; and quit. If they are not equal, then there is data in the buffer and
; we should output the next character.

                mov     TestBuffer, 0
                jmp     TryAnother      ;Handle other pending interrupts.

; The buffer pointers are not equal, output the next character down here.

OutputChar:     mov     al, [bx]        ;Get the next char from the buffer.
                mov     dx, ComPort     ;Select output port.
                out     dx, al          ;Output the character

; Okay, bump the output pointer.

                inc     bx
                cmp     bx, offset OutBufEnd
                jb      NoOutWrap
                mov     bx, offset OutBuf
NoOutWrap:              mov     OutTail, bx
                jmp     TryAnother
ComIntISR       endp

These last two routines read data from the serial input buffer and write data to the serial output buffer. The ComIn routine, that handles the input chore, waits until the input buffer is not empty. Then it removes the first available byte from the input buffer and returns this value to the caller.

ComIn           proc
                pushf                   ;Save interrupt flag
                push    bx
                sti                     ;Make sure interrupts are on.
TstInLoop:      mov     bx, InTail      ;Wait until there is at least one
                cmp     bx, InHead      ; character in the input buffer.
                je      TstInLoop
                mov     al, [bx]        ;Get next char.
                cli                     ;Turn off ints while adjusting
                inc     bx              ; buffer pointers.
                cmp     bx, offset InpBufEnd
                jne     NoWrap2
                mov     bx, offset InpBuf
NoWrap2:        mov     InTail, bx
                pop     bx
                popf                    ;Restore interrupt flag.
                ret
ComIn           endp

The ComOut must check the TestBuffer variable to see if the 8250 is currently busy. If not (TestBuffer equals zero) then this code must write the character directly to the serial port and set TestBuffer to one (since the chip is now busy). If the TestBuffer contains a non-zero value, this code simply appends the character in al to the end of the output buffer.

ComOut          proc    far
                pushf
                cli                             ;No interrupts now!
                cmp     TestBuffer, 0           ;Write directly to serial chip?
                jnz     BufferItUp              ;If not, go put it in the buffer.

; The following code writes the current character directly to the serial port
; because the 8250 is not transmitting anything now and we will never again
; get a transmit holding register empty interrupt (at least, not until we
; write data directly to the port).

                push    dx
                mov     dx, ComPort             ;Select output register.
                out     dx, al                  ;Write character to port.
                mov     TestBuffer, 1           ;Must buffer up next char.
                pop     dx
                popf                            ;Restore interrupt flag.
                ret

; If the 8250 is busy, buffer up the character here:

BufferItUp:     push    bx
                mov     bx, OutHead             ;Pointer to next buffer position.
                mov     [bx], al                ;Add the char to the buffer.

; Bump the output pointer.

                inc     bx
                cmp     bx, offset OutBufEnd
                jne     NoWrap3
                mov     bx, offset OutBuf
NoWrap3:        cmp     bx, OutTail             ;See if the buffer is full.
                je      NoSetTail               ;Don't add char if buffer is full.
                mov     OutHead, bx             ;Else, update buffer ptr.
NoSetTail:      pop     bx
                popf                            ;Restore interrupt flag
                ret
ComOut          endp

Note that the Standard Library does not provide any routines to see if there is data available in the input buffer or to see if the output buffer is full (comparable to the ComTstIn and ComTstOut routines). However, these are very easy routines to write; all you need do is compare the head and tail pointers of the two buffers. The buffers are empty if the head and tail pointers are equal. The buffers are full if the head pointer is one byte before the tail pointer (keep in mind, the pointers wrap around at the end of the buffer, so the buffer is also full if the head pointer is at the last position in the buffer and the tail pointer is at the first position in the buffer).

Chapter Twenty Two (Part 1)

Table of Content

Chapter Twenty Three  

Chapter Twenty Two: The PC Serial Ports (Part 2)
30 SEP 1996