Teensy Z80 – Part 2 – Mode 2 Interrupts, Timer

Interrupts. Lovely interrupts. The Z80 has a maskable interrupt, and a non maskable interrupt. The maskable ones having the feature that they can be disabled and enabled from within code. For me, I wanted to implement maskable Mode 2 Interrupts.

Mode 2 interrupts are very powerful. They allow an external device to make the Z80 jump to one of 128 possible locations, by putting the lower half of a 16-bit address on the data bus. this is combined with the contents of the I register to form a location in memory, which should contain the location of the exception handler routine.

The steps are as follows:

  1. The Z80 is put into interrupt mode 2.
  2. Set the I register to the hi-byte start of the Interrupt Vector Table
  3. Interrupts are enabled
  4. The z80 runs program code
  5. A device puts the INT pin low (it’s active low, so this is the active state)
  6. The Z80 will acknowledge the interrupt request at a time in the future, making pins M1 and IOREQ active.
  7. The device should place it’s interrupt vector on the data bus.
  8. The CPU will take the data bus and combine it with the I register to form an address interrupt_handler_start = ((I<<8U)|DATA_BUS)
  9. The current PC is placed on the stack and execution resumes at interrupt_handler_start
  10. The handler should disable interrupts, perform an action, enable interrupts and perform a reti instruction to return to user code. Registers should be manually preserved in the interrupt handler.

I want TeensyZ80 to have some timer interrupts so in the first instance I have an interrupt which fires every second.

timing_intLooking at the timing diagram for an interrupt, we see INT goes active and is only sampled at specific times. It is safe to then serve the interrupt vector when IOREQ is active in an M1 cycle. We then place the interrupt vector on the data bus, and continue. It’s important to note that we must add a check to the existing I/O servicing code on the Teensy to ensure we do not enter that if M1 is active.

In the Teensy sketch, we have some new variables.

elapsedMillis timer_seconds;
byte timer_seconds_intvector = 2;
byte currentInterruptVector = 0; // The vector that is to be signalled
int currentInterrupt = 0; // We are in an interrupt state

In loop() we have more code. Before we set the clock high, signal the interrupt if we need to.

if ((timer_seconds > 1000) && !currentInterrupt)
{
  timer_seconds = 0;
  digitalWrite(INT, LOW);
  currentInterruptVector = timer_seconds_intvector;
  currentInterrupt = 1;
}

After the clock goes high, we should check for the Interrupt Acknowledge. you can see here that I keep INT active untile I see an ACK, which doesn’t seem to cause issues at present.

if (currentInterrupt>0) {
  if (IOREQ_val && M1_val) // interrupt ack
  {
    digitalWrite(INT, HIGH);
    currentInterrupt = 0;
    dataBus = currentInterruptVector;
    writeDataBus();
  }
}

...snip... 

// the IO code shouldn't be executed in an M1 state
if (IOREQ_val && (ioDebounce == 0) && !M1_val)
{
...snip...

And then on the Z80, we need to write several bits – the init for mode 2 interrupts and the I register, the interrupt handler, and the pointer to that handler in a specified table location, aligned to 256 bytes.

The Z80 boot code

  .org 0000h
start:
  di                        ;disable interrupts
  ld sp, 3fffh
  im 2                      ;enable mode 2 interrupts
  ld a, 01h
  ld I, a                   ;set the high byte of the interrupt table
  ld de, str_console_prompt
  call print_string
  ei                        ;enable interrupts
iloop:
  halt
  jr iloop

We then have the interrupt handler:

ihdlr_second_timer:
  di                ; disable interrupts
  push af           ; preserve registers
  ld a, 02eh        ; print period to console
  out ($03), a
  pop af
  ei                ; enable interrupts
  reti              ; return from interrupt

Port 3 is our console putChar port. So this simply prints a ‘.’ to the console (ASCII 0x2E)

The interrupt vector table:

; interrupt vector table
  .ORG 0100h
int_vector_table:
  dw ihdlr_unknown       ; vector 0 - will print 'unknown vector' to console if fired
  dw ihdlr_second_timer  ; vector 2 - fired every second
  dw ihdlr_unknown
  dw ihdlr_unknown
  dw ihdlr_unknown
...snip...

And this works well. Here is the Z80 running the test.

In addition to this, I also ran the TeensyZ80 in a debug mode with a slow clock so you could see the process of running the z80 code. This prints a log of each cycle, with a descriptive name and where appropriate some further information. A log line of ? prints the signal lines within square brackets with M1_val = F, RFSH_val = H, MEMREQ_val=M, and IOREQ_val=I. These cycles are for when the CPU is executing long instructions, etc.

This post comes to quite an abrupt end, but really once those fundamental bits of functionality are on the Teensy sketch the z80 code just falls into place. I did have some issues when the interrupt vector table was higher up in memory, but I’ll have to look at that issue again sometime. This timer works well, and Ive got another interrupt which fires when serial data is available for consumption by the z80. Setting that up is fairly simple, it’s a serial command where I tell the emulated serial device what interrupt vector to use.

INTVECTOR_SERIAL_DEVICE equ 8

.. skip forward to the serial device init ...
ld a, SERIAL_CMD_SET_INTVECTOR
out ($01), a                      ; put the serial device in SET_INTVECTOR mode
ld a, INTVECTOR_SERIAL_DEVICE
out ($01), a                      ; set the interrupt vector for the serial device to use

When the Teensy sees there is data available through serial, this interrupt will be fired, which is useful!

Again, I hope this was interesting! Let me know your thoughts via twitter @domipheus.

The next part in this series is available here.

Comments are closed.