interrupts - z88dk/z88dk GitHub Wiki

INTERRUPTS (im2.h)

Header {z88dk}/include/im2.h
Source {z88dk}/libsrc/im2
Include #include <im2.h>
Linking
Compile
Comments none

The interrupts library provides functions supporting the im2 interrupt mode on the z80. Although specifically designed with im2 in mind, im0 and im1 are also supported with appropriate [C startup](C startup) code that contains JPs to either C ISRs or Generic ISRs as described below.

Technical Overview of Interrupts on the z80

The z80 supports both maskable (MI) and non-maskable (NMI) interrupts. A non-maskable interrupt is generated by pulsing the z80's falling-edge triggered NMI pin low. The cpu responds by jumping to address 0x66 to service the NMI interrupt after the current instruction has completed execution. A maskable interrupt is generated by pulsing the z80's active low level-triggered INT pin low. The INT pin is sampled at the rising edge of the last T-state of an instruction. The z80 responds to a maskable interrupt by first generating an interrupt acknowledge cycle on the bus and then acts according to one of three maskable interrupt modes it is in. These modes are known as IM0, IM1 and IM2.

IM0 exists for compatibility with the 8080 processor. The z80 retrieves a single-byte instruction (usually a RSTn) from the interrupting device during the acknowledge cycle and executes that instruction after disabling maskable interrupts. This mode is rarely used in z80 systems.

IM1 is a commonly used interrupt mode. Once the interrupt acknowledge cycle is completed the z80 disables maskable interrupts, saves the program counter on the stack and executes a jump to address 0x38 to service the interrupt. In this mode there is no knowledge of which device caused the interrupt and the interrupt service routine must rely on polling or external hardware to determine which interrupting device(s) require attention.

IM2 is also a commonly used interrupt mode. It is a vectored interrupt mode and is the most powerful mode the z80 offers. The z80 retrieves a single-byte identifier from the interrupting device during the interrupt acknowledge cycle. By convention only this identifier is an even number. The I register is concatenated with this identifier to form a 16-bit address that is used to retrieve the address of the interrupt service routine to call. After the z80 disables maskable interrupts and saves the program counter on the stack this interrupts service routine address is fetched and jumped to. In effect, I256 points at a table of interrupt service routine addresses that is indexed by the identifier byte supplied by the interrupting peripherals. The convention of using only even identifiers ensures that table entries do not collide. Eg: A peripheral supplying identifier 0 has its interrupt service routine address stored in the table at address I256+0 for the least significant byte and I256+1 for the most significant byte. The next even identifier is 2 and has its interrupt service routine address stored in the table at address I256+2 for the least significant byte and I*256+3 for the most significant byte. Had the odd identifier 1 been allowed, its interrupt service routine address would be stored in the table on top of these other two. Nominally this even identifier convention means the z80 can deal with 128 unique interrupting sources and stores their associated interrupt service routine addresses in a 256 byte table pointed at by the I register. Common z80 peripheral ICs like the CTC (counter-timer), PIO (parallel IO), SIO (serial IO), etc, are designed for IM2 and have programmable peripheral ids. The internal registers storing these ids can store even numbers only, thus forcing these devices to adhere to the even-identifier convention. However the z80 will read a full 8-bit identifier that could be odd if the interrupting peripheral supplies an odd identifier. Indeed many small micros from the 1980s, when operated in IM2 mode, will receive odd identifiers.

NMI interrupt service routines must exit with the "RETN" instruction which will restore the previous masked state of the maskable interrupts and return to the point of interrupt.

Maskable interrupt service routines should exit with the "EI; RETI" combination. Although "EI; RET" is functionally equivalent, z80 peripherals operating in IM2 are designed to recognize the "RETI" instruction on the bus to indicate completion of their interrupt service routines. Even if not operating in the IM2 mode, using "RETI" will ensure that attached IM2 peripherals do not get confused.

C Functions as Interrupt Service Routines

This library offers two methods for creating interrupt service routines from C functions.

The first method is for creating a raw C interrupt service routine. The C function must be defined using one of two macro pairs as shown below:

:::c
M_BEGIN_ISR(myisr)
{
   // insert C code here
}
M_END_ISR

M_BEGIN_ISR_LIGHT(myotherisr)
{
   // insert C code here
}
M_END_ISR_LIGHT

main()
{
   ...
   im2_InstallISR(0, myisr);        // install myisr() on vector 0
   im2_InstallISR(28, myotherisr)   // install myotherisr() on vector 28
   ...
}

