Greyscale and im2

by Ciaran McCreesh
Created: 28th November 1999
Last Modified: 28th November 1999

This page explains the alternative interrupt mode, im2, and gives an example Greyscale routine.

Interrupt Modes

There are two modes of interrupt - im1 and im2. There is also im0, but it crashes the calculator (it calls a random memory location...) and should be avoided. By default the calculator uses im1, and if you exit your program with any other mode set the calculator will freeze.

im2 is the fastest mode to use, but it is also most work. Full details of how it functions are available at 86central, I might also explain it later on. Many advanced programmers refuse to listen to people who use im1 because it is so slow - when using im1 the operating system does a lot of useless stuff that takes a lot of time. However there is a way to avoid nearly all of this (the final result loses only 3 milliseconds each second and frees up a 16K area of memory for general usage). My routine below uses im1 and it is almost as flickerless as the 'standard' im2 routine.

im1 Specifics

To write a routine it is necessary to know the details of what happens when an interrupt occurs. Before I start I'll just mention shadow registers. When an interrupt occurs the processor is probably in the middle of something else. If the interrupt modifies any registers there is no way of knowing what will happen when control returns to the program. The easiest way of saving the registers would be the stack, and on most processors this is what is done. But there is a faster way of doing it on the z80. The af, bc, de and hl registers all have 'shadows'. By swapping the values of these registers with the values in the shadow registers we can save a lot of time (and also memory - these instructions take up less space). Once the interrupt is finished we swap the value back again, returning to the program with the registers preserved.

When an interrupt occurs the CPU checks to see if interrupts are enabled. If they are it performs a restart (a restart is like a call, but it can only call a few memory locations) to $38. The code here does the following:

As I mentioned in chapter 3, a call pushes the current Program Counter to the stack, so when it returns it just has to pop the Program Counter from the stack (the PC has already been incremented). As our routine is called before the default interrupt code is carried out we can pop the top value of the stack and return from the interrupt in our own code (using the reti instruction). This saves us having to wait for the default interrupt code to be carried out.

The Routine

Now that we know what an interrupt does we can write our own code. The routine below was written by me. It is not designed to be the most efficient routine possible, but rather as a good learning tool.

The Interrupt Code

Listed below is the code that we want to execute every time the interrupt is called. If you don't understand any of this, in particular the first line, send me an email and I'll try and explain it properly.

GreyscaleRoutineStart:
  pop hl                          ;remove the calling routine from the stack - 
                                  ;we'll exit the routine in our own code
                                  ;without going through the default OS code

  ld bc,$3600                     ;b contains value to toggle link port with,
                                  ;c contains port to send value out of
  ld hl,GreyMem                   ;hl points to a memory location containing
                                  ;the interrupt counter

  dec (hl)                        ;decrease the interrupt counter... 
  jr z,SetCount3                  ;...and if it's zero goto SetCount3...
  inc hl                          ;...otherwise make hl point to current
                                  ;value of port 0 (port 0 is write only)
  ld a,(hl)                       ;load a with value of port 0
  xor b                           ;toggle it with b...
  out (c),a                       ;...send it out of port 0...
  ld (hl),a                       ;...and save it to memory

ExitInt:                          ;code to exit the interrupt

  in a,(3)                        ;The following was copied (nearly) from
  rra                             ;the ROM - it seems to handle the ON
  ld a,c                          ;key. If it is left out the calculator
  adc a,9                         ;may freeze, especially if the ON key
  out (3),a                       ;is pressed.
  ld a,$0b
  out (3),a

  exx                             ;restore normal registers
  ex af,af'
  ei                              ;make sure interrupts are enabled
  reti                            ;return from interrupt

SetCount3:
  ld (hl),3                       ;set interrupt counter to 3
  jr ExitInt                      ;and return from the interrupt

The above routine uses two bytes of initialised memory. They should be included in the main program somewhere:

GreyMem:
.db $01, $3c
The Loader Code

We need to copy the above code into the memory starting at $d2fe (the equate for this is _alt_interrupt_exec). We also need to set the checksum. Use the code below to calculate the actual checksum - it's the smallest version that isn't completely ridiculous. The formula for calculating the check digit is tedious - mail me if you'd like to know it. In case you were wondering, the ldir instruction is used to copy blocks of memory - the entry in the instruction reference is about here.

LoadGreyscale:
  di                              ;disable interrupts in case they are
                                  ;called whilst we install our code

  ld hl,GreyscaleRoutineStart     ;copy user routine to the place
  ld de,_alt_interrupt_exec       ;it will be executed from
  ld bc,200
  ldir

  ld de,$28                       ;set up check digit
  ld a,(_alt_interrupt_exec)
  ld hl,_alt_int_chksum + $28
  add a,(hl)
  add hl,de
  add a,(hl)
  add hl,de
  add a,(hl)
  add hl,de
  add a,(hl)
  add hl,de
  add a,(hl)
  ld (_alt_int_chksum),a

  set 2,(iy + $23)                ;enable user routine
  ei                              ;enable interrupts

  ret                             ;return to main program
Closing Greyscale

Before returning to the OS we need to revert to the normal screen mode. We also need to tell the OS that the graph is 'dirty' (we've been using the graph memory).

CloseGrey:
  res 2,(iy + $23)                ;disable user routine
  ld a,$3c                        ;restore screen to normal memory location
  out (0),a
  set graphdraw,(iy + graphflags) ;graph memory is 'dirty'
  ret                             ;return to main program
Clearing the Screen

When using greyscale call _clrScrn doesn't work - it only erases the memory starting at $fc00. The easiest way to clear both sets of memory is as follows:

ClearScreen:
  ld hl,$fc00                     ;clear the memory at $fc00...
  ld (hl),l
  ld de,$fc01
  ld bc,1023
  ldir
  ld hl,$fc00                     ;...and the memory at $ca00
  ld de,$ca00
  ld bc,1023
  ldir
  ret                             ;return to main program

Greyscale Graphics

There are a number of different greyscale sprite routines. I think there's a greyscale version of ZMR somewhere, and there's definitely a version of ASCR (although it's half the speed of ZMR, so not an ideal option). The basic idea of the routines is the same, although the sprite format is slightly different. A typical sprite is shown below, along with the data to draw it (in this case the first 8 bytes are for the darker video page and the next 8 for the lighter video page).

Greyscale Sprite

MySprite:
.db %11110000
.db %00101000
.db %01111000
.db %00111110
.db %00111110
.db %01111000
.db %00101000
.db %11110000

.db %00001100
.db %00101000
.db %01110110
.db %00111001
.db %00111001
.db %01110110
.db %00101000
.db %00001100