The Z80 CPU supports three different modes of handling interrupts. They are called IM 0, IM 1, and IM 2. In all modes, after accepting an interrupt, the Z80 disables interrupts and you must manually enable them again at the end of your interrupt service routine. The IM 0 mode is identical to the Intel 8080's interrupt handling and is provided for compatibility. In IM 0, the interrupting hardware takes over the bus and sends an instruction to the CPU to execute. This instruction would usually be an RST instruction, which is what the RST instructions were designed for. In IM 1, the Z80 always executes RST 38h.
In IM 2, the Z80 uses an interrupt vector table. The interrupting hardware gives the Z80 an index into the table, such that up to 128 devices can automatically specify which interrupt service routine (ISR) needs to be run. The I register specifies where the IVT starts. Since I is 8 bits, it only specifies the high byte; the table always starts at an address in which the low byte is zero. The interrupting hardware is supposed to send an 8-bit number that's an index into the table. So the Z80 combines the index and the value of the I register to form a 16-bit address. The Z80 uses the 16-bit number at that address as the location of the ISR.
The TI-83+ family operating system always operates in IM 1, in which any interrupt signal causes the CPU to execute the RST 38h instruction. The OS provides no hooks for allowing software to catch and process interrupts. Fortunately, the Z80 also has the IM 2 mode, which allows us to move the interrupt vector table (IVT) to any of 256 different locations. Unfortunately, in the TI-83+ IM 2 is semibroken because it would actually be useful to us if it functioned properly. The ASIC doesn't bother to specify an index into the table, so we have to poll different ports to figure out what generated the interrupt. Furthermore, you will note that the number the Z80 expects to be passed is an 8-bit number. Because the addresses in the table are 16-bits, each address takes 2 bytes. But the number the Z80 receives from the ASIC is effectively random. And the Z80 doesn't mask out the bottom bit. So you have to fill the table with 257 repeating bytes, and your ISR can only be at an address in which the high and lows bytes are the same.
If you want to know whether interrupts are currently enabled or not (so you can disable them, and then enable interrupts again if and only if they were enabled before), the ld a, i and ld a, r instructions will copy the value of IFF2 into the P/V flag. In other words, if interrupts are enabled, the P/V flag will be set. The Z80 has a bug causes ld a, i and ld a, r to reset the P/V flag if an interrupt occurred near the instruction. The workaround is to perform the ld a, i/r operation twice, and compare the results.
Basic Interrupt Sources
The TI-83+ has four basic interrupt sources:
- ON key
- HW timer 1 (TI calls this APD)
- HW timer 2 (TI calls this PAPD)
|3||Not related to interrupts|
|5||Crystal timer 1|
|6||Crystal timer 2|
|7||Crystal timer 3|
Port 3 (pIntMask) controls which interrupts are passed on to the Z80. Set a bit to enable the interrupt. When an interrupt occurs, check port 4 to see which interrupt triggered. A set bit means that the interrupt is actively firing. Note that I used the present progressive tense. If you don't acknowledge the interrupt, the Z80 will be reinterrupted the moment interrupts are reenabled. Currently, the popular method of ACKing interrupts is to zero port 3, and then rewrite the old value of port 3 again. However, the proper method is to write to port 2: write a 0 to a particular bit to reset that interrupt source, and write a 1 to leave it alone. Thus, if multiple interrupt sources trigger at once, you can ACK only the one you see first, and upon EI, the remaining ones will trigger, which you can then check and ACK in due time, instead of blithely ACKing everything.
Ports 2 and 4 are asymmetric: reading and writing control completely different parts of the hardware. Bit 3 does nothing in port 2. In port 3, bit 3 enables low power mode on HALT if zero. In port 4, bit 3 is 0 if the ON key is currently being held. On everything after the TI-83+, reading port 4 can tell you which crystal timer fired an interrupt, but you can't ACK or mask using ports 2 and 3.
Extra Interrupts on the TI-83+SE and Above
On everything after the TI-83+, there are even more interrupt sources: Link assist, crystal timers, and USB.
For link assist interrupts: See port 8.
KillLinkAssist: xor a out (pLnkAstSeEnable), a ret
For the crystal timers, the page on port 30 says everything.
KillCrystalTimers: xor a out (pCrstlTmr1Freq), a out (pCrstlTmr2Freq), a out (pCrstlTmr3Freq), a ret
For USB: Unless you're actually planning to handle USB activity, just kill USB entirely by doing this:
KillUsb: xor a out (57h), a out (5Bh), a out (4Ch), a ld a, 2 ; This is important. Resetting this bit will ENABLE USB stuffs! out (54h), a ret