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