Interrupts are a mechanism to make the CPU stop processing one task and temporarily switch to another. They are typically used for time-critical applications where immediate response to a changing condition is required, or to prevent the CPU from polling some type of status input which wastes valuable processing time.

The Z80 supports two types of interrupts, non-maskable and maskable. They are triggered by two pins: /NMI and /INT.

/NMI is an negative edge triggered input. When it goes from the high to low level only, the Z80 will start non-maskable interrupt processing. You will not get continuous interrupts by holding /NMI low. The Z80 has no way to disable this interrupt, hence it is non-maskable.

When an NMI occurs, the Z80 starts executing code at address $0066. This makes it mandatory to place an interrupt handler at that address, even if it's a jump instruction to a larger routine elsewhere in memory.

/INT is an active-low level sensitive input. When pulled low, the Z80 wil start interrupt processing, and repeatedly do this after each instruction executed until /INT goes high again. This type of interrupt can be controlled by the Z80, using the DI (disable interrupt) and EI (enable interrupt) instructions.

When interrupts are disabled via DI, the Z80 will respond to /INT as soon as interrupts are enabled again. If /INT goes high before they are enabled, then the interrupt is 'lost' and the Z80 never responds to it.

Typically the external hardware which triggered the interrupt will have some facility to pull /INT high again, either after a set period of time (auto-acknowledge) or through a dedicated memory address or control register that can be accessed by the interrupt handler, to indicate that the interrupt is being serviced.

The Z80 has three processing modes for maskable interrupts. The mode resets to 0 by default, and can be changed using the IM 0, IM 1, or IM 2 instructions.

Interrupt Mode 0

When an interrupt is requested, the Z80 reads a byte from the data bus and executes it. This can be a single-byte instruction or the start of a multi-byte instruction; for speed reasons commonly RST $nn is used to start processing an interrupt handler due to it's compact encoding.

If no external device is driving the bus, the Z80 will read a garbage value. In systems with pull-up or pull-down resistors on the bus, the value read will be $FF (RST $38) or $00 (NOP) respectively.

Interrupt Mode 1

When an interrupt is requested, the Z80 starts executing code at address $0038.

Interrupt Mode 2

When an interrupt is requested, the Z80 reads the address of the interrupt handler from a vector table that is located at the following address in memory:

(I register * 256) + Data bus value

.. and then jumps to that address. E.g. if register I is $50 and the data bus value is $2A, the two-byte address of the interrupt handler is read from offset $502A onwards.

Despite the official documentation, bit 0 of the low byte ($2A in the previous example) does not have to be zero.

As with interrupt mode 0, if no external device provides the low-byte then a garbage value is used. However this mode can still be used by populating the entire vector table with identical values; regardless of which low-byte is selected, the same address (such as $1111 or $AAAA) is selected. The I register only has to point to the start of the 257-byte vector table.

Interrupt handlers

An interrupt handler has to perform several tasks:

  1. Save any registers that will be used by pushing them to the stack or by exchanging the working register set with the alternate one (using EX and EXX).
  2. Perform whatever tasks are needed
  3. Restore the registers
  4. Enable interrupts for further ones to occur (EI). Not necessary for an NMI.
  5. Return from the interrupt handler using RETI (slow, indicates to other devices that the interrupt has been serviced) or RET (faster, no indication) An NMI handler has to use the RETN instruction.

A typical interrupt routine that follows the above steps might look like this:

; Maskable interrupt
.org $0038
 push af                   ; Save A so we can modify it later
 ld a, (interrupt_flag)    ; Increment an interrupt-occurance flag
 inc a
 ld (interrupt_flag), a
 pop af                    ; Restore A
 ei                        ; Enable subsequent interrupts
 reti                      ; Return from the interrupt handler
; Non-maskable interrupt
.org $0066
 exx                       ; Use the alternate versions of BC, DE, HL
 outi                      ; Store (HL) to I/O port (C)
 exx                       ; Switch back to the regular register set
 retn                      ; Return from the interrupt handler

There are other ways to write an interrupt handler, depending on what functions it needs to perform.

Return to top