Home > Z80 Assembly > How To Write ZX Spectrum Games – Chapter 17

How To Write ZX Spectrum Games – Chapter 17

Interrupts

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

Setting up your own interrupts can a nightmare the first time you try it, as it is a complicated business.  With practice, it becomes a little easier.  To make the Spectrum run our own interrupt routine, we have to tell it where the routine is, put the machine into interrupt mode 2, and ensure that interrupts are enabled.  Sound simple enough?  The tricky part is telling the Spectrum where our routine is located.

With the machine in mode 2, the Z80 uses the I register to determine the high byte of the address of the pointer to the interrupt service routine address.  The low byte is supplied by the hardware.  In practice, we never know what the low byte is going to be – so you see the problem?  The low byte could be 0, it could be 255, or it could be anywhere in between.  This means we need a whole block of 257 bytes consisting of pointers to the start address of our service routine.  As the low byte supplied by the hardware could be odd or even, we have to make sure that the low byte and the high byte of the address of our service routine are identical.  This seriously restricts where we can locate our routine.

We should also only locate our table of pointers and our routine in uncontended RAM.  Do not place them below address 32768.  Even paging in an uncontended RAM bank for the purpose, such as bank 1, will produce problems on certain models of Spectrum.  Personally, I find bank 0 to be as good a place as any.

Let us say we choose address 51400 as the location of our interrupt routine.  This is valid as both the high byte and low byte are 200, since 200*256+200 = 51400.  We then need a table of 129 pointers all pointing to this address, or 257 instances of defb 200, located at the start of a 256-byte page boundary.  Assuming we put it high up out of the way, we could start it at 254*256 = 65024.

We would do this:

       org 51400

int    ; interrupt service routine.

       org 65024

       ; pointers to interrupt routine.

       defb 200,200,200,200
       defb 200,200,200,200
       .
       .
       defb 200,200,200,200
       defb 200

Ugh! Still, now we come to our interrupt routine. Interrupts can occur during any period, so we have to preserve any registers we are likely to use, perform our code, optionally call the ROM service routine, restore the registers, re-enable interrupts, then return from the interrupt with a RETI. Our routine might resemble this:

int    push af             ; preserve registers.
       push bc
       push hl
       push de
       push ix
       call 49158          ; play music.
       rst 56              ; ROM routine, read keys and update clock.
       pop ix              ; restore registers.
       pop de
       pop hl
       pop bc
       pop af
       ei                  ; always re-enable interrupts before returning.
       reti                ; done.
       ret

If you are not reading the keyboard via the system variables you may wish to dispense with the RST 56. Doing so will free up the IY registers. However, if your game’s timing counts the frames using the method described in the timing chapter, you will need to increment the timer yourself:

       ld hl,23672         ; frames counter.
       inc (hl)            ; move it along.

With all this in place, we are ready to set off our interrupts. We have to point the I register at the table of pointers and select interrupt mode 2. This code will do the job for us:

       di                  ; interrupts off as a precaution.
       ld a,254            ; high byte of pointer table location.
       ld i,a              ; set high byte.
       im2                 ; select interrupt mode 2.
       ei                  ; enable interrupts.
  1. slenkar
    October 27, 2013 at 1:22 am

    This is the only tutorial on IM2 that I understand, thanks!

  2. Prophet
    February 10, 2014 at 3:48 am

    I’m still learning Speccy machine code so forgive me if I’m wrong, but in the last code block, shouldn’t “ld a, 200” be “ld a, 254”? Register I is supposed to hold the high byte of the pointer table address, not the interrupt routine address, right?

    • February 10, 2014 at 4:45 pm

      Well spotted prophet! I’ve updated the article to correct the mistake. Thanks for bringing it to my notice. 🙂

  3. February 12, 2014 at 4:14 pm

    IIRC the low byte used for the look-up was often predictable (same as ‘i’ – e.g. 254,254), although this is of course bad practice. However some hardware (Kempston-compatible joystick interfaces for example) caused it to be more or less unpredictable, which broke early Spectrum games.

    Somebody noticed that the empty top-end of the standard Spectrum ROM was padded with 0xff bytes, and that with i=63, the interrupt would execute code at 0xffff. By placing a 0x18 byte (jr) in the final byte of memory, the offset would be collected from the first byte of ROM, which was 0xf3 (di) which conveniently jumped back to 0xfff3(?), where you could locate your handler (usually another jump!).

    I think this broke on later ROMs, and possibly with Interface 1.

  4. Zoltar
    December 4, 2014 at 7:29 pm

    Any thoughts on how to do this on a 16k machine? I was thinking the printer buffer would be a good place for the vector table but you say it’s a bad idea sticking stuff below 32768. The other option would be to use my own code to simply replace the BASIC ROM, much like the 16k Interface II carts that were briefly available, then use I could use my own routine for IM 1.

  5. KrazyKattapilla
    August 25, 2016 at 5:02 pm

    I agree with Slenkar – this article provides a clear explanation of the key points. Thank you

    Two things which it is worth keeping in mind.

    1 – The RET after the RETI would seem to be redundant.
    2 – The opcode IM2 is not recognised by Pasmo (I don’t know about other compilers) and is treated as a label. Changing it to IM 2 (putting a space between the M and the 2) seems to rectify this.

    KK

  1. No trackbacks yet.

Leave a reply to KrazyKattapilla Cancel reply