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.