>'BOOK1P - uSEFUL sUBROUTINES
 
    iN THIS SECTION, i WILL PASS ALONG TO YOU SOME VERY USEFUL SUBROUTINES THAT YOU MAY USE IN YOUR OWN PROGRAMS.  sOME ARE SIMPLE AND EFFECIENT, AND SOME ARE VERY COMPLEX.  tHESE ARE OFFERED MAINLY AS SAMPLES, AND DO NOT REPRESENT THE UTMOST IN EFFECIENT CODE, BUT ARE REPRESENTED IN SUCH A MANNER TO BE MOST USEFUL TO THE NOVICE PROGRAMMER.
 
                   * iNTERRUPTS *
    i WILL BEGIN WITH THE INTERRUPT SERVICE BECAUSE IT CAN BE USED TO SERVICE MOST OF THE SLOW DEVICES IN A VERY EFFICIENT MANNER.  dUE TO THE NATURE OF THE MASKABLE INTERRUPTS, AND THEIR INDEPENDENCE TO THE "FOREGROUND" OPERATING CODE, THEY ARE VERY SUITABLE FOR EXTENSIVE USE.
    nORMALLY, THE USER WILL WISH TO HAVE SEVERAL INTERRUPT ROUTINES ACTIVE AT THE SAME TIME.  i AM VERY FOND OF TABLES, AND SO WE WILL USE ONE HERE TO SET UP AN INTERRUPT TASK THAT WILL EXECUTE SEVERAL SUBROUTINES:
 
table    dw        task1      ;ROUTINE #1 (REAL TIME CLOCK)
         dw        task2      ;ROUTINE #2 (CLOCK DISPLAY)
         dw        task3      ;ROUTINE #3 (PRINTER SPOOLER)
         dw        task4      ;ROUTINE #4 (TYPE AHEAD KEYBOARD)
         dw        task5      ;ROUTINE #5 (PROGRAM COUNTER TRACE)
         dw        0          ;TERMINATOR WORD
 
    uSING TABLES, IT IS EASY FOR US TO ADD OR DELETE ROUTINES INTO THE INTERRUPT TASKING PROCEEDURE.  nOW WE NEED TO "SETUP" OUR INTERRUPTS TO POINT TO OUR OWN ROUTINES.
 
setup    di                   ;DISABLE FOR SETUP
         im        1          ;SETUP FOR INTERRUPT MODE 1
         ld        hl,task    ;INTERRUPT SERVICE SUBROUTINE
         ld        (4013h),hl ;SETUP ADDRESS FOR rst 38h
         ld        a,0c3h     ;jp OPCODE
         ld        (4012h),a  ;SETUP JUMP TO OUR ROUTINE
         ei                   ;CAN ENABLE NOW
         ret                  ;ALL SETUP NOW
 
    nOW THE MASKABLE INTERRUPTS HAVE BEEN SETUP TO EXECUTE task EACH TIME A REAL TIME CLOCK INTERRUPT OCCURS.  iT IS THE JOB OF task TO CALL ALL THE ROUTINES IN THE TASK table, AND SAVE ALL REGISTERS BEFORE AND AFTER THEIR EXECUTION.
 
task     ex        (sp),hl    ;GET THE CALLER ADDRESS
; AT INTERRUPT TIME, THE CURRENT pc IS PUSHED ONTO THE STACK
;   AND A rst 38h OPCODE IS EXECUTED.  tHE ABOVE INSTRUCTION
;   WILL LOAD THE ADDRESS OF THE FOREGROUND TASK THAT WAS
;   EXECUTING WHEN THE INTERRUPT WAS GENERATED INTO hl.
         ld        (trace),hl ;SAVE FOR task5 - TRACE
         ex        (sp),hl    ;EVERYTHING AS IT WAS
         push      af         ;MUST SAVE ALL REGISTERS USED
         if        mod1
         ld        a,(37e0h)  ;READ INTERRUPT LATCH
         bit       7,a        ;SEE IF rtc INTERRUPT
         endif
         if        mod3
         in        a,(0e0h)   ;READ INTERRUPT LATCH
         cpl                  ;REVERSE BITS (0 = INTERRUPT)
         bit       2,a        ;SEE IF rtc INTERRUPT
         endif
         jr        z,tskret   ;NOT A rtc INTERRUPT
         push      hl         ;SAVE ALL REGISTERS
         push      de
         push      bc
         ld        hl,table   ;TABLE OF ROUTINES TO EXECUTE
loop     ld        a,(hl)     ;GET LSB ADDRESS
         inc       hl         ;POINT TO NEXT ADDRESS
         ld        d,(hl)     ;GET MSB ADDRESS
         inc       hl         ;hl => NEXT ENTRY
         ld        e,a        ;de = ADDRESS TO CALL
         or        d          ;TERMINATOR?
         jr        z,tskpop   ;DONE IF YES
         ld        (call),de  ;SAVE AHEAD INTO CODE
         push      hl         ;SAVE TABLE ADDRESS
         call      0          ;CALL THE SUBROUTINE
call     equ       $-2        ;POINT TO THE CALL VECTOR
         pop       hl         ;GET TABLE BACK
         jr        loop       ;DO THE NEXT ONE
tskpop   pop       bc         ;RESTORE ALL REGISTERS
         pop       de
         pop       hl