The examples above create two C functions with signatures "void myisr(void)" and "void myotherisr(void)". The macros attach all the preamble and postamble code necessary for a z80 interrupt service routine. The BEGIN block saves registers on the stack and the END block restores them and terminates the function with "EI; RETI". The LIGHT version only saves and restores the "af,bc,de,hl" registers. This makes the LIGHT version faster but you must be careful to ensure the enclosed C code does not use any unsaved registers otherwise the program will crash. If you are unsure, stick with the safe option.

With the functions defined, they can be installed directly to service any interrupt with the im2_InstallISR() call as shown.

The second method is to register a C function with a Generic Interrupt Service Routine. The Generic ISR is a special ISR provided by the library that allows a list of functions to be registerd with it. On interrupt the Generic ISR calls each function in its list one after the other. Because the Generic ISR takes care of details such as saving and restoring registers, any regular C (or assembler) function can be registered with it. Should any function in the Generic ISR's list return with the carry flag set, the succeeding functions in the list are not run. For this reason C functions registered with a Generic ISR should make use of the special z88dk keywords return_c() and return_nc() to specify the carry flag's state on exit. Any number of independent Generic ISRs can be created and installed on specific vectors with im2_InstallISR(). Further details can be found in the IM2 API description.

NMI

Response to a non-maskable interrupt is always a jump to a fixed ISR at address 0x66 that must terminate with a "RETN" instruction. There is no direct support for the NMI in z88dk; the best way to accommodate this mode is to include the NMI interrupt routine in the C startup code.

IM 0

Response to a maskable interrupt is to execute the instruction supplied by the interrupting peripheral. This instruction is typically a single-byte "RSTn" instruction which causes a subroutine call to one of eight fixed entry points in the first 256 bytes of memory. Any ISRs servicing these interrupts must terminate with "EI;RET" or "EI;RETI" (latter preferred). The best way to accommodate this mode is to include ISRs at the relevant fixed entry points in the C startup code. The generic ISR supplied in this library can be used to field these interrupts by placing a JP instruction to the relevant generic ISR in the startup at each fixed entry point.

IM 1

Response to a maskable interrupt is to jump to a fixed ISR at address 0x38 that must terminate in "EI;RET" or "EI;RETI" (latter preferred). The best way to accommodate this mode is to include an ISR at address 0x38 in the C startup code. The generic ISR supplied in this library can be used to field this interrupt by placing a JP instruction to the generic ISR at 0x38 in the startup.

IM 2

Response to a maskable interrupt is to jump to a specific ISR whose address is stored in a table of interrupt service routines (called the interrupt vector table) that is indexed by an identifier byte supplied by the interrupting peripheral. The interrupt service routine must be terminated by "EI;RETI".

The z80 allows the interrupt vector table to be located in any 256-byte aligned page in the z80's address space. This location must be specified during initialization with the im2_Init() function which sets the z80's I register appropriately. Other library functions use the I register to locate the table.

The size of this table depends on whether a peripheral identifier of 255 will be supplied by an interrupting peripheral. By convention only even identifiers from 0-254 will be supplied making the table exactly 256 bytes long. However it is not uncommon for systems not designed with IM2 in mind to have peripherals generate identifiers of 255 while in IM2 mode in which case the table needs to be 257 bytes long.

To set up im2 mode using this library the following sequence of calls are made:

  1. Disable interrupts. The z80 starts with interrupts disabled at power up.
  2. Call im2_Init(), specifying the location of the im2 vector table (which must begin on a 256-byte page), to place the z80 in im2 mode.
  3. Create the im2 vector table using eg, memset() or combinations of memcpy() and wpoke(). The library routine im2_EmptyISR() can be used as default entries on even vectors of the table to provide a do-nothing default response (the function simply reenables interrupts and returns).
  4. Optionally create generic interrupt service routines in RAM. This is a special interrupt routine supplied by the library that calls a list of hooks registered with it in sequence. These registered hooks can be C or asm subroutines.
  5. Install interrupt service routines on specific interrupt vectors with im2_InstallISR()
  6. Use the relevant functions to attach hooks to any generic isrs created.
  7. Reenable interrupts.

From this point on the z80 will be operating in IM2 mode.

The setup described here assumes the interrupt vector table is generated by software in RAM at power-up or program initialization. It is also possible to store a fixed vector table in ROM. In this case interrupt routine addresses should be pre-stored in the table. The generic ISRs can still be created in RAM during initialization and should be pointed at by pre-stored entries in the interrupt vector table in ROM.

IM2 API

Maskable interrupts must be disabled while any of these functions are called. Do so with some embedded assembler:

#asm
di
#endasm

When finished reenable interrupts with another assembler fragment:

#asm
ei
#endasm

1. im2_Init()

