Lesson 11

\       Lesson 11 - Terminal Program Using Interrupts
\       The Forth Course
\       by Richard E. Haskell
\          Dept. of Computer Science and Engineering
\          Oakland University, Rochester, MI 48309

comment:



                                Lesson 11

                    TERMINAL PROGRAM USING INTERRUPTS


                11.1  8086/8088 INTERRUPTS               11-2

                11.2  THE 8250 ACE CHIP                  11-3

                11.3  A QUEUE DATA STRUCTURE             11-5

                11.4  SENDING CHARACTERS TO THE SCREEN
                      AND/OR TO DISK                     11-8

                11.5  DOWNLOADING FILES                 11-10

                11.6  MAIN TERMINAL PROGRAM             11-12




























11.1  8086/8088 INTERRUPTS

        In this lesson we will write a terminal program using interrupts
        that we can use to communicate with other computers or to
        download Forth code to Forth chips such as the 68HC11 single-chip
        microcomputer that contains Max-Forth.

        We will want to communicate at baud rates up to 9600 baud.  This
        means that we will need to use interrupts to store incoming
        characters without losing them while scrolling the screen.  We
        will write an interrupt service routine that is called every time
        a character is received in the serial port.  This interrupt service
        routine will read the character and store it in a queue.  The main
        terminal program will then alternate between checking the keyboard
        for key pressings and checking the queue for received characters.
        When a key is pressed the character will be transmitted out the
        serial port.  When a character is in the queue (i.e. has been
        received in the serial port) it will be displayed on the screen
        and, optionally, stored on disk.

        The segment and offset addresses of the interrupt service routine
        must be stored in the interrupt vector table at the beginning of
        segment zero in memory.  The DOS functions 25H (set interrupt
        vector) and 35H (get interrupt vector) can be used for this purpose.
        The following Forth words make this easy.
comment;