tskret
         if        mod1
         ld        a,(37e0h)  ;CLEAR INTERRUPT LATCH
         endif
         if        mod3
         in        a,(0ech)   ;CLEAR INTERRUPT LATCH
         endif
         pop       af         ;STACK BACK TO NORMAL
         reti                 ;RE-ENABLE INTERRUPTS, AND RETURN
 
    bY CREATING A table OF ADDRESS OF SUBROUTINES AS ABOVE, AND MAKING A call TO setup, WE HAVE INSTALLED AN INTERRUPT SERVICE WITH 3 TASKS TO PERFORM EVERY 25 MS IN THE mOD i, AND EVERY 33 1/3 MS IN THE mOD iii.  tHESE ROUTINES WILL EXECUTES REGARDLESS OF WHAT FOREGROUND PROGRAM IS RUNNING.  bY SAVING THE bc, de, AND hl REGISTERS BEFORE THE loop, WE HAVE ALLOWED OURSELVES FREE USE OF af, bc, de, AND hl REGISTERS IN THE INTERRUPT SERVICE.  iF A SINGLE INTERRUPT SERVICE IS ALL THAT IS NEEDED, THEN THE table APPROACH NEED NOT BE USED.  sIMPLY PLACE YOUR SUBROUTINE DIRECTLY IN LINE AND SAVE ALL REGISTERS THAT ARE TO BE USED.  nOW THAT WE HAVE "INSTALLED" OUR INTERRUPT SERVICE, LETS DEFINE SOMETHING THAT WE CAN DO WITH IT.
 
    tASK #1 - rEAL tIME cLOCK
rEMEMBER THAT THE mOD i GENERATES A rtc INTERRUPT EVERY 25 MS, AND THE mOD iii EVERY 33 1/3 MS.  a MILLISECOND IS 1 THOUSANDTH OF A SECOND.  iF WE DIVIDE 1000/25 (1 SECOND / INTERRUPT TIME) WE FIND THAT THE mOD i WILL INTERRUPT 40 TIMES EACH SECOND.  tHE mOD iii WILL INTERRUPT AT 1000/33.333 OR 30 TIMES EACH SECOND.  wE MAY THEN COUNT THE NUMBER OF INTERRUPTS AS THEY ARE GENERATED, AND COUNT SECONDS FAIRLY ACCURATELY (AS LONG AS THE MASKABLE INTERRUPTS ARE ENABLED).  iT SHOULD BE NOTED THAT THE DISK DRIVES AND CASSETTE i/o REQUIRE THAT THE INTERRUPTS BE TURNED OFF DURING THEIR OPERATION DUE TO THE TIMING SENSITIVITY OF THESE DEVICES, AND THE CLOCK WOULD NATURALLY BE RENDERED INACCURATE.  wE WILL DEFINE tabl1 WHICH WILL CONTAIN THE TIME AND DATE THE WE WILL UPDATE, AND tabl2 WHICH WILL DEFINE THE MAXIMUM VALUES THAT tabl1 MAY HAVE IN THE time FIELDS.  i WILL ALSO PROVIDE AN OPTIONAL CALL TO ADVANCE THE CALENDAR EACH TIME THE CLOCK CYCLES A 24 HOUR PERIOD.  iF ONLY A CLOCK IS NEEDED, THIS CALL MAY BE OMITTED. iF THE USER DESIRES A 12 HOUR CLOCK, THEN CHANGE THE VALUE IN tabl2 AND DELETE THE date CALL AS IT WOULD ADVANCE A DAY EACH 12 HOURS.
 
tabl1    db        0          ;TICKS
time$    db        0          ;SECONDS
         db        0          ;MINUTES
         db        0          ;HOURS
date$    db        21         ;DAY OF THE WEEK
         db        5          ;MONTH
         db        82         ;YEAR -1900
tabl2
         if        mod1
         db        40         ;40 TICKS / SECOND mOD i
         endif
         if        mod3
         db        30         ;30 TICKS / SECOND mOD iii
         endif
         db        60         ;MAX SECONDS
         db        60         ;MAX MINUTES
         db        24         ;MAX HOURS
tabl3    db        31,28,31,30,31,30,31,31,30,31,30,31
; TABL3 IS NEEDED ONLY FOR date, AND MAY BE DELETED IF NOT USED
; THIS IS A TABLE USED TO ESTABLISH THE NUMBER OF DAYS PER A GIVEN MONTH
; THIS ROUTINE ASSUMES THAT tabl2 AND tabl3 LIE WITHIN A PAGE BOUNDARY
; SEE THE SECTION ON coding FOR MORE INFORMATION ON THIS
 
task1    ld        hl,tabl1   ;POINT TO TIME TABLE
         ld        de,tabl2   ;POINT TO MAX VALUE
         ld        b,4        ;4 BYTES TO CHECK
task1l   inc       (hl)       ;BUMP THE VALUE
         ld        a,(de)     ;GET MAX VALUE
         sub       (hl)       ;THERE YET?
         ret       nz         ;NOT TIME TO CHANGE YET
         ld        (hl),a     ;RESET THE VALUE
         inc       l          ;BUMP TIME POINTER
         inc       e          ;BUMP MAX POINTER
         djnz      task1l     ;DO ALL 4
         call      date       ;ADVANCE CALENDAR ONE DAY (OPTIONAL)
         ret                  ;COMPLETED
date     push      hl         ;SAVE DAY POINTER
         inc       l          ;POINT TO MONTH
         ld        a,(hl)     ;GET THE MONTH
         dec       a          ;MAKE 1-12 INTO 0-11
         add       a,e        ;POINT TO MAX DAYS / MONTH
         ld        e,a        ;(de) = MAX DAYS THIS MONTH
         inc       l          ;POINT TO YEAR
         ld        a,(hl)     ;GET YEAR - 1900
         and       3          ;CHECK FOR LEAP YEAR
         ld        a,28       ;28 DAYS IN fEB NON LEAP-YEAR
         jr        nz,datok   ;GOT IT
         inc       a          ;29 DAYS IN LEAP YEAR
