Table of Contents
|
Existing Tutorials:
|Joe Pemberton's Interrupts Guide for TI-83+ calculators
Note: Learn ASM in 28 days: day 23 Interrupts is inaccurate!
Introduction
Interrupts are exactly as their name states: They interrupt the operation of code to operate other code, occurring at fairly steady intervals. Using interrupts is a good way to keep track of time in games because they operate regardless of what other code is currently being executed. There are two kinds of interrupts: hardware and software. Software interrupts are triggered by the RST command, and won't be discussed in this tutorial. Examples of hardware interrupt use:
grey-scale
steady time counters
animated sprites
Animated Tile maps (similar to animated sprites)
Using interrupts
Enabling/Disabling
To enable interrupts, use the EI command. To disable interrupts, use the DI command.
Interrupt Modes
There are three different interrupt modes. To change the interrupt mode use the IM command. Each mode is triggered slightly differently or when triggered performs different actions.
Mode 0
We won't worry about mode 0, as TI-Calculators can't make use of this mode. In mode 0, interrupts are triggered by external hardware. However, since we can't connect external hardware to TI-Calculators, an interrupt is never triggered. You can test this with this code:
ei
im 0
halt ; captures the last interrupt from previous mode
halt ; captures the 1st interrupt generated in mode 0
im 1
ret
Note: It is very important not to return to the OS in mode 0! It will cause your calculator to crash. It's better in fact to not ever use mode 0, as if you want to turn off interrupts, use DI.
Mode 1
Mode 1 is the 'default' interrupt mode. It is triggered roughly every 0.007 seconds at a frequency of 140 Hz. It is used by the OS, and messing with this interrupt might not be a good idea. The OS uses it to update the run indicator (which is why it always moves at roughly the same speed), detect input (using GetKey/GetCSC), and various other OSy things.
Mode 2
Mode 2 is similar to Mode 1, as it is triggered in roughly the same frequency, except instead of calling a specific location, it jumps to theoretically any location in memory. This means that we can have it jump consistently to code we want executed every 0.007 seconds.
Setting up a Mode 2 Interrupt
How does a Mode 2 interrupt work? The answer is not so carefully. It retrieves a value from the I register, then the lower byte of the address is chosen at random. However, it gets worse. You'd think that'd be where it jumps to, but instead it loads the byte at that address as the MSB and the byte at the address after as the LSB and the interrupt jumps to that address. Confused yet?
Interrupt Jump Table
So how do we safely install a mode 2 interrupt? First, we need to identify a safe area of ram that has at least 257 bytes free. The location must be somewhere that the MSB of the address is constant ($00, $01,…$FE). This will be our interrupt jump table.
Luckily for us, appbackupscreen is located from $9872 to $9B72. So, if we take $9900 to $9A00 inclusive, we'll have the necessary 257 bytes for our jump table.
Interrupt Location
Next, we need to identify an address like $0000, $0101, $0202,…, $FFFF that is also safe to modify. This location will be the location of our interrupt. We'll write this value to every location in the Interrupt Jump Table, resulting in our interrupt being called all the time.
Again, appbackupscreen provides the perfect location for us to install our interrupt. $9A9A satisfies all of the conditions mentioned above, and is well within the confines of appbackupscreen.
The Interrupt Code
What does the interrupt code look like? Well, it is completely identical to regular z80 code. Anything that can be executed normally can be executed from interrupt code. There are a few things you must do to make interrupt code not crash your calculator, though.
Shadow Registers
Since interrupt code is executed normally like regular code, it modifies the regular registers. A way to get around this to either not modify any registers (extremely hard, most instructions modify some flag or another), or to use shadow registers. Shadow registers are copies of the registers. There are not instructions that modify their values except the swap instructions (EX and EXX). It is common practice for interrupts to swap the registers as one of the first instructions.
interrupt:
; start of the interrupt code
; swap AF, BC, DE, and HL with their shadow registers
ex AF,AF'
exx
Returning
Interrupts return using the RET instruction. There are the RETI and RETN instructions, but they return from certain types of interrupts not available on TI calculators. Note that if you do put RETI or RETN at the end of an interrupt it will still return, but they cost 6 more T-states plus an additional byte, and so their use is discouraged.
Re-enabling interrupts
We wouldn't want another interrupt to be triggered while our interrupt code is being run, so the first thing to do when interrupt code is called is to disable interrupts with DI. To ensure that our interrupt will be executed again, we need to re-enable interrupts with EI at the end of our code.
interrupt:
; interrupt code
; ...
ei
ret
A Simple Interrupt
Here's a simple program that will install an interrupt that increases a 16-bit timer. It demonstrates most of the topics discussed above.
main:
bcall(_ClrLCDFull)
; disable interrupts while we're install ours
di
; set up the interrupt jump table to jump to $9A9A
ld a,$9A
ld ($9900),a
ld hl,$9900
ld de,$9901
ld bc,256
ldir
; copy our interrupt to $9A9A
ld hl,interruptStart
ld de,$9A9A
ld bc,interruptEnd - interruptStart
ldir
; set $99 into I
ld a,$99
ld I,a
; set interrupt mode 2
im 2
loop:
; re-enable interrupts
ei
; rest of our program
ld hl,0
ld (curRow),hl
ld hl,(counter)
bcall(_DispHl)
jr loop
interruptStart:
; disable interrupts during our interrupt
di
; swap registers
ex AF,AF'
exx
; our interrupt code
ld hl,(counter)
inc hl
ld (counter),hl
; swap back registers
ex AF,AF'
exx
; re-enable interrupts and return
ei
ret
interruptEnd:
counter:
.dw 0