2

I'm trying to set an interrupt to catch presses of the ON button.

This is my code so far:

SetInterrupt:
 di
  ; copy the 4 bytes from InterruptVectorTable to cursorImage
  ; (I chose cursorImage because it's on a 512-byte boundary, 0E30800h)
 ld   hl, InterruptVectorTable
 ld   de, cursorImage
 ld   bc, 4
 ldir
  ; clone the same 4 bytes into the rest of the 256-byte interrupt vector table
 ld   hl, cursorImage
 ld   de, cursorImage + 4
 ld   bc, 252
 ldir
  ; load the address of the new interrupt vector in the i register
  ;  and set interrupt mode to 2
 ld   hl, cursorImage >> 8 & 0ffffh
 ld   i, hl
 im   2
 ei
 ret
FillScreen:
  ; fills the screen with black pixels
 ld a, 0
 ld hl, vRam
 ld bc, 320*240*2
 call _MemSet
 ret
InterruptVectorTable:
  ; try to call FillScreen whenever there's an interrupt
 .db 00, FillScreen & 0ffh, FillScreen >> 8 & 0ffh, FillScreen >> 16 & 0ffh

However, this just freezes my calculator (since the I can't use any keys to stop the program).

I believe the problem is in InterruptVectorTable. I don't really understand how the table is supposed to be formatted. The ez80 Application Note I linked to below says "Each vector is a 4-byte address pointing to the __vectptr segment," but the ez80 uses 24-bit addresses, so I'm not sure how to structure each vector.

Any help is greatly appreciated.


References I've read/tried to read:

Benjy Wiener
  • 1,085
  • 2
  • 9
  • 27
  • Can you use a debugger or simulator? That's normally extremely helpful for asm. – Peter Cordes Aug 27 '18 at 03:49
  • @PeterCordes I tried using CEmu for that reason, but the program just crashed the calculator as soon as I tried setting the interrupt. – Benjy Wiener Aug 27 '18 at 03:51
  • The emulator crashed? Or the virtual calculator *inside* the emulator crashed? If it's only the latter, then does it have a built-in debugger that lets you examine memory / registers at this point? The point of using an emulator is that is can let you debug and single-step even with interrupts disabled, totally invisibly to the guest. – Peter Cordes Aug 27 '18 at 04:04
  • @PeterCordes The virtual calculator. I started to look at the debugging tools, but once I saw that the programming was crashing on the emulator (while working on my actual device), I gave up on the emulator. When I get a chance I’ll try to check if CEmu has an option to break and debug at crashes. – Benjy Wiener Aug 27 '18 at 04:14
  • Wait, didn't you say it locks up on real hardware, too? I don't know TI-84 or z80, just the general principle that an *accurate* emulator/simulator with a built-in debugger can be *very* useful for debugging OS/kernel kind of activities like managing interrupts that just crash without any hint at *what* was wrong, if you make any mistakes. If you're sure CEmu isn't accurately emulating the way it runs on real hardware, then yeah it might not be useful. – Peter Cordes Aug 27 '18 at 04:18
  • 1
    @PeterCordes It froze my device, because all key-press input uses interrupts. On CEmu, though, it caused a RAM reset. – Benjy Wiener Aug 27 '18 at 04:20
  • seems to me you have wrong content of IVT, the unused 24-31 bits of 32b address are the last fourth byte (little endian), i.e. `.db FillScreen & 0ffh, FillScreen >> 8 & 0ffh, FillScreen >> 16 & 0ffh, 00` (or doesn't it have some directive to define 32 bits, like `.dd FillScreen` would be expected coming from x86 world) - based on reading that "Setting Interrupts" PDF document, page 13 ("10" on paper), chapter "Mapping the ISR Location in the Interrupt Vector Table". Also does your device really have eZ80F91 CPU? (only one which has 16 bit register `I`). – Ped7g Aug 27 '18 at 10:58
  • @Ped7g Regarding endian-ness, I actually tried both ways, with no luck. I also tried both ways with the null byte before and then again with the null byte after the address. There is a `.dl` directive for 3 bytes LE (which I also tried), but I couldn't find anything for 4 bytes. According to [https://wiki.tiplanet.org/TI-84_Plus_CE], the CPU is "eZ80F91-like". – Benjy Wiener Aug 27 '18 at 14:50
  • In early hardware revisions, interrupts were allowed. Now they are disabled. Looks like your real calc is one of the originals. Source: https://www.cemetech.net/forum/viewtopic.php?t=13966&start=0 – Zeda Aug 27 '18 at 23:08
  • @Zeda Interesting, thanks! Do you by any chance know how programs disable/catch the ON key without `im 2`? (Also, the post mentioned removing interrupts from the CE toolchain, maybe I’ll go through the revisions and try to see exactly what was removed, might be helpful.) – Benjy Wiener Aug 28 '18 at 00:09
  • 2
    @Zeda I did some research (looking a bunch of asm programs that seemed to disable the ON key), and it seems that the trick is to keep calling _GetCSC (no interrupts at all). I can also catch ON key presses by just checking if there's an ON-interrupt waiting and then resetting the flag. This will suit my purposes, and I'll try to remember to write an answer tomorrow. Thanks again! – Benjy Wiener Aug 28 '18 at 07:08
  • 1
    Ohhh, you are trying to prevent it from within an assembly program? That's definitely feasible then. Just remember to `res onInterrupt,(iy+onFlags)` in order to prevent the user from getting an Err: Break on exit. – Zeda Aug 28 '18 at 22:37

1 Answers1

1

Any help is greatly appreciated.

Thus posting this chit-chat as answer, because the comment section is annoying me by length limit plus formatting, and your "question" above makes this kind of "answer" sort of legit.

According to "Setting Interrupts" PDF document, on page 13, the 32 bit format of address is clear, in case of address 0x123456 the memory should contain bytes 56 34 12 00 (not sure if the last 00 can be any junk, or must be zero, I guess for future-compatibility zero is better, although I guess eZ80F91 will use only 24 bits, ignoring the last 8).

So the definition in your original question is very likely wrong, the 00 should be after the 3 bytes, not ahead.

The three byte .dl should be enough with additional extra junk byte by .db too (to avoid manual label decomposing into bytes).

I'm only familiar with classic Z80, so I don't have exact idea what is wrong with your code, but general principles and things which are probably worth a check:

  • check if the device has truly eZ80F91 ("-like" ??? who else is manufacturing some clone of it? I guess it's either original from Zilog or not, no "-like" possible), because any other eZ80 variant has only 8 bit I register, and needs different interrupt table setup and interrupt handling (maybe try ld hl,0x1234 ld i,hl ld hl,0 ld hl,i and check the value in hl if it is back to 0x1234).

  • check the binary produced or in the debugger while the code is initializing vector table, that the memory contains values as expected

  • check the device description (if available), which kind of interrupts are fired and when, why do you actually expect button "on" to fire an interrupt? (for example ZX Spectrum - the Z80 machine I'm familiar with - has no keyboard interrupt, the keyboard must be polled by code, the only interrupt was fired upon start of vertical retrace of display beam, i.e. at ~50Hz with PAL/SECAM ZX models, and about ~60Hz with US NTSC variants, causing the games to run a tad faster on US ZX clones ... IIRC the TI calculators have timer interrupt, like 100Hz one, but I never studied them in depth, so this may be completely wrong info)

  • make sure you are in "ADL" mode, not in "Z80" compatibility mode.

  • your "FillScreen" routine is trying to return, but it doesn't seem to have proper interrupt-like prologue/epilogue, so wherever it will return, it did damage the content of registers, and it does not return through reti.

  • you also return from SetInterrupt routine into something... what is being run meanwhile, while your interrupt is installed?

You can first try "empty" interrupt handler like

FillScreen:
    ei        ; not sure if there's implicit DI - if yes, EI needed
    reti

to see if the code running in main thread is ok (and your interrupt handler works). Mind you, if it's the ordinary calculator handler running, and it requires it's own interrupt handler for it's life, then just installing empty interrupt will hamper the functionality already ... maybe you shouldn't ever return from your code (the main thread, like where the setInterrupt is called) and do your own infinite main loop).

If you want to do more in your interrupt, you must preserve register values for the code in the main thread, for example if you know the main thread does NOT use alternate registers, then you can switch registers quickly by

interruptHandler:
    di        ; disable interrupts until done
    ; (especially if you know your interrupt may take longer to run)

    ; preserve current register values (by switching to alternate ones)
    ex   af,af
    exx
    ; do your stuff here (destroying alternate register values)
    ; which is OK, if your interrupt handler is the only code using them
    ...
    ; restore the register values back (by switching to original ones)
    exx
    ex   af,af
    ; return from interrupt
    ei
    reti

Or if you know there's always enough space on the stack, you use push/pop to preserve original register values.

Or if the stack space may be too tense, but you have separate memory block which can be used as interrupt handler stack, you can switch to that first:

interruptHandler:
    di
    ; preserve current stack pointer (self-modify code)
    ld   (interruptHandler_SP+1),sp
    ld   sp,top_of_interrupt_stack
    ; preserve registers as needed (AF with flags being a MUST)
    push ...
    ; do your stuff here
    ...
    ; restore registers as needed
    pop ...
    ; restore stack pointer
interruptHandler_SP:
    ld   sp,0x123456     ; this will be overwritten at start of handler
    ; return from interrupt
    ei
    reti

Thinking about it, your interrupt handler is obviously not interrupt handler, so even if it would run once correctly, that would be the last correct thing happening in your device.

Also filling the screen is a bit unfortunate choice (as it will take loooong time to finish and it's difficult to see it twice).

Maybe as a quick test do something like:

testInterrupt:
  di
  push   af
  ; increment first byte of video ram to make some visible "noise"
  ld     a,(vRam)
  inc    a
  ld     (vRam),a
  ; restore flags, enable interrupts, return back to main code
  pop    af
  ei
  reti

And generally the interrupt handlers should be very fast and tiny, doing task like clearing vram should be left to main code, the interrupt should probably just set some global flag, that vram clearing is required (to finish within few T cycles), and then the main code can in loop test for various event flags, and react to the "clear vram" flag by clearing the vram. There shouldn't be any serious "biz" logic in the handlers, only collecting the state/data which is imminent (like data on serial bus I/O) into some flags/buffers, and letting the main code outside of interrupt to process such flags/buffers with the full logic.


Maybe even consider trying first some classic Z80, unless you really want eZ80F91 badly. The classic Z80 has ton of materials available, with different machines and emulators as it was very popular CPU (for example I'm sort of Z80 "expert" thanks to the ZX Spectrum computer, I did code several demos and games for it back around 1991-1996 years). So it will be easier to help with that (seems this question was unanswered long enough to guess there's not that many people coding for eZ80F91).

Ped7g
  • 16,236
  • 3
  • 26
  • 63
  • Thanks. I haven't gotten a chance to go through everything you wrote yet, but I was able to confirm that I is 16 bits (using `ld hl, $4162` , `ld i, hl` , `ld hl, 0` , `ld hl, i` , `ld (pixelShadow), hl` , `ld hl, pixelShadow` , `call _PutS`). – Benjy Wiener Aug 27 '18 at 17:38