datok    ld        (tabl3+1),a;SAVE # DAYS IN FEB
         ex        de,hl      ;de => MAX BYTE
         pop       hl         ;NOW POINTING TO DAY
         inc       (hl)       ;BUMP THE DAY
         ld        a,(de)     ;GET MAX DAYS THIS MONTH
         cp        (hl)       ;TEST FOR MAX DAYS
         ret       nc         ;NOT AT END YET
         ld        (hl),1     ;SET FIRST DAY
         inc       l          ;POINT TO MONTH
         inc       (hl)       ;BUMP IT
         ld        a,(hl)     ;FETCH IT
         cp        13         ;TEST FOR 1-12
         ret       c          ;DONE IF NOT
         ld        (hl),1     ;SET MONTH 1
         inc       l          ;POINT TO YEAR
         inc       (hl)       ;BUMP YEAR
         ret                  ;COMPLETED
 
    tHE ABOVE ROUTINE WILL ALLOW US A COMPLETE CLOCK AND CALENDAR RUNNING AS A "BACKGROUND" TASK AND ACCOUNTING FOR LEAP YEARS, WHILE WE GO ABOUT OUR BUSINESS RUNNING A "FOREGROUND" TASK.  wE MAY LOAD BYTES INTO THE time$ OR date$ AREAS TO SET THE TIME/DATE, OR FETCH BYTES FROM THEM TO GET THE TIME/DATE.  tHE BYTES STORED HERE ARE KEPT IN BINARY OF COURSE, AND MUST BE CONVERTED OVER TO ASCII BEFORE BEING DISPLAYED.  uSING THE ABOVE METHOD, IN THE YEAR 1999 AT MIDNIGHT, THE YEAR WILL GO BACK TO 0, THUS INDICATING 1900 AGAIN.  tHE USER MAY OPT FOR ANY "BASE YEAR" TO BE USED IN THE COMPUTING, THAT IS A NUMBER TO BE ADDED TO THE STORED YEAR TO PRODUCE THE CORRECTED YEAR.  iT MAY BE A GOOD IDEA IN THIS TIME PERIOD TO USE 1950 AS A BASE YEAR, THEREFORE, THIS BEING 1982, THE YEAR stored WOULD BE 32.  uSING THIS METHOD, THE YEAR WILL BE ACCURATE UNTIL 2050, OR IN OTHER WORDS, AT LEAST THE LIFE OF THE trs80 COMPUTER.
 
    tASK #2 - cLOCK dISPLAY
wELL, WE HAVE COVERED HOW TO MAKE THE trs80 KEEP TIME.  wE COULD ALSO HAVE ANOTHER ROUTINE TO DISPLAY THIS CLOCK ON THE VIDEO IN HUMAN READABLE FORM.  sINCE WE HAVE DEFINED THE STORAGE AREA WHERE THE TIME AND DATE ARE KEPT, WE CAN CONVERT THIS TO ascii FORMAT AND PLACE THE RESULTS ON THE VIDEO.  wE WILL USE THE task2 SLOT AS DEFINED IN THE ABOVE INTERRUPT DRIVER.
 
task2    call      prtime     ;DISPLAY THE TIME
         call      prdate     ;DISPLAY THE DATE
         ret
; i HAVE BROKEN THESE TWO UP SEPARATELY IN CASE ONLY ONE
;   OR THE OTHER IS DESIRED BY THE USER
prtime   ld        hl,3c00h+46
; THIS IS THE VIDEO LOCATION WHERE WE WILL START THE TIME
;   DISPLAY AT, ROW 0, COLUMN 46 IN THE FORMAT hh:mm:ss
         ld        de,time$   ;POINT TO WHERE TIME IS STORED
         ld        a,':'      ;SEPARATING CHARACTER
         jr        puttdt     ;GO COMMON ROUTINE
prdate   ld        hl,3c00h+56;WHERE DATE GOES ON THE VIDEO
         ld        de,date$   ;POINT TO WHERE DATE IS STORED
         ld        a,'/'      ;SEPARATOR FOR DATE
puttdt   ld        (tdgap),a  ;SAVE INTO CODE
         ld        b,3        ;3 BYTES EACH LOOP
         jr        tdgo       ;DO THE STRING
tdloop   ld        (hl),'/'   ;SEPARATOR BYTE
tdgap    equ       $-1        ;LABEL USED ABOVE
         inc       l          ;NEXT VIDEO LOCATION
tdgo     ld        a,(de)     ;GET A BYTE
         push      bc         ;SAVE IT FROM @ascii
         call      @ascii     ;DEFINED FURTHER IN THIS CHAPTER
         ld        (hl),c     ;SAVE lsb
         inc       l          ;BUMP VIDEO POSITION
         ld        (hl),b     ;SAVE msb
         inc       l          ;NEXT POSITION
         inc       e          ;NEXT TABLE BYTE
         pop       bc         ;GET COUNTER BACK
         djnz      tdloop     ;DO IT ALL
         ret                  ;DONE
 
    hERE IS AN ALTERNATE METHOD OF task2 IF BOTH TIME AND DATE WILL BE USED.  tHIS ROUTINE IS MORE EFFICIENT AND TAKES UP LESS MEMORY.
 
task2    ld        hl,3c00h+46;WHERE IT GOES ON THE VIDEO
         ld        de,time$   ;POINT TO TIME/DATE DATA
         ld        a,':'      ;GAP BYTE
         call      puttd      ;PUT TIME
         inc       l          ;2 SPACES BETWEEN
         inc       l          ;TIME AND DATE
         ld        a,'/'      ;GAP BYTE
puttd    ld        (tdgap),a  ;STORE INTO CODE
         ld        b,3        ;3 BYTE LOOP
         jr        tdgo       ;DO ALL 3
