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.