Day 23: Interrupts

An interrupt is a break in the normal flow in a program that can be returned to. Speaking generally, all calls (but not jumps) are interrupts. When an interrupt is initiated, control immediately passes to an Interrupt Service Routine which does what it needs to, then returns to the point of interruption.

Software Interrupts

A software interrupt is intentionally generated by the programmer. A CALL instruction is technically a software interrupt, but we will reserve the term for restarts. A restart is identical to CALL but it takes only one byte. The are used for different things on different platforms, but on all the TI calculators they execute common OS routines.

RST xx: Software restart to address $00xx. See below for valid values for xx.
00h
Simulates taking all the batteries out of the calculator.
08h
Execute system routine OP1ToOP2.
10h
Execute system routine FindSym.
18h
Execute system routine PushRealO1.
20h
Execute system routine Mov9ToOP1.
28h
Part of the bcall() macro.
30h
Execute system routine FPAdd.
38h
System interrupt routine.

The RST commands are two bytes smaller than the corresponding bcall() command, and are also a lot faster, so use them whenever possible. And yes, the arguments to RST have to be hex numbers in -h form.

Hardware Interrupts

Hardware interrupts are based on some external event that has nothing to do with the instructions the CPU is executing.

Software or hardware, all interrupts when triggered are like normal CALLs, in that the current PC value is pushed onto the stack and there is a transfer to wherever the ISR is located. The actual address depends on the current interrupt mode.

IM x: Sets interrupt mode x. x = 0, 1, or 2.

Mode 0

In mode 0, only external hardware peripherals can generate interrupts. The ISR is a single byte sent by the peripheral that the CPU executes as an opcode. The calculator cannot use mode 0.

Mode 1

The CPU executes a RST 38h instruction when an external interrupt occurs; normally this is triggered by a hardware timer that ticks at about 140 Hz. The TI-83 Plus uses this interrupt to detect keys, blink the cursor, check the linkport, etc.

The system interrupt uses SP (naturally) and IY, so if you want to use these registers for other purposes you need to disable interrupts (or just not process the system interrupt).

Mode 2

The most fun. In mode 2, the CPU can theoretically jump to any address in memory. This is the interrupt mode we are interested in.

Setting Up a Mode 2 ISR

In Mode 2, the CPU gets the interrupt address using the I register. You’d figure I would be a 16-bit register to hold the address of the interrupt, but that would be too easy. Instead, I holds bits 15 to 8 of a pointer to a vector table where the interrupt’s real address is stored. (Note: you can only load into/from I using A). Bits 7 to 1 of this address is a number taken from the data bus and is functionally random. Since addresses are 16 bits the pointer must be an even number, and for this reason bit 0 is always zero

This is the official ZiLog description of a Mode 2 interrupt; it seems TI never intended mode 2 be used on the calculators, so things are a bit harder for us. The low byte of the interrupt address coming from the data bus is effectively random, so we need to set aside a 257-byte region of memory that contains a repeating byte that refers to where our interrupt code is placed.1

Assume we’ve already placed code to run in interrupts at $8A8A:

; Vector table is based at $8B00
    LD A, $8B
    LD I, A
; Fill the vector table with $8A so interrupts always jump to $8A8A,
; no matter the value of the low byte.
    LD HL, $8B00
    LD (HL), $8A
    LD DE, $8B01
    LD BC, 256
    LDIR

Now we should create the ISR. When an ISR begins, it has to save the values of all the registers it will modify. The reason for this is simple: if it didn’t, then the values of the registers would be changed 140 times a second to something unknown, which would play havoc with the normal program. This can be done either with the stack or very quickly with two instructions.

EX AF, AF': Exchange register pair AF with alternate register pair AF'.
EXX: Exchange register pairs BC, DE, and HL with alternate register pairs BC', DE', and HL'.

Interrupt Maintenance

DI: Disable interrupts
EI: Enable interrupts
HALT: Stop execution and enter low-power mode. On next interrupt, ‘wake up’ and resume execution.

You should have interrupts disabled while you are loading the interrupt data. Some cretin might have left the CPU in Mode 2 when you exited his game. If an interrupt triggers while you are overwriting pre-existing interrupt code, you’re definitely gonna be feeling below average.