tdloop   ld        (hl),' '   ;PUT GAP BYTE
tdgap    equ       $-1        ;POINT TO IT FOR ABOVE SET
         inc       l          ;NEXT VIDEO LOCATION
tdgo     ld        a,(de)     ;GET A BYTE
         push      bc         ;SAVE COUNTER FROM @ascii
         call      @ascii     ;CONVERT TO DECIMAL ASCII
         ld        (hl),c     ;PUT lsb
         inc       l          ;NEXT VIDEO
         ld        (hl),b     ;PUT msb
         inc       l          ;NEXT VIDEO
         inc       e          ;NEXT TIME/DATE SLOT
         pop       bc         ;RESTORE COUNTER
         djnz      tdloop     ;DO WHOLE THING
         ret                  ;DONE
 
    tASK #3 - pRINTER sPOOLER
aS MENTIONED ELSEWHERE IN THE MANUAL, THERE IS A METHOD OF USING THE INTERRUPT SERVICE TO "SPOOL" OUT CHARACTERS TO SLOW DEVICES.  bASICALLY, THIS INVOLVES THE USE OF A "BUFFER" AREA TO TEMPORARILY STORE BYTES UNTIL THE SLOW DEVICE IS READY TO ACCEPT A CHARACTER.  tHE TYPE OF BUFFER ESTABLISHED IS TERMED A "FIRST IN, FIRST OUT" (fifo) BUFFER.  tHIS MEANS THAT BYTES ARE REMOVED IN THE SAME ORDER THAT THEY ARE RECEIVED.  a QUICK PRINTER MAY OPERATE AS FAST AS 1500 BAUD, BUT THIS IS RELATIVELY SLOW COMPARED TO THE SPEED OF THE PROCESSOR, AND MUCH TIME COULD BE WASTED WAITING FOR THE DEVICE TO ACCEPT EACH CHARACTER.  iNSTEAD OF SENDING THE BYTE WE WANT TO PRINT DIRECTLY TO THE PRINTER, AND WAITING FOR IT TO BECOME READY, WE WILL SIMPLY STORE THE BYTE INTO A "BUFFER" AREA THAT WE WILL DEFINE.  tHEN WE WILL SET UP AN INTERRUPT SERVICE TO SEND THE BYTES TO THE PRINTER AS A "BACKGROUND" TASK.  tHIS WAY THE cpu WILL NOT BE SLOWED DOWN BY THE PRINTER AS LONG AS BUFFER SPACE REMAINS.
    iN ORDER TO GET THIS ROUTINE TO OPERATE CORRECTLY, WE MUST SETUP TWO SUBROUTINES.  oNE RUNNING UNDER "FOREGROUND" THAT WILL INTERCEPT BYTES THAT ARE SUPPOSED TO GO TO THE PRINTER, AND INSTEAD PLACE THEM INTO OUR BUFFER AREA.  aNOTHER RUNNING UNDER "BACKGROUND" (INTERRUPTS) THAT WILL ACTUALLY SEND THE BYTES TO THE PRINTER.  tHE STANDARD CALL IN THE trs80 TO SEND A BYTE TO THE PRINTER IS VIA A CALL TO 003bh WITH THE BYTE IN THE a REGISTER.  tHIS WILL THEN LOOK UP THE ADDRESS THAT HANDLES THE PRINTER IN THE dcb BEGINNING AT ADDRESS 4025h IN BOTH THE mOD i AND iii.  tHE ACTUAL ADDRESS OF THE DRIVER IS AT ADDRESS 4026h AND 4027h.  tHE ONLY THING WE NEED TO DO TO SET UP THE FOREGROUND TASK IS TO POKE THE ADDRESS OF OUR SUBROUTINE INTO THESE ADDRESSES:
 
prinit   ld        hl,newprt  ;POINT TO OUR ROUTINE
         ld        (4026h),hl ;SAVE INTO THE PRINTER dcb
         ret                  ;THAT'S ALL FOR NOW
 
    nOW WE HAVE INTERCEPTED THE PRINTER dcb TO CALL newprt EACH TIME A BYTE IS ATTEMPTED TO BE SENT TO THE PRINTER.  tHE BYTE TO BE PRINTED WILL NOW BE IN THE c REGISTER.  tHE rom WILL SAVE THE hl AND bc REGISTERS, SO WE ARE FREE TO USE THEM.  tHE de REGISTER WILL POINT TO THE FIRST BYTE OF THE dcb, IN OTHER WORDS, IT WILL EQUAL 4025h.  tHE de REGISTER IS DESTROYED BY THIS rom CALL ANYWAY, SO WE ARE FREE TO USE IT HERE.
 
newprt   ld        hl,(prcnt) ;GET THE COUNT OF CHARS IN BUFFER
         inc       hl         ;CHECK IF ROOM FOR MORE
         ld        a,h        ;GET THE SIZE OF THE BUFFER
         cp        4          ;OUR BUFFER IF 1k LONG
         jr        nc,newprt  ;WAIT FOR INTERRUPT TO MAKE ROOM
         di                   ;MUST DISABLE WHILE WE CHANGE BUFFER
         ld        hl,(prcnt) ;GET CURRENT COUNT
         inc       hl         ;ADD ONE FOR NEW CHARACTER
         ld        (prcnt),hl ;UPDATE THE BUFFER COUNT
         ld        hl,prbuff+3feh ;END OF BUFFER -1
         ld        de,prbuff+3ffh ;END OF BUFFER
         ld        a,c        ;GET CHARACTER
         ld        bc,3ffh    ;LENGTH OF BUFFER -1
         lddr                 ;BLOCK MOVE DATA UP ONE BYTE
         ld        (hl),a     ;PUT NEW CHAR IN BUFFER
         ei                   ;CAN ENABLE NOW
         ret                  ;COMPLETED