PREFIX  HEX
\       Get interrupt vector
CODE get.int.vector   ( int# -- seg offset )
        POP AX
        PUSH ES
        PUSH BX                 \ AL = interrupt number
        MOV AH, # 35            \ DOS service 35H
        INT 21                  \ ES:BX = segment:offset
        MOV DX, ES              \  of interrupt handler
        MOV AX, BX
        POP BX
        POP ES
        2PUSH
        END-CODE

\       Set interrupt vector
CODE set.int.vector   ( segment offset int# -- )
        POP AX                  \ AL = interrupt number
        POP DX                  \ DX = offset addr
        POP BX                  \ BX = segment addr
        MOV AH, # 25            \ DOS service 25H
        PUSH DS                 \ save DS
        MOV DS, BX             \ DS:DX -> int handler
        INT 21                  \ DOS INT 21H
        POP  DS                 \ restore DS
        NEXT
        END-CODE

\       Store interrupt vector of routine at addr
: store.int.vector      ( addr int# -- )
        ?CS: -ROT set.int.vector ;

\       We will need words from Lessons 7, 8 and 10.  Therefore,
\       we will FLOAD these files.
DECIMAL

fload lesson7
fload lesson8
fload lesson10

comment:

11.2  THE 8250 ACE CHIP
        Serial communication is handled by the 8250 Asynchronous
        Communication Element (ACE) chip.  The interrupt line from
        this chip goes to IRQ4 of the Priority Interrupt Controller (PIC)
        chip for COM1 and to IRQ3 of the PIC for COM2.  OUT2 of the modem
        control register of the 8250 must be set to enable the output
        buffer of the 8250 IRQ line.
comment;

HEX
300     CONSTANT        COM1            \ base address for COM1
200     CONSTANT        COM2            \ base address for COM2
0C      CONSTANT        INT#1           \ interrupt number for COM1
0B      CONSTANT        INT#2           \ interrupt number for COM2
EF      CONSTANT        ENABLE4         \ interrupt 4 enable mask
10      CONSTANT        DISABLE4        \ interrupt 4 disable mask
F7      CONSTANT        ENABLE3         \ interrupt 3 enable mask
08      CONSTANT        DISABLE3        \ interrupt 3 disable mask
\       Default COM1
COM1    VALUE   COM                     \ current COM base address
INT#1   VALUE   INT#                    \ interrupt # for current COM
ENABLE4 VALUE   ENABLE                  \ enable mask for current COM
DISABLE4 VALUE  DISABLE                 \ disable mask for current COM

\       The following values are added to the base COM address to obtain
\       the corresponding register addresses:
F8      CONSTANT        txdata          \ transmit data reg (write only)
F8      CONSTANT        recdat          \ receive data reg (read only)
FC      CONSTANT        mcr             \ modem control reg
F9      CONSTANT        ier             \ interrupt enable reg
FD      CONSTANT        lsr             \ line status reg
21      CONSTANT        imask           \ mask reg in PIC
20      CONSTANT        eoi             \ end of int value
20      CONSTANT        ocw2            \ PIC ocw2

VARIABLE        int.vec.addr            \ save int vector offset address
VARIABLE        int.vec.seg             \ save int vector segment address
DECIMAL



\       We will use the BIOS INT 14H (20 decimal) initialize communications
\       port routine (AH = 0) to set the baud rate.  This MUST be done
\       before the modem control register bits are set to enable interrupts
\       because the INT 14H call will undo them!

\       The following table contains the control register masks
\       for baud rates of 300, 1200, 2400, 4800 and 9600
\       with no parity, 8 data bits and 1 stop bit.

CREATE baud.table  67 , 131 , 163 , 195 , 227 ,
\       Index   Baud rate
\         0         300
\         1        1200
\         2        2400
\         3        4800
\         4        9600

CODE INIT-COM   ( mask -- )
                POP     AX
                MOV     AH, # 0
                MOV     DX, # 0
                INT     20
                NEXT
                END-CODE

\       Default 9600 baud
\       Modify this word if you want to change the baud rate from the screen.
: get.baud#     ( -- n )
                4 ;

: set.baud.rate     ( -- )
                get.baud# 2*
                baud.table + @
                INIT-COM ;

comment:



















11.3  A QUEUE DATA STRUCTURE
        A circular queue will be used to store the received characters
        in an interrupt service routine

        The following pointers are used to define the queue
comment;

VARIABLE front          \ pointer to front of queue (oldest data at front+1)
VARIABLE rear           \ pointer to rear of queue (most recent data at rear)
VARIABLE qmin           \ pointer to first byte in queue
VARIABLE qmax           \ pointer to last byte in queue
VARIABLE   qbuff.seg    \ segment of queue
10000   CONSTANT qsize  \ size of queue in bytes

\       Initialize queue
: initq         ( -- )
                qsize alloc.mem qbuff.seg !     \ allocate memory for queue
                0 front !                       \ front = 0
                0 rear !                        \ rear = 0
                0 qmin !                        \ qmin = 0
                qsize 1- qmax ! ;               \ qmax = qsize - 1

\       Check queue
: checkq        ( -- n tf | ff )
                front @ rear @ <>               \ if front = rear
                IF                              \ then empty
                   INLINE
                      CLI                       \ disable interrupts
                      NEXT
                   END-INLINE
                   1 front +!                   \ inc front
                   front @ qmax @ >             \ if front > qmax
                   IF
                      qmin @ front !            \ then front = qmin
                   THEN
                   qbuff.seg @ front @ C@L      \ get byte
                   TRUE                         \ set true flag
                   INLINE
                      STI                       \ enable interrupts
                      NEXT
                   END-INLINE
                ELSE
                   FALSE                        \ set false flag
                THEN ;











\       Store byte in AL in queue
LABEL   qstore
                PUSH    SI
                PUSH    ES
                MOV     SI, qbuff.seg
                MOV     ES, SI                  \ ES = qbuff.seg
                INC     rear WORD               \ inc rear
                MOV     SI, rear                \ if rear > qmax
                CMP     SI, qmax
                JBE     2 $
                MOV     SI, qmin                \ then rear = qmin
                MOV     rear SI
2 $:            CMP     SI, front               \ if front = rear
                JNE     4 $                     \ then full
                DEC     SI                      \    dec rear
                CMP     SI, qmin                \    if rear < qmin
                JAE     3 $                     \    then rear = qmax
                MOV     SI, qmax
                MOV     rear SI
3 $:            POP     ES
                POP     SI
                RET
4 $:            MOV     ES: 0 [SI], AL          \ else store at rear
                POP     ES
                POP     SI
                RET
                END-CODE

\       Interrupt service routine
\       This routine gets data from the receive serial port
\       and stores it in the queue.
LABEL INT.SRV    ( -- )
                PUSH    AX
                PUSH    DX
                PUSH    DS
                MOV     AX, CS
                MOV     DS, AX                  \ DS = CS
                MOV     DX, # COM               \ if data is ready
                ADD     DX, # lsr
                IN      AL, DX
                TEST    AL, # 1
                JE      1 $
                MOV     DX, # COM
                ADD     DX, # recdat
                IN      AL, DX                  \ read it
                CALL    qstore
1 $:            MOV     AL, # eoi
                MOV     DX, # ocw2
                OUT     DX, AL                  \ clear eoi
                POP     DS
                POP     DX
                POP     AX
                IRET
                END-CODE

\       Set up interrupts
: int.setup             ( -- )
                12 COM mcr + PC!                  \ modem cr out2 lo
                1 COM ier + PC!                   \ enable recv int
                INT# get.int.vector               \ save old int vector
                int.vec.addr !  int.vec.seg !
                INT.SRV INT# store.int.vector ;   \ set new int vector

\       Terminal initialization routine
: init.term             ( -- )
                initq                           \ initialize queue
                int.setup                       \ set up interrupts
                imask PC@
                ENABLE AND                      \ enable irq4 (COM1 default)
                imask PC! ;

: disable.term          ( -- )
                imask PC@
                DISABLE OR                      \ disable irq4 (COM1 default)
                imask PC!
                0 COM mcr + PC!                 \ 0 -> modem control reg
                int.vec.seg @                   \ restore original
                int.vec.addr @                  \  interrupt vector
                INT# set.int.vector ;
comment:






























11.4  SENDING CHARACTERS TO THE SCREEN AND/OR TO DISK
        Characters in the queue will be displayed on the screen and,
        optionally, sent to a disk file.
comment;

FALSE   VALUE   ?>disk          \ flag to "send to disk"
0       VALUE   col.at          \ saved cursor position
0       VALUE   row.at

VARIABLE t_handle               \ terminal file handle
CREATE edit_buff  70 ALLOT      \ temporary edit buffer

: $HCREATE      ( addr -- f )   \ create file for counted string at addr
                SEQHANDLE HCLOSE DROP
                SEQHANDLE $>HANDLE
                SEQHANDLE HCREATE ;

: file.open.error       ( -- )
                33 12 65 14 box&fill
                ." Could not open file!!"
                KEY DROP ;

\       The following word will display a window on the screen in which
\       to enter a filename, which will then be opened.  Data coming in
\       the serial port will be sent to this file.  This word will be
\       called by pressing function key F1.

: select.nil.file       ( -- )
                20 4 60 7 box&fill
                ." Enter a filename"
                "  " ">$
                edit_buff OVER C@ 1+ CMOVE
                21 6 edit_buff 30 lineeditor
                IF
                   edit_buff $HCREATE
                   IF
                      file.open.error
                   ELSE
                      SEQHANDLE >HNDLE @
                      DUP handl ! t_handle !
                      TRUE !> ?>disk
                   THEN
                THEN ;

: >term         ( -- )
                t_handle @ handl ! ;

\       Pressing function key F1 will turn 'data capture' on
: disk.on.nil       ( -- )
                IBM-AT? !> row.at !> col.at
                SAVESCR
                select.nil.file
                RESTSCR
                col.at row.at AT ;

\       Pressing function key F2 will turn 'data capture' off
: disk.off      ( -- )
                t_handle @ ?DUP
                IF
                   close.file
                   0 t_handle !
                THEN
                FALSE !> ?>disk ;

\       Transmit ascii code out serial port
: XMT           ( ascii -- )
                COM                     \ use base address in COM
                BEGIN
                   DUP lsr +            \ wait for bit 5 in line status
                   PC@ 32 AND           \ register (TDRE) to be set
                UNTIL
                txdata + PC! ;          \ send data

\       Pressing CTRL P will toggle the printer on and off
: ?PRINT        ( -- )
                PRINTING C@ NOT PRINTING C! ;

\       Send character to the screen
: do.emit       ( n -- )
                DUP 13 =                \ if CR
                IF
                   DROP CR              \ do a carriage return
                ELSE
                   DUP 32 >=            \ ignore other control characters
                   IF
                      EMIT
                   ELSE
                      DROP
                   THEN
                THEN ;

: ?EMIT         ( n -- )
                127 AND                         \ mask parity bit
                DUP 13 =                        \ ignore control char
                OVER 10 = OR                    \  other than CR and LF
                OVER 32 >= OR
                IF
                   ?>disk                       \ if data capture on
                   IF
                      DUP >term send.byte       \    send to disk
                   THEN
                   do.emit                      \ send to screen
                ELSE
                   DROP
                THEN ;

comment:



11.5  DOWNLOADING FILES

        The following words can be used to download a file containing
        MaxForth code to the 68HC11.  MaxForth will load a line at a
        time, compiling the words in the dictionary.  After loading a
        line it will send a line-feed (ASCII 10) back to the PC.
comment;

VARIABLE        wait.count

\       Transmit a string given its address and length

: xmt.str       ( addr cnt -- )    \ XMT string + CR
                0 DO
                   DUP I + C@
                   XMT
                LOOP
                DROP
                13 XMT ;

\       Wait for a particular character to be received

: wait.for      ( ascii -- )
                0 wait.count !
                BEGIN
                   checkq                       \ char n tf | char ff
                   IF                           \ char n    | char
                      DUP ?EMIT                 \ char n
                      OVER =                    \ char f
                      0 wait.count !
                   ELSE
                      1 wait.count +! FALSE     \             char ff
                   THEN
                   wait.count @ 32000 =         \ char f f
                   IF
                      CONTROL G EMIT 2DROP      \ beep -- no response
                      CR ." No response..."
                      KEY DROP
                      2R> 2DROP                 \ exit wait.for
                      2R> 2DROP                 \ exit file.download
                      EXIT                      \ exit DO-KEY
                   THEN
                UNTIL
                DROP ;











\       Download a file to the 68HC11

: file.download         ( -- )
                GETFILE
                DARK
                IF
                   $HOPEN
                   IF
                      file.open.error
                   ELSE
                      ." File: " .SEQHANDLE CR
                      BEGIN
                         LINEREAD COUNT 2-              \ addr cnt
                         OVER C@ 26 = NOT               \ while not EOF
                      WHILE
                         xmt.str                        \ send line
                         10 wait.for                    \ wait for LF
                      REPEAT
                      CLOSE
                   THEN
                ELSE
                   2R> 2DROP
                   EXIT                                 \ exit DO-KEY
                THEN ;

comment:





























11.6  MAIN TERMINAL PROGRAM

        Pressing the ESC key will exit the terminal word HOST
comment;

: ESC.HOST      ( -- )
                disable.term                    \ disable all interrupts
                disk.off                        \ close file if necessary
                qbuff.seg @ release.mem         \ release queue buffer
                DARK
                ABORT ;

\       This is the jump table for all key pressings

EXEC.TABLE DO-KEY
        CONTROL P | ?PRINT     ( PRINTER ON/OFF )
              27  | ESC.HOST        ( ESCAPE KEY )
              187 | disk.on.nil     ( F1 )     188 | disk.off     ( F2 )
              189 | file.download   ( F3 )     190 | UNUSED       ( F4 )
              191 | UNUSED          ( F5 )     192 | UNUSED       ( F6 )
              193 | UNUSED          ( F7 )     194 | UNUSED       ( F8 )
              195 | UNUSED          ( F9 )     196 | UNUSED       ( F10 )

              199 | UNUSED      ( HOME )   200 | UNUSED       ( UP )
              201 | UNUSED      ( PUP )    203 | UNUSED       ( LEFT )
              205 | UNUSED      ( RIGHT )  207 | UNUSED       ( END )
              208 | UNUSED      ( DOWN )   209 | UNUSED       ( PGDN )
              210 | UNUSED      ( INS  )   211 | UNUSED       ( DEL  )
           DEFAULT|  XMT


: T-LINK        ( -- )
                set.baud.rate
                CURSOR-ON
                FALSE !> ?>disk
                DARK
                ." 4thterm is on-line..." CR CR
                init.term ;

\       To run the terminal program, type HOST

: HOST          T-LINK
                BEGIN
                   KEY?
                   IF
                      KEY DO-KEY
                   THEN
                   checkq
                   IF
                      ?EMIT
                   THEN
                AGAIN ;