The Z80 interrupt on the Spectrum is triggered at the start of the vertical blank period of the screen refresh. This cannot be changed. What can be changed is the address that is jumped to upon interrupt.
The Z80 supports three interrupt modes:
- IM 0: Executes an instruction that is placed on the data bus by a peripheral.
- IM 1: Jumps to address &0038
- IM 2: Uses an interrupt vector table, indexed by value on data bus.
IM 0 is not used on the Spectrum.
The Spectrum normally operates in IM 1; this is set by the ROM bootup process shortly after power up by a routine at address &121C. The interrupt implements the ROM keyboard scanning routine. As an aside; this interrupt does not preserve the IY register so if you wish to use it in your code you will either need to disable interrupts or use IM 2.
Games programmers will usually set the Z80 to Interrupt Mode 2 (IM 2) on the Spectrum. As the Spectrum interrupts are tied to screen refresh it provides a method of frame locking games for smooth tear free drawing of sprites and backgrounds.
Interrupt Mode 2
An interrupt vector table of 128 words is created. This must be on a 256 byte boundary; i.e. the low byte of the table address is &00. The I register is set to the high byte of that table, an IM 2 instruction is issued and interrupts are enabled.
When an interrupt occurs, the CPU will use the value on the data bus as an index into this table to fetch an address. Ideally bit 0 of the data bus should be reset to ensure that the index address is on a word boundary.
The principle behind this is that you can have a number of peripherals, all putting unique values on the data bus and triggering interrupts. The system can then call the relevant interrupt handling routine for that peripheral.
In practice, the Spectrum didn’t use IM 2 like this; and it was widely assumed that you couldn’t guarantee what was on the data bus when the interrupt occurred, so programmers were forced to generate a vector table of 257 bytes; all containing the same value. This meant that the interrupt routine was restricted to being at an address in memory where the high and low bytes were the same, i.e. at 0xFDFD.
On the 48K Spectrum there was a sneaky shortcut that saved the 257 bytes required for the interrupt vector table. There is a block of over 256 &FF’s at location &3900 in the 48K ROM. This space was reserved for functionality that didn’t make it into the 48K Spectrum.
Programmers used to use this as the vector table. The Spectrum would then jump to location &FFFF upon interrupt. This is the clever bit. By placing the opcode for JR (&18) at that location and knowing that the PC would wrap to address &0000 (in the ROM) during execution of that instruction, which contains the value &F3, this would then execute the instruction JR &F3; a negative jump back 13 bytes to address &FFF4. By sticking a JMP instruction here you can then jump to the location of your interrupt routine of choice.
It does not work on the 16K Spectrum as there is no RAM from &8000 onwards, and doesn’t work on the 128K variants as the 128K ROM has code at location &3900. You will need to create your own vector table for games to be compatible with those systems.
Here is some sample code to illustrate the sneaky shortcut:
Initialise_Interrupt: DI ; Disable interrupts LD HL,Interrupt LD IX,0xFFF0 ; This code is to be written at 0xFF LD (IX+04h),0xC3 ; Opcode for JP LD (IX+05h),L ; Store the address of the interrupt routine in LD (IX+06h),H LD (IX+0Fh),0x18 ; Opcode for JR; this will do JR to FFF4h LD A,0x39 ; Interrupt table at page 0x3900 (ROM) LD I,A ; Set the interrupt register to that page IM 2 ; Set the interrupt mode EI ; Enable interrupts RET Interrupt: DI ; Disable interrupts PUSH AF ; Save all the registers on the stack PUSH BC ; This is probably not necessary unless PUSH DE ; we're looking at returning cleanly PUSH HL ; back to BASIC at some point PUSH IX EXX EX AF,AF' PUSH AF PUSH BC PUSH DE PUSH HL PUSH IY ; ; Your code here... ; POP IY ; Restore all the registers POP HL POP DE POP BC POP AF EXX EX AF,AF' POP IX POP HL POP DE POP BC POP AF EI ; Enable interrupts RET ; And return
In order to make your game compatible with the 128K Spectrum, you will need to create your vector table. I usually arrange my memory map as follows:
- SP @ 0x000 – so it grows down from the top of RAM
- Interrupt vector table @ 0xFE00 (257 bytes) with the value 0xFD
- A 3 byte jump instruction @ 0xFDFD to the interrupt routine
The modified version of Initialise_Interrupt is as follows:
Stack_Top: EQU 0x0000 ; Stack at top of RAM IM2_Table: EQU 0xFE00 ; 256 byte page (+ 1 byte) for IM2 IM2_JP: EQU 0xFDFD ; 3 bytes for JP routine under IM2 table Initialise_Interrupt: DI LD DE, IM2_Table ; The IM2 vector table (on page boundary) LD HL, IM2_JP ; Pointer for 3-byte interrupt handler LD A, D ; Interrupt table page high address LD I, A ; Set the interrupt register to that page LD A, L ; Fill page with values 1: LD (DE), A INC E JR NZ, 1B: INC D ; In case data bus bit 0 is not 0, we LD (DE), A ; put an extra byte in here LD (HL), 0xC3 ; Write out the interrupt handler, a JP instruction INC L LD (HL), low Interrupt ; Store the address of the interrupt routine in INC L LD (HL), high Interrupt IM 2 ; Set the interrupt mode EI ; Enable interrupts RET
This is quite economical on memory, and will ensure that your 48K game is compatible with the 128K Spectrum.
If you are planning on using memory paging on the 128K Spectrum, I’d suggest moving the interrupt routine elsewhere. The above code will work, but remember to set IM2_JP to an address where the high and low bytes are the same.