prbuff   ds        400h       ;DEFINE 1k OF SPACE FOR BUFFER
 
    nOW THAT WE HAVE INTERCEPTED THE PRINTER DRIVER, AND PLACED THE CHARACTERS INTO OUR BUFFER, WE NOW NEED THE INTERRUPT DRIVER TO PASS THESE STORED BYTES TO THE PRINTER AS IT BECOMES READY.  tHE ROUTINE WILL LOOP BACK INTO ITSELF UNTIL THE PRINTER FIRST CANNOT ACCEPT A BYTE.  tHIS WAY, WE MAY LOAD THE INTERNAL PRINTER BUFFER AS RAPIDLY AS IT CAN ACCEPT BYTES AND THEREBY INCREASE OUR PRINTER THROUGHPUT.
 
task3    ld        de,0       ;GET COUNT OF CHARACTERS
prcnt    equ       $-2        ;POINT TO LAST 2 BYTES
         ld        a,d        ;SEE IF ANYTHING
         or        e          ;ANY BITS ON?
         ret       z          ;BUFFER IS EMPTY
         ld        a,(37e8h)  ;READ STATUS OF PRINTER
         and       0f0h       ;TOP 4 BITS SIGNIFICANT ONLY
         cp        30h        ;7,6 MUST BE RESET - 5,4 SET
         ret       nz         ;NOT READY NOW
         dec       de         ;ADJUST NEW COUNT
         ld        (prcnt),de ;UPDATE THE COUNTER
         ld        hl,prbuff  ;POINT TO THE BUFFER
         add       hl,de      ;POINT TO THE BYTE
         ld        a,(hl)     ;GET IT
         if        mod1
         ld        (37e8h),a  ;SEND TO PRINTER
         endif
         if        mod3
         out       (0f8h),a   ;SEND TO PRINTER
         endif
         jr        task3      ;SEE IF IT CAN TAKE ANOTHER NOW
 
    wE NOW HAVE SUCCESSFULLY IMPLEMENTED A PRINTER SPOOLER INTO OUR FOREGROUND AND BACKGROUND SERVICES.  nOW, ANYTIME THAT A BYTE IS DISPLAYED VIA A CALL TO 003bh, IT WILL ACTUALLY BE INTERCEPTED BY OUR SUBROUTINES AND SPOOLED TO THE PRINTER UNDER INTERRUPT SERVICE (AS LONG AS THE MASKABLE INTERRUPTS ARE ENABLED).
 
    tASK #4 - tYPE aHEAD kEYBOARD dRIVER
tHE tYPE-aHEAD DRIVER IS VERY SIMILAR TO THE PRINTER SPOOLER, BUT IN THE REVERSE ORDER.  bYTES ARE PLACED INTO THE BUFFER UNDER "BACKGROUND" (INTERRUPT) SERVICE, AND TAKEN OUT AS NEEDED UNDER "FORGROUND" SERVICES.  wE WILL DEFINE A MUCH SMALLER BUFFER FOR THIS PURPOSE THAN WAS NEEDED BY THE PRINTER SPOOLER, 128 CHARACTERS, WHICH IS NORMALLY MORE THAN ANYONE COULD ENTER BEFORE THE CURRENT FOREGROUND TASK HAD COMPLETED.
     wITH THIS ROUTINE, WE MUST AGAIN SETUP TWO SUBROUTINES.  tHE STANDARD CALL IN THE trs80 TO FETCH A BYTE FROM THE KEYBOARD IS VIA 002bh.  tHE BYTE IS RETURNED IN THE a REGISTER.  tHIS WILL LOOK UP THE DRIVER ADDRESS IN THE dcb LOCATED AT ADDRESS 4015h, THE ACTUAL DRIVER ADDRESS AT 4016h.  tO INITIALIZE, WE MUST POKE THE ADDRESS OF OUR ROUTINE INTO THESE ADDRESSES:
 
tainit   ld        hl,newkey  ;POINT TO OUR ROUTINE
         ld        (4016h),hl ;SAVE INTO THE KEYBOARD dcb
         ret                  ;THAT'S ALL FOR SETUP
 
    nOW WE HAVE INTERCEPTED THE KEYBOARD dcb TO CALL newkey EACH TIME A BYTE IS REQUESTED FROM THE KEYBOARD.  tHE BYTE MUST BE RETURNED IN THE a REGISTER.  iF NO KEY IS AVAILABLE, THE a REGISTER WILL BE RETURNED WITH 0 FOR null.
 
newkey   ld        a,(kicnt)  ;GET COUNT OF CHARS IN BUFFER
         or        a          ;ANYTHING THERE ?
         ret       z          ;RETURN WITH 0 IF NOT
         di                   ;MUST DISABLE WHILE BUFFER IS CHANGED
         ld        a,(kicnt)  ;GET AGAIN, COULD HAVE CHANGED
         dec       a          ;LESS ONE KEY TAKEN OUT
         ld        (kicnt),a  ;UPDATE THE COUNTER
         ld        de,kibuff  ;POINT TO BUFFER
         ld        a,(de)     ;GET THE BYTE
         ld        hl,kibuff+1;POINT TO SECOND BYTE
         ld        bc,7fh     ;LENGTH OF BUFFER -1
         ldir                 ;ADJUST THE BUFFER
         ei                   ;FINISHED, CAN ENABLE NOW
         ret                  ;HAVE THE KEY