Places the z80 in im2 mode and points the z80's I register to the interrupt vector table address passed as parameter. Once interrupts are enabled, im2 mode will be active.

:::c
void im2_Init(void *tableaddr);
//   tableaddr = 16-bit address of the interrupt vector table, LSB ignored

2. im2_InstallISR()

Installs an interrupt service routine on a specific interrupt vector. The interrupt service routine must be a raw z80 isr -- ie it must save registers it uses on entry and restore them and reenable interrupts on exit. A C function can act as a raw z80 isr if assembler is embedded to take care of these details (see the BEGIN_ISR / END_ISR macros described at the top of this page). C functions can also service interrupts by being registered with a Generic ISR -- see im2_CreateGenericISR() and im2_CreateGenericISRLight().

Returns the ISR formerly registered on the vector.

:::c
void *im2_InstallISR(unsigned char vector, void *isr);
// vector = peripheral id supplied by the interrupting device (0-255, even by convention)
//    isr = void (*isr)(void)
//            interrupt service routine to be installed on the given vector
//            interrupts on the given vector will be serviced by this function

3. im2_EmptyISR()

An interrupt service routine that simply reenables interrupts and returns. A candidate as default isr while filling the im2 vector table during initialization.

:::c
void im2_EmptyISR(void);

4. im2_CreateGenericISR()

An interrupt service routine designed to accept a list of functions to call on interrupt. Saves all registers, calls its list of functions in sequence and finally restores registers and reenables interrupts before exit. The called functions do not have to worry about saving registers or reenabling interrupts. If any individual function returns with the carry flag set, succeeding functions in the list are not run. C functions should make use of the special z88dk keywords return_c() and return_nc() to exit with known carry flag state.

A generic isr is created by making a copy of itself into RAM. During creation you must specify the destination address and the maximum number of functions that will be registered with the isr. The isr will occupy (15+2*numfunctions) bytes. Once the generic isr is created, you can install it on a specific vector with the library routine im2_InstallISR().

The library routines im2_RegHookFirst(), im2_RegHookLast() and im2_RemoveHook() are used to manage functions registered with the isr.

Returns the next address following the generic isr created in RAM.

:::c
void *im2_CreateGenericISR(unsigned char numhooks, void *addr);
// numhooks = maximum number of hooks that will be registered with this function; must be at least one
//     addr = destination address to which the generic interrupt service routine will be written; occupies (15+2*numhooks) bytes

5. im2_CreateGenericISRLight()

As im2_CreateGenericISR() but only saves the AF,BC,DE,HL registers.

:::c
void *im2_CreateGenericISRLight(unsigned char numhooks, void *addr);
// numhooks = maximum number of hooks that will be registered with this function; must be at least one
//     addr = destination address to which the light generic interrupt service routine will be written; occupies (15+2*numhooks) bytes

6. im2_RegHookFirst()

Adds a function to the beginning of a generic isr's hook list. This function will be the first function run on interrupt.

:::c
void im2_RegHookFirst(unsigned char vector, void *hook);
// vector = peripheral id on which a generic interrupt service routine is installed (0-255, even by convention)
//   hook = void (*hook)(void)
//           function to be called by the generic isr when an interrupt occurs
//           all hooks registered on a particular vector are called in sequence; if any return with the carry flag set the following hooks will not be run
//           if a hook is a C function, use the special z88dk keywords return_c() and return_nc() to return from the function with known carry flag state

7. im2_RegHookLast()

Adds a function to the end of a generic isr's hook list. This function will be the last function run on interrupt.

:::c
void im2_RegHookLast(unsigned char vector, void *hook);
// vector = peripheral id on which a generic interrupt service routine is installed (0-255, even by convention)
//   hook = void (*hook)(void)
//           function to be called by the generic isr when an interrupt occurs
//           all hooks registered on a particular vector are called in sequence; if any return with the carry flag set the following hooks will not be run
//           if a hook is a C function, use the special z88dk keywords return_c() and return_nc() to return from the function with known carry flag state

8. im2_RemoveHook()

Removes the function from the generic isr's hook list. The function will no longer be run on interrupt. Returns 0 and carry flag reset if the function is not found in the generic isr's hook list.

:::c
int im2_RemoveHook(unsigned char vector, void *hook);
// vector = peripheral id on which a generic interrupt service routine is installed (0-255, even by convention)
//   hook = void (*hook)(void)
//           function to be removed from the generic isr's hook list

Code Example 1

In this scenario, common to many micros in the 1980s, the system was not designed to work in im2 mode. However it is still desired to use im2 so that user functions can be registered to handle interrupts. None of the system peripherals were designed with im2 in mind so none of the interrupting peripherals supply an identifier byte during the im2 interrupt acknowledge cycle. Instead the z80 grabs as identifier an essentially random byte found floating on the bus.