Interrupts should be short and execute quickly (rendering a raytraced 3D scene in an interrupt is definitely not on :-). If an interrupt takes too long to complete it may very well be re-initiated and loop forever. To make sure this doesn’t happen, you must know the interrupt enable port (#3).

Bit If Set If Reset
0 ON key interrupts are serviced ON key interrupts are ignored
1 Timer interrupts are serviced Timer interrupts are ignored
4 Linkport interrupts are serviced Linkport interrupts are ignored

Bits 2 and 3 are usually unimportant, so we’re ignoring them here. Just know they should usually be set to 0 and 1, respectively.

When the interrupt is entered, output %00000000 to disable all interrupts and prevent an infinite loop. To return, output %00001011, restore the registers, re-enable interrupts, and then finally return.

Detecting Interrupts

You might want to know if interrupts are currently active or not. I can’t think of a use for this, but…
On the Z80 CPU, there are two devices (flip-flops) that are called IFF1 and IFF2. When the interrupt timer goes off, IFF1 is checked to see if the interrupt can run. IFF2 is used to save the status of IFF1.

To check the status of the flip-flops, a LD A, I or LD A, R instruction will store the status of IFF2 in the P/V flag. Reset means interrupts are disabled, set means interrupts are active.

Program 23-1

Here’s a sample program that demonstrates everything so far.

INTRPT_MASK   .EQU   %00001011

    bcall(_ClrLCDFull)

    DI                     ; Turn interrupts off until we're ready

; Set up vector table
    LD A, $8B
    LD I, A
    LD HL, $8B00
    LD (HL), $9A
    LD DE, $8B01
    LD BC, 256
    LDIR
; Copy the ISR into position
    LD HL, interrupt
    LD DE, $9A9A
    LD BC, interruptEnd - interrupt
    LDIR

    LD A, INTRPT_MASK      ; Enable hardware
    OUT (3), A

    IM 2                   ; Switch to Mode 2
    EI                     ; Activate interrupts

; GetKey and GetCSC only function in Mode 1, 
; so gotta use the key port.
    LD     A, %10111111
    OUT    (1), A

KeyLoop:
    IN     A, (1)
    CP     %01111111       ; If [DEL] pressed, exit
    JR     NZ, KeyLoop

    LD     A, %00001011    ; Enable hardware
    OUT    (3), A
    IM     1               ; Calculator needs Mode 1
    RET

counter:
    .DW    $0000

interrupt:
    .org $9A9A             ; This code runs from $9A9A
    EX     AF, AF'
    EXX
    XOR     A              ; Disable hardware
    OUT    (3), A

    LD     HL, 0
    LD     (CurRow), HL
    LD     HL, (counter)
    INC    HL
    LD     (counter), HL
    bcall(_DispHL)

    LD     A, INTRPT_MASK   ; Enable hardware
    OUT    (3), A
    EX     AF, AF'
    EXX
    EI
    RET
interruptEnd:

The program counts at a frenetic pace until you press DEL. For more fun, change the value of INTRPT_MASK to disable certain hardware events.

Interrupt Ports

You already know port 3, here’s another

Port 4 — Interrupt Status Port

Inputs

Bit If Set If Reset
0 ON key interrupt has been generated ON key interrupt has not been generated
1 Timer interrupt has been generated Timer interrupt has not been generated
3 ON key is being depressed ON key is up

Outputs

Bit Effect
1-2 Interrupt speed (0 to 3). %11 is slowest, %00 is fastest. Normal speed is %11

The TI-OS has the system flag OnInterrupt that is set if a one is read from bit 0 of port 4. This will result in an ERR: BREAK message when the program returns to the home screen. You can prevent this by

  • Disabling the ON interrupt.
  • Clearing the ON status flag.
  • Resetting the system flag.
  • The flag is only set in the system interrupt, so just don’t run it.

TSRs

TSRs are Terminate and Stay Resident programs. If you change the RET and exchange instructions in your interrupt to JP $003A, then you’ll process the calculator’s system interrupt as well as your own.

Whoa, whoa, wait a minute. Why are we jumping to $003A? Isn’t the system routine at $0038??

Well, yes, the Mode 1 interrupt does jump to $0038. What we are doing is swapping the shadow registers when our interrupt is run, and we want them to stay swapped when the system interrupt is running. A section of the code at $0038 looks like

0038: JR    $006A
003A: IN    A, (4)
.
.
006A: EX    AF, AF'
006B: EXX
006C: JR    $003A

Yes this is completely redundant, but by jumping to $003A the exchanges get skipped over.

This is useful if you still want GetKey and GetCSC and other Mode 1 features to work while you’re in Mode 2.
Also, if you don’t switch back to Mode 1 when the program ends, your interrupt will still be active, even during graphing and (provided they don’t use interrupts themselves) other programs! Unfortunately, drawing and archiving will kill ’em.

Unfortunately, because you have no control over other code once your program exits, TSRs tend to be unpredictable- it’s very easy for something else to stomp on the memory you were using and cause a crash.

Program 23-2

You can use this program instead of masking tape and a sharpie. Test it by pressing LOG.

Note that this is very similar to the last program, but we deliberately skip returning to mode 1 before exiting the program.

    DI

; We must store the interrupt somewhere in RAM so it sticks around
    LD     HL, interrupt
    LD     DE, $9A9A
    LD     BC, interrupt_end - interrupt
    LDIR

    LD     HL, $9900
    LD     DE, $9901
    LD     BC, 256
    LD     (HL), $9A
    LDIR

    LD     A, $99
    LD     I, A

    IM     2
    EI

    RET

interrupt:
    EX     AF, AF'
    EXX

    LD     A, $FF
    OUT    (1), A

    LD     A, $DF
    OUT    (1), A

    IN     A, (1)
    CP     $F7
    JP     NZ, $003A

    SET    TextInverse, (IY + TextFlags)

    LD     HL, interrupt_message - interrupt + $9A9A

    LD     DE, $3300
    LD     (PenCol), DE
    bcall(_VPutS)

    LD     D, $39
    LD     (PenCol), DE
    bcall(_VPutS)

    RES    TextInverse, (IY + TextFlags)

    JP     $003A

interrupt_message:
    .DB    "This TI-83 Plus is property of", 0
    .DB    "PUT YOUR NAME HERE!!... HANDS OFF!", 0

interrupt_end:

  1. Earlier versions of this guide claimed that the value on the data bus was predictable such that only a few vectors need be specified, but this is not true across different calculator hardware versions. You might find that your calculator always chooses one of a few addresses, but assuming others will do the same will probably make your users sad when your assumption turns out to have been false. ↩︎