kibuff   ds        80h        ;128 BYTE BUFFER
 
    nOW WE HAVE INTERCEPTED THE NORMAL KEYBOARD DRIVER, AND WILL TAKE CHARACTERS FROM THE TYPE-AHEAD BUFFER INSTEAD OF JUST ACCEPTING WHAT IS CURRENTLY BEING PRESSED.  tHIS MEANS THAT WHILE THE COMPUTER IS PROCESSING SOMETHING, AND NOT CURRENTLY "SCANNING" THE KEYBOARD, THE KEYS PRESSED AT THIS TIME WILL NOT BE "LOST", BUT INSTEAD WILL BE PICKED UP UNDER THE INTERRUPT SERVICE AND STORED UNTIL THE CURRENTLY RUNNING PROGRAM CALLS FOR THE CHARACTERS.  aNY CHARACTERS ENTERED AFTER THE BUFFER IS FULL (128 CHARACTERS) WILL NATURALLY STILL BE LOST, BUT THE BUFFER SIZE MAY BE CHANGED TO COMPENSATE FOR THIS IF MORE SPACE IS NEEDED.
    tHERE ARE SEVERAL WAYS TO DECODE A KEYBOARD AS MENTIONED IN THE i/o SECTION OF THIS MANUAL.  eITHER THE "DECODE" METHOD OR THE "LOOKUP" METHOD WILL BOTH BE FAST ENOUGH NOT TO TIE UP THE INTERRUPTS FOR LONG.  iF WE NEED QUICK RESPONSE, THE TABLE METHOD IS BEST USED.  tHE ONLY CONDITION IS THAT NO "KEYBOUNCE" DELAY CAN BE ADDED TO THE KEYBOARD SCAN.  iF THERE WAS, AND IT LASTED MORE THAN 25MS (33 IN THE mOD iii), THEN THE COMPUTER WOULD BE FORCED INTO A "CONSTANT INTERRUPT" STATE AND THE FOREGROUND TASK WOULD NEVER BE EXECUTED.  tHE TIME BETWEEN INTERRUPTS IS ENOUGH OF A DELAY FOR KEYBOUNCE TO BE EFFECTIVE USING THIS TYPE AHEAD METHOD.  aS METIONED ABOVE, ANY OF THE KEYBOARD ROUTINES IS ACCEPTABLE HERE, AND i WILL USE THE "LOOKUP" METHOD IN THIS EXAMPLE DUE TO ITS QUICK SPEED.
 
task4    ld        a,0        ;SEE IF ROOM FOR MORE
kicnt    equ       $-1        ;LABEL FOR COUNT
         cp        80h        ;SEE IF AT MAXIMUM
         ret       nc         ;CAN'T HOLD ANY MORE
         call      kiscan     ;SCAN THE KEYBOARD
         or        a          ;ANYTHING ?
         ret       z          ;RETURN IF NOT
         ld        de,(kicnt) ;GET CURRENT COUNT OF KEYS
         ld        d,0        ;SETUP LABEL POINTING TO COUNT
         ld        hl,kibuff  ;POINT TO BUFFER
         add       hl,de      ;POINT TO CURRENT POSITION
         ld        (hl),a     ;SAVE CHARACTER IN BUFFER
         inc       e          ;BUMP COUNTER
         ld        a,e        ;GET NEW COUNT
         ld        (kicnt),a  ;UPDATE COUNTER
         ret                  ;DONE, CHARACTER IN BUFFER
 
    tHIS IS ALL THAT IS NEEDED FOR THE TYPE-AHEAD PART.  nOW LETS DEFINE THE ACTUAL KEYBOARD "SCANNER".
 
kimask   db        0,0,0,0,0,0,0 ;7 BYTE KEYBOARD MASK AREA
kiscan   ld        hl,kimask  ;MASK AREA FOR NEW KEY DETECT
         ld        bc,3801h   ;FIRST ROW OF KEYBOARD
         ld        d,0        ;USE AS ROW COUNTER
kirow    ld        a,(bc)     ;READ KEYBOARD MATRIX
         ld        e,a        ;SAVE KEY HERE
         xor       (hl)       ;SEE IF SAME AS LAST KEY
         ld        (hl),e     ;SAVE THIS KEY HERE
         and       e          ;SEE IF NEW KEY
         jr        nz,kihave  ;HAVE A KEY
         inc       d          ;BUMP ROW COUNTER
         inc       hl         ;BUMP MASK
         rlc       c          ;NEXT KEYBOARD ROW
         jp        p,kirow    ;DO ALL BUT SHIFT KEY HERE
         ret                  ;a = 0 FOR NO KEYS PRESSED
kihave   ld        e,a        ;SAVE MASKED BYTE
         ld        a,d        ;GET ROW
         rlca                 ;TIMES 2
         rlca                 ;TIMES 4
         rlca                 ;TIMES 8
         ld        e,a        ;e = ROW TIMES 8
         ld        c,1        ;SETUP BIT MASK
kidet    ld        a,c        ;GET MASK
         and       e          ;SEE IF THIS BIT IS IT
         jr        nz,kigot   ;HAVE THE DISPLACEMENT
         inc       d          ;BUMP ROW*8
         rlc       c          ;MOVE BIT MASK OVER
         jr        kidet      ;DETERMINE THE CORRECT BIT
kigot    ld        d,0        ;de = KEY OFFSET INTO TABLE
         ld        hl,keys1   ;NON-SHIFTED KEYBOARD TABLE
         ld        a,(3880h)  ;READ THE SHIFT KEY
         or        a          ;ANY BITS ON?
         jr        z,kifind   ;NO SHIFT KEY
         ld        hl,keys2   ;SHIFTED KEYS
kifind   add       hl,de      ;POINT TO THE KEY IN TABLE
         ld        a,(hl)     ;GET THE BYTE
         ret                  ;a = KEY
