Interrupts are breaks the processor takes
in order execute other code or to repair internal resources. When
an interrupt has been signaled, the current PC is pushed onto the stack
to be popped off the stack when the interrupt completes. Most of
the time, the programmer has no control over when the interrupts occurs;
in a few instances, however, the programmer has control over what
code is run, which can turn out very useful. There are twelve types
of interrupts, four of which can be externally programmed.
The three maskable interrupt types are interrupt
mode 0, interrupt mode 1 and interrupt mode 2. Only one of these
modes can be set at time; the instructions <im 0>, <im 1>, and <im
2> will set the interrupt mode to 0, 1, or 2 respectively. These
three types are called maskable interrupts because they can be masked
(disabled) with the instruction <di> (disable maskable interrupts) and
enabled with the instruction <ei> (enable maskable interrupts).
With the interrupts enabled, the current interrupt mode will be triggered
to execute on even intervals of once every 1/174 seconds, meaning that
any code you have designated as an interrupt handler will run 174 times
per second.
Now, you are probably thinking that this will screw
up all your suspended code, by destroying the registers and flags in the
interrupt and then returning with those modified values. Well, an
interrupt can do that, but you have to program it correctly so that
it doesn't use the registers (including F) used in normal operation.
This is done by employing what are called the shadow registers, which exist
as shadows of registers A, F, HL, BC and DE. To access the shadow
registers, you must exchange the contents of these registers and their
shadows: <exx> (exchange the contents of the shadow registers for HL,
BC, DE and the registers HL, BC, DE) and <ex af,af'> (exchange the contents
of the shadow registers for A, F and the registers A, F). At the
start of your interrupt handler you should execute <exx / ex af,af'>,
then execute it once again at the end of your interrupt handler (allowing
you to use the important instructions for the standard registers, without
causing your calculator to crash as it returns to suspended code).
The most common interrupt type is interrupt mode 1. Every assembly program you have written so far has been running the ROM's im 1 interrupt handler (I know, it's discouraging to find out that you've been running code where you previously thought you had complete control). When the processor recieves the signal to run an im 1 interrupt, it will push the program counter and jump to the address $0038. Since this is ROM, you can't install your own handler at $38, but TI was benevolent even to create a user interrupt routine at $d2fe that is called from $38 if the flag iy+$23 is set and the checksum matches. So, if you copy code to $d2fe, set (iy+$23) and load the right checksum to $d2fd, not only will your code be run every time the interrupt is called while your asm program running, it will be run everytime the interrupt is called from TI-OS as well. Here is an example im 1 interrupt handler:
#include "asm86.h"
#include "ti86asm.inc"
_alt_int_chksum equ
$d2fd ;define these
equates for checksum
_alt_interrupt_exec equ $d2fe
;interrupt RAM addresses
user_counter equ _alt_interrupt_exec+counter-_asm_exec_ram
.org _asm_exec_ram
di
;disable interrupts so that they won't get called while finding checksum
ld hl,int_start
;point HL to our interrupt handler
ld de,_alt_interrupt_exec ;point DE to
where we'll copy it
ld bc,int_end-int_start
;BC is length of our handler
ldir
;copy handler to $d2fe
;now we find the new checksum by adding the values at every $28th byte of the interrupt
ld de,$28
;increment between pointers
ld a,(_alt_interrupt_exec)
;start accumulator with first byte of interrupt
ld hl,_alt_int_chksum+$28 ;ld HL with
pointer to first checksum byte
add a,(hl)
;add to accumulator
add hl,de
;checksum + ($28*2)
add a,(hl)
;add to accumulator
add hl,de
;checksum + ($28*3)
add a,(hl)
;add to accumulator
add hl,de
;checksum + ($28*4)
add a,(hl)
;add to accumulator
add hl,de
;checksum + ($28*5)
add a,(hl)
;add to accumulator
ld (_alt_int_chksum),a
;put result into checksum byte
set 2,(iy+$23)
;enables our routine to be called from interrupt
ei
;reenable interrupts
ret
;end of interrupt installation program
;what follows will be run every im 1 interrupt (174 times a second)
;put whatever you want here, this code just runs a simple interrupt
counter
int_start:
ld hl,user_counter
;all we're going to do is increment a RAM
inc (hl)
;location called user_counter
ret
counter:
.db 0
;which is this location
int_end:
.end
.end
Hmm. So what does our interrupt handler do? Let's say that we want to have a cursor in our program that will toggle between off and on every $60 interrupts (~ every .5 seconds). The way we can tell the amount of time that has passed, is by looking at the user_counter RAM location in the main loop of our program:
;***
ld a,(user_counter)
;check the interrupt counter
sub $60
;for $60 (the homescreen cursor flashes this often also)
call z,toggle_cursor
;if $60, toggle the cursor
;***
toggle_cursor:
ld (user_counter),a
;reset counter to zero (A is zero iff toggle_cursor is called)
;***
Let's take a look at the user_counter RAM location:
since we want the address to be defined in the interupt handler, and not
in asm space, we must find the offset of counter in asm space by subtracting
it's address from _asm_exec_ram, then add that offset to _alt_interrupt_exec.
This must be done for all pointers made to point to asm space from an interrupt.
I should stress that these calculations are made during compilation, not
at run-time. Also note that you don't need to exchange shadow registers
because that was done for you at $38.
$38 does alot; take a look at it disassembled using
ti86emu.
An interrupt mode 2 handler will do closely the same
thing, but won't run any of the ROM's code and is a little more difficult
to set up. When an im 2 interrupt is signaled, the processor takes
the value from the interrupt vector register (register I) and a
random byte to form a 16-bit pointer to the address that it loads to get
the interrupt handler address. Since the random byte is least significant,
there are 256 possible RAM locations where the calc could choose to get
its interrupt handler address from. So, to make it point to our handler,
we must create an interrupt vector table that tells the calc where
to jump to. This is accomplished using ldir.
To load a value into the interrupt vector register,
you must transfer it from the accumulator using the instruction <ld
i,a> (the only other instruction you can use I with is <ld a,i>).
This value will form the most significant 8-bits of the pointer location,
while the least signifcant 8-bits are beyond your control. <ld
a,$90 / ld i,a> will cause (when an interrupt occurs) the processor to
jump to an unknown address somewhere between $9000 and $90ff that would
contain our vector table.
ld hl,$9000
;create vector table at $9000
ld de,$9001
ld bc,256
ld a,$91
;that will point to $9191
ld (hl),a
ldir
ld hl,int_start
;move interrupt handler to $9191
ld de,$9191
ld bc,int_end-int_start
ldir
ld a,$90
;set up interrupt vector register
ld i,a
im 2
;and set interrupt mode
ei
Your interrupt handler will need to exchange shadow registers once at the beginning, then a second time at the end. Also, instead of using the normal return instruction at the end, you will need to use <reti>, which will signal to the processor that the interrupt has completed, returning to the suspended code. Make sure that you change the interrupt mode back to im 1 after you're done, or TI-OS will crash.
exx
ex af,af'
;interrupt handler
exx
ex af,af'
ei
reti
Interrupt mode 0 is the last maskable external interrupt level. What you do, is load an instruction into the I register, and that instruction will execute every interrupt. Useless. The only single byte instructions that could be used are the rst instructions, but these are in ROM and the only rst instruction that is defined as an interrupt handler is <rst $38>. If you wanted to, you could load <rst $38> into I, but that would be pointless as im 1 already does this. (If you want to be super cool, load a <halt> instruction into I for im 0 ;-)
Another kind of external interrupt is a non-maskable interrupt (NMI), meaning it's an interrupt that can not be disabled by <di>. These, to the best of my knowledge, do not exist on the 86, but if they did, their handler would be located at $0066.
Each interrupt level has a priority level, which means that if more than one interrupt is waiting to be run, the higher priority interrupt will run first or on top of the lower priority. The priority levels from top (highest) to bottom (lowest):
1) Trap - run if the processor comes across an undefined opcode like
$edfe (internal)
2) NMI - external
3) im 0 - external and internal
4) im 1 - external
5) im 2 - external
6) Timer 0 - internal
7) Timer 1 - internal
8) DMA Channel 0* - internal
9) DMA Channel 1* - internal
10) Clocked Serial I/O Port* - internal
11) Asynchronous SCI Channel 0* - internal
12) Asynchronous SCI Channel 1* - internal
*the speed of occurance of these interrupts is increased in turboed calculators
<di> will disable all interrupts but the NMI and Trap interrupts.
There are two bits you should know about as well called the interrupt flip flop bits (IFF1 and IFF2) that contain interrupt status information. IFF1 will be reset if interrupts are disabled and set if interrupts are enabled, and will be copied into IFF2 during a NMI. The four instructions <ld a,i>, <ld i,a>, <ld a,r>, <ld r,a> will copy IFF2 into the parity overflow flag.