Since the identifier byte read by the z80 is unknown and may be even or odd, the solution is to generate a 257-byte table (to accommodate all possible identifiers 0-255 even and odd) all containing a single byte. Since the table contains just one byte, $XX, the interrupt service routine address fetched by the z80 from the table will always be $XXXX. We arrange to store the user interrupt service routine at address $XXXX which will service all interrupts.

:::c
#include `<stdlib.h>`                 // for bpoke(), wpoke()
#include `<string.h>`                 // for memset()
#include `<im2.h>`

...

int numtimers = 10;
int timer[10];

M_BEGIN_ISR(isr)                    // this isr will count down several timers to 0
{
   int i;

   for (i=0; i!=numtimers; i++)
      if (timer[i]) timer[i]--;
}
M_END_ISR

...

main()
{

   ...

   // initialize im2 mode

   #asm
   di
   #endasm

   im2_Init(0xd300);                // place z80 in im2 mode with interrupt vector table located at 0xd300
   memset(0xd300, 0xd4, 257);       // initialize 257-byte im2 vector table with all 0xd4 bytes
   bpoke(0xd4d4, 195);              // POKE jump instruction at address 0xd4d4 (interrupt service routine entry)
   wpoke(0xd4d5, isr);              // POKE isr address following the jump instruction

   #asm
   ei
   #endasm

   ...

}

Code Example 2

In this scenario we have a system implementing im2 mode properly, however some im2-unaware devices attached to the system cause interrupts but do not provided an identifier byte. The solution is to put pull-up resistors on all data lines so that when the z80 looks for an identifier byte for a device that does not provide one, 255 will be read. The situation then is to assign even identifiers less than (and not equal to!) 254 to all im2-aware interrupting peripherals and all im2-unaware devices will have identifier 255. Since there are multiple im2-unaware devices in the system, a Generic ISR is installed for vector 255. The interrupt service routines for the im2-unaware devices are registered with the Generic ISR which runs each one after the other.

:::c
#include `<im2.h>`
#include `<string.h>`

// the following devices are im2-aware and
// these are their interrupt service routines

M_BEGIN_ISR(im2_dev0_isr)
{
   // C code to take care of device 0
}
M_END_ISR

M_BEGIN_ISR(im2_dev1_isr)
{
   // C code to take care of device 1
}
M_END_ISR

extern void im2_dev2_isr(void);
#asm
_im2_dev2_isr:                     // an assembly language isr
   push af
   push bc
   push de
   push hl

   ...

   pop hl
   pop de
   pop bc
   pop af
   ei
   reti
#endasm

// the following devices are not im2-aware and
// these are their interrupt service routines

void im1_dev0_isr(void)
{
   if (dev0 needs attention) {     // we don't know which of the devices caused interrupt on vector 255
      // C code to take care of device 0
   }
   return_nc;                      // exit with carry flag reset so next ISR in hook list is run
}

void im1_dev1_isr(void)
{
   if (dev1 needs attention) {     // we don't know which of the devices caused interrupt on vector 255
      // C code to take care of device 1
   }
   return_nc;                      // exit with carry flag reset so next ISR in hook list is run
}

...

main()
{

   ...

   // Initialize im2

   #asm
   di
   #endasm

   im2_Init(0xd300);                           // place z80 in im2 mode with interrupt vector table located at 0xd300
   wpoke(0xd300, im2_EmptyISR);                // place the default im2_EmptyISR()on vector 0
   memcpy(0xd302, 0xd300, 255);                // initialize the entire 257-byte im2 table with im2_EmptyISR on all even vectors
   im2_CreateGenericISR(2, 0xd401);            // create a generic isr at address 0xd401 handling a maximum of two hooks
   im2_InstallISR(255, 0xd401);                // install the generic isr on vector 255
   im2_RegHookFirst(255, im1_dev0_isr);        // register im2-unaware isrs with the generic isr now on vector 255
   im2_RegHookFirst(255, im1_dev1_isr);
   im2_InstallISR(4, im2_dev0_isr);            // register im2-aware isrs on their vectors
   im2_InstallISR(202, im2_dev1_isr);
   im2_InstallISR(138, im2_dev2_isr);

   // initialize all devices, including storing identifier bytes in im2-aware devices and turning on their interrupt generation

   #asm
   ei
   #endasm

   ...

}

Questions & Answers

Post common questions and answers here. Report any bugs or ask a question not listed here on the z88dk mailing list.

⚠️ **GitHub.com Fallback** ⚠️