keys1    db        '@ABCDEFG'
         db        'HIJKLMNO'
         db        'PQRSTUVW'
         db        'XYZ',0,0,0,0,0
         db        0,'!"#$%&',27H
         db        '()*+<=>?'
         db        13,31,1,91,10,8,9,32
keys2    db        '`abcdefg'
         db        'hijklmno'
         db        'pqrstuvw'
         db        'xyz',0,0,0,0,0
         db        '01234567'
         db        '89:;,-./'
         db        13,31,1,27,26,24,25,32
 
    nOW WE HAVE NOW SUCCESSFULLY IMPLEMENTED A TYPE AHEAD KEYBOARD ON OUR trs80 mOD i AND iii.  iF THE FOREGROUND PROGRAM IS NOT CURRENTLY REQUESTING A KEYBOARD INPUT, BUT A KEY IS BEING PRESSED, IT WILL NOT BE LOST (PROVIDED THE MASKABLE INTERRUPTS ARE ENABLED).
 
    tHE INTERRUPTS MAY SEEM VERY COMPLICATED AT FIRST, BUT MASTERING THEM CAN GIVE YOU GREAT CONTROL OVER CRITICAL TIMING OR SLOW DEVICES AS DEMONSTRATED ABOVE.  tHE TYPE AHEAD AND PRINTER SPOOLER ARE INVALUABLE TIME SAVING FEATURES, AND THEY TAKE UP RELATIVELY LITTLE MEMORY.  cOUPLING THESE ROUTINES WITH SELF-PROTECTING, SELF-RELOCATING CODE (SEE "CODE" SECTION IN THIS BOOK) ALLOWS THE USER TO IMPLEMENT THESE FEATURES ON ANY dos.  tHEIR EFFICIENCY AT dos RUNTIME WILL DEPEND ON THE INDIVIDUAL DISK i/o ROUTINES AS MENTIONED IN THAT SECTION.  tHEY NATURALLY WILL ONLY BE "ON" WHILE THE MASKABLE INTERRUPTS ARE ENABLED, AND SOME TIMING CRITICAL EVENTS (DISK AND TAPE i/o) MUST HAVE THE INTERRUPTS DISABLED.  cERTAIN TECHNIQUES CAN INCREASE THIS EFFICIENCY.
 
    tASK #5 - pROGRAM cOUNTER tRACE
iN THIS ROUTINE, WE WILL DISPLAY THE ADDRESS OF THE PROGRAM COUNTER OF THE FOREGROUND ROUTINE EVERY HEARTBEAT INTERRUPT (25MS mOD i, 33.333MS mOD iii).  aT THE ENTRY OF OUR INTERRUPT SERVICE, WE FETCHED THE ADDRESS FROM THE TOP OF THE STACK, AND SAVED IN INTO ADDRESS trace.  wE WILL NOW TAKE THIS ADDRESS, CONVERT IT TO hex ascii (4 CHARACTERS), AND PLACE THE RESULT INTO THE VIDEO MEMORY.
 
task5    ld        hl,0       ;GET THE STORED ADDRESS
trace    equ       $-2        ;SETUP LABEL TO SAVE IT
         ld        a,h        ;GET msb OF ADDRESS
         call      @hexcv     ;CONVERT TO HEX ASCII
         ld        (3c00h+64+60),bc ;PUT ON THE VIDEO
         ld        a,l        ;GET lsb OF ADDRESS
         call      @hexcv     ;ROUTINE DEFINED FURTHER
         ld        (3c00h+64+62),bc ;PUT ON VIDEO
         ret                  ;THAT IS ALL
 
    oNCE WE HAVE IMPLEMENTED OUR INTERRUPT SERVICE, WE MAY EASILY CHANGE THE table TO ALTER WHICH ROUTINES ARE ACTIVE OR NOT.  iF THE PRINTER SPOOLER IS ACTIVE, AND DATA IS STILL BEING SENT TO OUR SUBROUTINE, AND WE REMOVE THIS TASK FROM THE INTERRUPT CHAIN, WE WILL "HANG" OUR PROGRAM AS THE DATA WILL NEVER BE PRINTED AND THE BUFFER WILL REMAIN "FULL".  lIKEWISE, IF WE SIMPLY REMOVE THE TYPE AHEAD FEATURE FROM THE INTERRUPT CHAIN, THE KEYBOARD WILL NO LONGER BE USABLE.  rEMEMBER, WE HAVE INTERCEPTED THE KEYBOARD DRIVER TO LOOK FOR CHARACTERS IN OUT BUFFER INSTEAD OF THE KEYBOARD.  iF THE INTERRUPT TASK IS REMOVED (OR INTERRUPTS DISABLED) THERE WILL NOT BE ANY SUBROUTINES TO PLACE THE CHARACTERS into THE BUFFER.  tHERE FORE, WITH THESE DEVICES, WE MUST FIRST "RESET" THE DEVICE BACK TO THE WAY IT WAS BEFORE WE INTERCEPTED IT, AND THEN WE MAY REMOVE THE INTERRUPT TASK.
    aS AN EXAMPLE, WE WILL RE-DEFINE OUR TYPE AHEAD KEYBOARD INITIALIZATION CODE:
 
tainit   ld        hl,(4016h) ;GET CURRENT DRIVER ADDRESS
         ld        (taaddr),hl;SAVE THE ADDRESS
         ld        hl,newkey  ;POINT TO OUR DRIVER
         ld        (4016h),hl ;PUT INTO THE dcb
         ld        hl,0       ;SET 0 KEYS IN BUFFER
         ld        (kicnt),hl ;FOR RE-ENTRY TO THE ROUTINE
         ret                  ;DONE WITH INIT
tafix    ld        hl,0       ;GET "OLD" ADDRESS
taaddr   equ       $-2        ;SETUP FROM ABOVE
         ld        (4016h),hl ;PUT IT BACK
         ret
 
    uSING THE ABOVE ROUTINE INSTEAD, WE NOW HAVE A WAY TO "GET OUT" OF THE TYPE AHEAD KEYBOARD.  wE MAY CALL tainit ANYTIME TO INTERCEPT THE KEYBOARD DRIVER, AND tafix ANYTIME THEREAFTER TO RESTORE THE DRIVER BACK TO ITS ORIGINAL.  tHIS CONCEPT MAY BE USED EXACTLY THE SAME WITH THE PRINTER SPOOLER USING THE APPROPRIATE dcb ADDRESS.
    nOW WE NEED A WAY TO ADD AND REMOVE INTERRUPT TASKS FROM THE CHAIN.  fIRST OF ALL, THE INTERRUPT CHAIN MAY BE ANY SIZE USING THE "TERMINATOR BYTES" TECHNIQUE USED ABOVE, AS LONG AS ENOUGH SPACE IS PRE-ALLOCATED.  iF ALL OF THE SLOTS ARE NOT USED, WE MAY INSTALL A "DUMMY VECTOR" IN THE TABLE TO FILL THE GAPS.  lETS SAY THAT WE WANT TO HAVE A TABLE CAPABLE OF EXECUTING 16 INTERRUPT TASKS (NICE hex NUMBER) NUMBERING FROM 0-15.  lETS RE-DEFINE OUR INTERRUPT TASK TABLE:
 
table    dw        task1
         dw        task2
         dw        task3
         dw        task4
         dw        task5
         dw        return
         dw        return
         dw        return
         dw        return
         dw        return
         dw        0          ;TERMINATOR
 
    tHE return ADDRESS IN THE ABOVE TABLE WILL SIMPLY BE THE ADDRESS OF ANY ret OPCODE IN MEMORY.  tHAT WAY, WHEN THE ROUTINE IS CALLED, IT WILL IMMEDIATELY RETURN AND NOTHING WILL BE DONE.  wE MAY NOW EASILY ADD AND DELETE TASKS FROM OUR TABLE.  tO ADD A TASK, WE NEED TO KNOW WHICH TASK NUMBER (0-15), AND THE ADDRESS OF THE SUBROUTINE.  lETS SUPPLY THIS INFORMATION TO OUR SUBROUTINE USING de TO CONTAIN THE ADDRESS, AND a TO CONTAIN THE SLOT NUMBER.  wE CAN CHECK TO SEE THAT THE a REGISTER INDEED CONTAINS A VALID NUMBER AND IGNORE THE COMMAND IF IT IS OUT OF RANGE, OR WE MAY "MASK" OFF THE UPPER BITS AND PRODUCE A mod 16 RESULT IN THE CORRECT RANGE
 
addtsk   push      bc         ;WE'LL USE THIS
         push      hl         ;AND THIS
         and       a,0fh      ;MASK OFF UPPER 4 BITS
         ld        l,a        ;GIVE IT TO l
         ld        h,0        ;hl = INTERRUPT NUMBER
         add       hl,hl      ;MULTIPLY TIME 2 (2 BYTE TABLE)
         ld        bc,table   ;POINT TO START OF TABLE
         add       hl,bc      ;POINT TO THE ADDRESS
         di                   ;CAN'T BE INTERRUPTED WHILE CHANGING
         ld        (hl),e     ;PUT lsb ADDRESS
; THE REASON THAT THE INTERRUPTS MUST BE OFF WHILE WE DO
;   THIS IS BECAUSE IF WE HAVE AN INTERRUPT AT THIS EXACT
;   PLACE, YOU CAN SEE THAT THERE WOULD BE AN INVALID ENTRY
;   IN THE TABLE AND THE ADDRESS CALLED BY THE INTERRUPT
;   TASK WOULD BE AN UNKNOWN PLACE
         inc       hl         ;BUMP THE TABLE POINTER
         ld        (hl),d     ;PUT msb OF ADDRESS IN TABLE
         ei                   ;NOW WE CAN ENABLE
         pop       hl         ;RESTORE THE STACK
         pop       bc
         ret                  ;ALL REGISTERS INTACT
 
    tO REMOVE A TASK, WE ONLY NEED TO SPECIFY THE TASK NUMBER IN THE a REGISTER:
 
kiltsk   push      de         ;SAVE THIS
         ld        de,return  ;POINT TO A ret OPCODE
         call      addtsk     ;PUT THIS IN THE TABLE
         pop       de         ;EVERTHING THE SAME
         ret
 
    nOW WE HAVE EVERYTHING NECESSARY TO TURN ON AND OFF OUR TYPE AHEAD AND PRINTER SPOOLER.
 
taon     call      tainit     ;INTERCEPT DRIVER
         ld        a,4        ;SLOT NUMBER
         ld        de,task4   ;INTERRUPT DRIVER
         jp        addtsk     ;THAT'S IT
taoff    call      tafix      ;REPLACE ORIGINAL DRIVER
         ld        a,4        ;SLOT NUMBER
         jp        kiltsk     ;REMOVE IT
spon     call      prinit     ;INTERCEPT PRINTER DRIVER
         ld        a,3        ;SLOT NUMBER
         ld        de,task3   ;OUR DRIVER
         jp        addtsk     ;PUT IN THE CHAIN
spoff    call      prfix      ;RESTORE DRIVER
         ld        a,3        ;SLOT
         jp        kiltsk     ;REMOVE IT
 
    sTUDY CAREFULLY WHAT YOU HAVE LEARNED HERE.  tHE INTERRUPT SERVICE CAN BE VERY POWERFUL WHEN YOU HAVE MASTERED IT!
>*BOOK1P1

