\       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 8 =              \ keep backspace

                   OVER 32 >= OR        \ 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 8 = OR                     \  or backspace

                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@

                   DUP do.emit XMT

                LOOP

                DROP

                13 XMT ;

¡@

\       Wait for one of two particular characters to be received

¡@

: wait.for      ( ascii -- )

                0 wait.count !

                BEGIN

                   checkq                       \ asc n tf | asc ff

                   IF                           \ asc n    | asc

                      DUP ?EMIT                 \ asc n

                      OVER =                    \ asc f

                      0 wait.count !

                   ELSE

                      1 wait.count +! FALSE     \             char ff

                   THEN

       FALSE \            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 ;

¡@

¡@

¡@

¡@

¡@

¡@

¡@

¡@

\       Empty the queue buffer

: mt.qbuff      ( -- )

                BEGIN

                   checkq

                WHILE

                   ?EMIT

                REPEAT ;

¡@

¡@

\       Download a file to the 68HC11

¡@

: file.download         ( -- )

                GETFILE

                DARK

                IF

                   $HOPEN

                   IF

                      file.open.error

                   ELSE

                      ." File: " .SEQHANDLE CR

                      SEQHANDLE >HNDLE @ handl !

                      BEGIN

                         get.next.byte

                         eof @ NOT

                      WHILE

                         DUP XMT

                         BEGIN

                           CHECKQ

                         UNTIL

                         ?EMIT

                         13 =                     \ IF CR, THEN WAIT FOR

                         IF                       \ LF TO BE SENT BACK

\                           GET.NEXT.BYTE DROP     \ DROP LF

                           BEGIN

                             BEGIN

                               CHECKQ

                             UNTIL

                             DUP DUP ?EMIT

                             10 = SWAP 63 = OR

                           UNTIL

                         THEN

                         mt.qbuff                        \ empty qbuff

                      REPEAT

                      CLOSE

                   THEN

                ELSE

                   2R> 2DROP

                   EXIT                                 \ exit DO-KEY

                THEN ;

¡@

: s-rec.dwload         ( -- )

                GETFILE

                DARK

                IF

                   $HOPEN

                   IF

                      file.open.error

                   ELSE

                      ." File: " .SEQHANDLE CR

                      SEQHANDLE >HNDLE @ handl !

                      " LOAD T" xmt.str

                      BEGIN

                         handl @

                         1 PAD read.file

                      WHILE

                         PAD C@ DUP XMT

                         do.emit

                         mt.qbuff                        \ empty qbuff

                      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