Warning: Matt figured out most of this by himself and warned that it might not all be correct. I haven't verified any of this myself, except the isr (interrupt service routine) templates. They work nicely. If you have any comments or corrections, email me and I'll fix it.
There are two types of hardware events that causes interrupts.. the timer interrupts and the ON key interrupts. Basically a 200 hertz timer causes an interrupt, and so does the ON key. There is also a possibility that other events cause an ON key interrupt, although I don't know why or how. Port 3 seems to control all this mayhem. With Port 3, you can check the status of the ON key, the timer interrupt, the ON key interrupt, and the LCD status. This is (as far as I know) the same for the TI-86 as the TI-85. Thus when reading from Port 0:
Read 0000.... Bit 3 = 1: ON not pressed 0: ON pressed now Bit 2 = 1: Timer interrupt happened 0: timer interrupts hasn't happened Bit 1 = 1: LCD is on 0: LCD is off Bit 0 = 1: ON interrupt has happened 0: ON interrupt hasn't happenedAnd when we are writing to Port 3 we appear to be writing either 1 byte at a time, or 2 bytes at a time. Some bytes and there functions are listed below:
DI
HALT
Well the calc wouldn't lock up if the ON interrupt was an NMI because it should jump to the int handler routine no matter what happens. But lo and behold, it simply crashes. That's nice.
The reset - initial routine ran at $00
Right when you put your batteries in your calc, it's alive! Yes, it
will start executing assembly code immediately. First off the Z80 will
immediately start executing code at the start of memory at $0000. That
is:
0000 3e57 ld a,$57 ;RST 0 0002 d304 out ($04),a 0004 c32e0e jp $0e2ePort 4 has something to do with the power on routine, it probably signals the TI hardware to do it's things. It is just speculation, however, no one really knows what it does exactly (expect Pat, of course). Anyway, thats not really important. Let's see, after outputting $57 to Port 4 it jumps to $0E2E:
0e2e 0600 ld b,$00 ;RST 0 continued 0e30 319ffe ld sp,$fe9f ;sp=$FE9F 0e33 10fe djnz $0e33 0e35 dd210100 ld ix,$0001 ;inc sp 0e39 dd39 add ix,sp 0e3b ddf9 ld sp,ix 0e3d 30f4 jr nc,$0e33 ;if no overflow, delay and do it again 0e3f 31cefb ld sp,$fbce ;sp=$FBCE 0e42 3e57 ld a,$57 0e44 d304 out ($04),a 0e46 3e7c ld a,$7c ;set screen memory at 3C00 0e48 d300 out (scrOffset),a 0e4a ed56 im 1 0e4c d307 out (linkport),a 0e4e 3e0a ld a,10 ;set contrast 0e50 d302 out (contrast),a 0e52 3ec0 ld a,$c0 ;set default link port state (both lines high) 0e54 d307 out (linkport),a 0e56 97 sub a ;out (3),0 0e57 d303 out ($03),a 0e59 3e01 ld a,$01 ;out (3),1 0e5b d303 out ($03),a 0e5d fb ei ;enable interrupts 0e5e 76 halt ;wait for ON key interrupt 0e5f 7e ld a,(hl) ;print ASCIIZ string. 0e60 b7 or a 0e61 2806 jr z,$0e69 0e63 cd933f call _putc 0e66 23 inc hl 0e67 18f6 jr $0e5f 0e69 c9 retAs you see, it initializes the basic Z80 system stuff - the stack, the linkport, the contrast, the screen display area. It then sends $00 $01 to Port 3 and waits for the ON key interrupt. Once the ON keyt interrupt occurs (you press the ON key), it jumps to $38, branches off at $69 since it is in the middle of an ON key interrupt, jumps to $8F, calls $ When the ON interrupt occurs it branches at $8F:
0066 db03 in a,($03) 0068 1f rra 0069 3824 jr c,$008f ;On interrupt has happened - goto 008f(calc on,reset?) 006b 1f rra 006c 3828 jr c,$0096 ;if LCD is on and On interrupt hasnt occured,goto 0096 006e 1816 jr $0086 ;if neither set, end interruptHere is $8F:
008f cdd30b call $0bd3 ; On key pressed - go here 0092 3e0a ld a,$0a ; On has been pressed 0094 18da jr $0070 ; Finish interrupt$0BD3 (calc on, reset):
0bd3 0600 ld b,$00 0bd5 217a09 ld hl,$097a ;wait until on key status is the same loop: 0bd8 db03 in a,($03) ;for a certain period of time. 0bda e608 and 8 0bdc b8 cp b 0bdd 47 ld b,a 0bde 20f5 jr nz,$0bd5 0be0 2b dec hl 0be1 7d ld a,l 0be2 b4 or h ; Check if HL = 0 0be3 20f3 jr nz,$0bd8 ; If not, loop back to loop: 0be5 b8 cp b ;if on was pressed, goto 0bfc 0be6 2814 jr z,$0bfc ;on NOT pressed 0be8 db06 in a,(RAMport) ;if we turned off from 0c5c, goto 0e52 0bea b7 or a 0beb ca520e jp z,$0e52 0bee fdcb095e bit onRunning,(iy+onflags) ;if we're on, return 0bf2 c0 ret nz 0bf3 e1 pop hl ;if on not pressed and calc is off, 0bf4 97 sub a ;end interrupt. 0bf5 d303 out ($03),a ; $00 $01 - Wait for ON key interrupt, stay off 0bf7 3e01 ld a,$01 0bf9 c38800 jp $0088 ;on pressed 0bfc db06 in a,(RAMport) ;if we turned off from 0c5c, goto 0ced 0bfe b7 or a 0bff caed0c jp z,$0ced ; Init routine 0c02 fdcb125e bit shift2nd,(iy+shiftflags) ;if not 2nd, turn calc on. 0c06 2861 jr z,$0c69 ; Calc on routine 0c08 3e02 ld a,$02 ; 2nd pressed?? 0c0a d303 out ($03),a 0c0c fdcb0f7e bit 7,(iy+$0f) 0c10 2806 jr z,$0c18 0c12 fdcb0856 bit APDable,(iy+APDflags) 0c16 2832 jr z,$0c4a ;turn calc off ; Not from APD but [2nd][on] 0c18 cdb93d call $3db9 ;b,662f - _flushallmenus, fill bottom of screen ;from $c29c 0c1b cdbf3d call $3dbf ;1,44e3 - cursor stuff 0c1e fdcb01e6 set 4,(iy+$01) 0c22 cd1e0e call $0e1e 0c25 fdcb01a6 res 4,(iy+$01)Now the clever one would be like.. WTF? How can an ON interrupt occur but the ON key is not pressed? That is what I am wondering. The answer - there is a possbility that an ON interrupt is not only caused by the ON key being pressed, but also pressing "Down-Left", remmeber that little Down-Left bug? I mean, that damn loop at $0BD3 probably is what causes the calc to grind to a stop whenever Down-Left is pressed. Weird, ehh? There might be other things that cause an ON interrupt, but I have no idea.
If the calculator is being turned on (ON key pressed) a jump to $0C69 is made, the calc_on routine. Two things can cause a calculator to shut off. [2nd][ON] or APD expiring. If the APD is expiring (see $0C12 and $0C16) it skips to $0C4A, which skips part of the calc_off routine like clearing the screen.. If [2nd][ON] was pressed, the routine falls through at the next instruction at $0C25. Let us take a look at this mysterious calc_off routine:
calc_off: 0c29 fdcb237e bit 7,(iy+$23) ;call user off routine 0c2d c4c53d call nz,user_off 0c30 b7 or a 0c31 f5 push af 0c32 21eec3 ld hl,$c3ee ;res 2,(iy+onflags) 0c35 cb56 bit 2,(hl) ;if it was set, call _clrScrn 0c37 cb96 res 2,(hl) 0c39 c4cb3d call nz,_clrScrn ;$3dcb 0c3c f1 pop af 0c3d 3805 jr c,$0c44 0c3f cdfb0d call getRAMcksum ;getRAMcksum 0c42 1803 jr $0c47 0c44 215aa5 ld hl,$a55a 0c47 22d8c1 ld (_onCheckSum),hl ;save RAM checksum 0c4a 3e01 ld a,$01 0c4c d303 out ($03),a ;$01 - Wait for ON key interrupt I guess 0c4e fdcb129e res shift2nd,(iy+shiftflags) 0c52 fdcb099e res onRunning,(iy+onflags) 0c56 08 ex af,af' 0c57 d9 exx 0c58 fb ei 0c59 76 halt ;minumum power loop 0c5a 18fd jr $0c59Well there is the user_off routine! I never mentioned this before but it is the same concept as the user interrupt routine, except it is only called when the calc is shut off and not 200hz. Plus you must set different flags, etc, for its functionality. I might include some sample code for the user_on and the user_off routines in an Advanced TI-OS section.
What's with these flags? Your probably wondering. If the calc is off then it is OFF, right? Well the ON key can be pressed while the calculator is already ON, so they use these flags to determine if the calculator is really off or really on. If the calculator is ON you dont want it executing a Calc_ON routine everytime the ON key would be pressed.
Weirdness more when you see a jr $0C59. I mean, if this was a one time thing lets press the ON key and only one call to the interrupt handler, you wouldn't need this type of loop. But I assume some redudant calls are made to the $38 and it seems to return into this infinite loop until the ON key has actually been pressed. This futhurs me to believe that something else can trigger an ON interrupt other then the ON key being pressed.
Note about HALT: When the HALT instruction executes it puts the processor in a low power mode, consuming only 10 uA (10 millionths) of an AMP. Basically all HALT does is wait for an interrupt, and execut NOP's in the mean time to maintain memory refresh logic.
How to shut off the calc... QUICK!
The following code below does it well. It mimicks what happens in the
interrupt handler. The DI disables interrupts which is the state of the
calc in the interrupt handler. We send $01 to Port 3, reset those two flags,
and wait for the ON interrupt. Simple!
di ld a,$01 out ($03),a ;$01 - Wait for ON key int res shift2nd,(iy+shiftflags) ;Clear 2nd status res onRunning,(iy+onflags) ;Calc no longer running ei ; Enable interrupts halt ; Wait for on key interruptNow here is a program I wrote that demostrates this:
#include "ti86asm.inc" .org _asm_exec_ram InnerLoop: call _getkey cp kEnter jr nz, InnerLoop ; Shut off Calc di ld a,$01 out ($03),a ;$01 - Wait for ON key int res shift2nd,(iy+shiftflags) ;Clear 2nd status res onRunning,(iy+onflags) ;Calc no longer running ei ; Enable interrupts halt ; Wait for on key interrupt ld a, '!' call _putc ret .endTo use this program simply compile it and run it. The run indicator goes but nothing happens. It is simply waiting for the Enter key. Now press the enter key. Wow! That shut off real quick, didn't it?!? But notice how the ! never gets displayed on the screen. It doesnt return from the interrupt, dude. It branches to another part of the routine that turns on the calc and returns to the homescreen.
Calc-on routine:
Now lets see how the calculator turns ON. If you will look back from
earlier at $0C02 and $0C06, it knows the ON key has been pressed, so it
simply checks the 2nd key status. If the ON key was pressed but the second
key was not, it will jump to $0C69, the calc_on routine, which is:
0c69 cd9b01 call initTimeoutCnt 0c6c fdcb095e bit onRunning,(iy+onflags) ;if calc alreaday on, signal that 0c70 2059 jr nz,$0ccb ;on key was pressed and ret. 0c72 fdcb09de set onRunning,(iy+onflags) ; The calc is now officially on 0c76 3e03 ld a,$03 0c78 320bc0 ld (_APDWarmUp),a 0c7b 21edc3 ld hl,$c3e5+8 0c7e cb66 bit 4,(hl) 0c80 202b jr nz,$0cad 0c82 fdcb0f7e bit 7,(iy+$0f) 0c86 2805 jr z,$0c8d 0c88 cb56 bit 2,(hl) 0c8a ca230d jp z,$0d23 0c8d ed7bd6c1 ld sp,(_onSP) 0c91 cd9505 call $0595 0c94 fdcb0c76 bit 6,(iy+$0c) 0c98 c4d13d call nz,$3dd1 0c9b cdd00c call $0cd0 0c9e fdcb235e bit 3,(iy+$23) ;call user on routine 0ca2 c4d73d call nz,$3dd7 0ca5 3e0b ld a,$0b ;turn on screen 0ca7 d303 out ($03),a 0ca9 fb ei ;enable interrupts 0caa c38502 jp $0285First it checks to see if the ON key wasn't pressed while the calculator is already ON. If the calculator is ON and the ON key is pressed, it basically ends the routine by jumping somewhere else that sets the onInterrupt flag and returns from the interrupt handler.
It also seems to mess with the APD and the stack. After that is calls the user ON routine, sends $0B to Port 3 which is basically normal mode (i.e. Screen on, interrupts active, etc). It then enables the interrupts and jumps to $0285, which is basically the Home Screen.
So that is what happened when you ran that example program. It didnt return from the interrupt handler to print the '!' character, it went to the homescreen. If you wanted your program to be able to shut off and when ON is pressed it would return to the program, the calc you would either have to override the default interrupt handler with IM 2 (ahh! - pretty stupid) or simply do not return from the user on routine.
Fixing the Down-Left Bug
Jimmy Mardell posted a fix some time earlier that describes three ways
to fix the down-left bug. Since it isn't posted on his web page, I will
post it here. I slightly modified it for understandability. I am sure he
doesn't mind :-)
I've made some research on what causes the famous down-left freeze bug (or is it a feature!?) and how to get around it when programming. I hope some programmers find this information useful - the routines below is at least used in the upcoming Sqrxz 1.0.
So - how do you do to avoid the freeze bug? I've found three majorways to solve the problem:
In most games, this method is enough to avoid the problems. There are two disadvantages doing it this way though. Those are:
Set indicOnly, indicflags (SET 2,(IY+$12))
When disassembling the ROM I found that if the second bit at (IY+$12) is set, there will be no call to the routine that check port 1 for keypresses. That routine lies at $01A1, and if that routine is not called, the down-left bug is gone. This is also a very simple way to get around the problem, but it still has the disadvantage that you have to read keypresses through ports. And most of the time when method 1 (Disabling interrupts) isn't enough, it usually means you want to make your own interrupt handler, and then method 3 is best suited for it (seebelow). If you use this method, you MUST reset the flag with RES 2,(IY+$12) before the program terminates - else the calc freezes.
Creating your own interrupt handler
This is the most complex method, which I had some problems with first. Creating an interrupt handler is not much of a problem really. This is easily done with the following code:
ld hl,$8E00 ld de,$8E01 ld (hl),$8F ld bc,256 ldir ld hl,inthandler ld de,$8F8F ld bc,intend-inthandler ldir ld a,$8E ld i,a im 2This code stores a vector table at $8E00-$8F00 and the interrupt handler at $8F8F. Since most of RAM page 1 is free to use (????), this seems to work without problems. The interrupt handler could then look something like
inthandler: ex af,af' exxThis method is used in Sqrxz 0.9. The problem is that each interrupt ends with a jump to the default interrupt at $38 - which has the down-left bug. So, I tried to replace jp $38 with EI \ RETI (which is how the default interrupt handler ends) but that didn't work - The calc crashed :( To find out what was wrong, I had to play around with the default interrupt handler. Anyway, this is how a "homemade" interrupt handler should look like, that doesn't use the buggy handler in the ROM AND still uses ROM routines for reading keys (without having the down-left bug):ex af,af' exx jp $38 intend:
inthandler: ex af,af' exxIt's necessary to send $09 $0B to port 3 when ON is _NOT_ pressed and when ON is pressed, you have to send $0A $0B to port 3 - else the calccrashes. This method works very well - it allows the user to create his own interrupt handler and still use the ROM routines to convert port 1 bit code to scancodes without having the annoying down-left bug.in a,(3) rra push af ; This three lines are not needed call nc,$01A1 ; if you only read keys through ports pop af ld a,9 adc a,0 out (3),a ld a,$0B out (3),a ex af,af' exx ei reti intend:
Ok, it is back to Matt speaking again ;-). How the above routine works is simple. The Down-Left bug appears to somehow trigger an ON interrupt, I am almost sure of this. Since the above code is the only interrupt handler, it does not handle ON interrupts. It simply calls the getkey routine everytime, whether or not the ON interrupt has occured. It does update the hardware with the status of the ON key by sending $09 $0B or $0A $0B
Speeeeeedy IM 2
When we speak of speedy IM 2 we are talking about an IM 2 with an attitude! Yes. An IM 2 with an attitude. See, all that an interrupt routine requires is the basic exx stuff, the ei and reti at the end, and the $09 $0B or $0A $0B at the end of the interrupt routine which updates the status of the ON key and keeps the calc alive. It is fast because there is no jump to $38. We are the interrupt routine, the whole interrupt routine and nothing but the interrupt routine. Because of this, this speeedy IM 2 routine will only work in asm programs. This is because the standard interrupt routine in TI-OS needs to do other things like update cursor, get keys, etc.. If you use this outside of your program and try to make a TSR out of it will crash the calc. A IM 2 TSR routine was shown in the "Basic" interrupt section. Since we are running in an asm program, we know that we Ram Page 1 will be default set at $8000. Most of Ram Page 1 is free memory, so we will use $8E00 for the vector table of IM 2 and $8F8F for the actual routine. Bit 0 of Port 4 is used to check the status of the ON interrupt.
Here is the Speeeeedy IM 2 template:
#include "ti86asm.inc" int_addr equ $8F8F ; Start of interrupt code .org _asm_exec_ram ; Set instruction counter to $D748 (start of all asm programs) ;==================================================================================================== ; Install IM 2 Interrupt Handler ;==================================================================================================== ld hl,$8E00 ; Source = $8E00 (Start of vector table) ld de,$8E01 ; Destination = $8E01 ld (hl),$8F ; Set first byte so it is copied to all of them ($8F8F) ld bc,256 ; 256 bytes (size of vector table) ldir ; (DE) <- (HL), BC=BC-1, Loop till B=0 ld hl,int_copy ; Source = interrupt routine ld de,int_addr ; Destination = $8F8F (as specified in vector table) ld bc,int_end-int_start ; Length of custom interrupt routine ldir ; (DE) <- (HL), BC=BC-1, Loop till B=0 ld a,$8E ; Set up address of vector table ld i,a ; MSB = $8E ; LSB = (byte supplied by data bus - random) im 2 ; Set interrupt mode 2 ; ------[ program code goes here ]------------ im 1 ; Back to Interrupt Mode 1 ret ; Return to shell or TI-OS ;==================================================================================================== ; IM 2 Interrupt Handler code ;=================================