Day 23

Interrupts

Difficulty:
novice · intermediate · advanced · expert


· Previous · Lesson Index · Next ·

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 table 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 b_call() macro.
30h
Execute system routine FPAdd.
38h
System interrupt routine.

The RST commands are two bytes smaller than the corresponding b_call() 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

At a frequency of about 140Hz or less, the CPU executes a RST 28H instruction. 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, on the TI calculators something is awry and interrupt behavior is contradictory to what was described above. With a lot of experimentation (and a lot of RAM clears), I have discovered that the value of the interrupt vector is made up as follows:

In the past, programmers who wanted to use an interrupt just assumed the low-order byte of the interrupt vector was random and had to set up a 257-byte table made up of the same byte value over and over. As a result, the largest scrap RAM area would be untouchable for variable storage.

The vector table is initialized with code similar to the following:

; Assume an ISR located at $9DF0
    LD    HL, $9DF0

    LD    ($993F), HL
    LD    ($997F), HL
    LD    ($99BF), HL
    LD    ($99FF), HL

N.B. All the vectors are stored in AppBackupScreen RAM area.

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.

If a HALT is executed, the LSB of the next interrupt vector will always be %11111111.

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

The meanings of bits 2 and 3 are not known. When the interrupt is entered, output %00000000 to disable all interrupts and prevent an infinite loop. To return, output %00001101, 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

    b_call(_ClrLCDFull)

    DI                     ; Turn interrupts off until we're ready

; Load interrupt address vectors
    LD     HL, interrupt
    LD     ($993F), HL
    LD     ($997F), HL
    LD     ($99BF), HL
    LD     ($99FF), HL

    LD     A, $99
    LD     I, A

    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

interrupt:
    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
    b_call(_DispHL)

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

counter:
    .DW    $0000

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
0 Force [ON] interrupt status
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

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.

Program 23-2

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

    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

; Don't bother with specific memory locations, 
; just storing $9A everywhere will work
    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
    b_call(_VPutS)

    LD     D, $39
    LD     (PenCol), DE
    b_call(_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: