z88dk, Spectrum mode 2 interrupts in C

Show us what you're working on, (preferably with screenshots).
Post Reply
dfzx
Manic Miner
Posts: 673
Joined: Mon Nov 13, 2017 6:55 pm
Location: New Forest, UK
Contact:

z88dk, Spectrum mode 2 interrupts in C

Post by dfzx »

You might (or more likely might not) remember that a few weeks back I posted a reference to this piece of nonsense:

Image

This is an animation running from an interrupt mode 2 routine implemented in C using z88dk.

It wasn't quite ready for prime time though, because it needed a modification to z88dk's interrupt handling in order to ensure the IM2 routine is left running when returning to BASIC. That modification is now in the main z88dk build, so this sort of trickery is now possible with z88dk's nightly build.

I'm aware that z88dk has offered C-on-the-Spectrum support for many years and no one has ever asked for this. It's not exactly something the world has been waiting for. :) I did it more as a learning exercise than anything else. On the other hand, it does open another door for the imaginative. Anyone writing a BASIC program, who needs IM2 support for some reason, but who doesn't want to resort to assembly language, now has another option.

It makes this sort of thing much easier:

Image

and your BASIC can interact with the IM2 routine via simple pokes, like this one does:

Image

Source code for these is quoted below.

Code: Select all

/*
 * Build with the newer zsdcc compiler and libraries with:
 *
 * zcc +zx -vn -clib=sdcc_iy -startup=31 atts_ticker.c -o atts_ticker -create-app
 *
 */

/* Ensure IM2 is left at exit */
#pragma output CRT_INTERRUPT_MODE_EXIT = 2

#include <z80.h>
#include <string.h>
#include <im2.h>
#include <arch/zx.h>


static unsigned char  ticker_string[] = "Hello, world! ";
static unsigned char* current_char_ptr;

/* Address in ROM of the character being scrolled into view */
static unsigned char* rom_address;

/* Bit, left to right, of the character to scroll into view next. Goes 128, 64, 32...1 */
static unsigned char  bit;

/*
 * Off-screen buffer to put the display into. This is blitted into the screen, replacing
 * whatever the user's program happens to have put there. A "merge" would be friendlier. :)
 */
static unsigned char  off_screen_buffer[32*8];

IM2_DEFINE_ISR_WITH_BASIC(isr)
{
  unsigned char* buffer_address;
  unsigned char  i;

  /*
   * Scroll off-screen display buffer data leftwards one byte. This is just a memory move downwards by one. 
   */
  memcpy((unsigned char*)off_screen_buffer, (unsigned char*)off_screen_buffer+1, sizeof(off_screen_buffer)-1);

  /*
   * For each of the 8 lines (top to bottom) of the character we're displaying, pick out
   * the current bit (left to right). If it's a 1, set the rightmost attribute cell to
   * colour, otherwise set the attribute cell white. This is done in the off-screen buffer.
   */
  buffer_address = (unsigned char*)&off_screen_buffer+0x1f;
  for( i=0; i<8; i++ )
  {
    unsigned char attribute_value;

    attribute_value = ( *rom_address & bit ) ? PAPER_MAGENTA : PAPER_WHITE;

    *buffer_address = attribute_value;
    buffer_address += 0x20;

    rom_address++;
  }

  /*
   * If that was the rightmost bit of the character, that character's done with. Move to the
   * next character in the display string and start again at its left side (bit 128).
   * Otherwise keep with the same character and get ready for the next bit.
   */
  if( bit == 1 )
  {
    current_char_ptr++;
    if( *current_char_ptr == '\0' )
      current_char_ptr = ticker_string;

    rom_address = ((*current_char_ptr-0x20)*8)+(unsigned char*)0x3D00;

    bit = 128;
  }
  else
  {
    bit = bit/2;

    /* Still on the same character, so move back to the start of its data in ROM */
    rom_address -= 8;
  }

  /* Copy the off-screen buffer into the display */
  memcpy( (unsigned char*)0x5800, off_screen_buffer, sizeof(off_screen_buffer) );
}


int main()
{
  /*
   * Initialise the ticker and its buffer
   */
  memset( off_screen_buffer, PAPER_WHITE+INK_WHITE, sizeof(off_screen_buffer) );

  current_char_ptr = ticker_string;
  rom_address      = ((*current_char_ptr-0x20)*8)+(unsigned char*)0x3D00;
  bit              = 128;

  /* Set up the interrupt vector table */
  im2_init( (void*)0xd300 );

  memset( (void*)0xd300, 0xd4, 257 );
  z80_bpoke( 0xd4d4, 195 );
  z80_wpoke( 0xd4d5, (unsigned int)isr );

  return 0;
}

Code: Select all

/*
 * Build with the newer zsdcc compiler and libraries with:
 *
 * zcc +zx -vn -clib=sdcc_iy -startup=31 countdown.c -o countdown -create-app
 *
 */

/* Ensure IM2 is left at exit */
#pragma output CRT_INTERRUPT_MODE_EXIT = 2

#include <z80.h>
#include <string.h>
#include <im2.h>
#include <arch/zx.h>

/* Control byte, anywhere in RAM would do */
#define CONTROL_BYTE ((unsigned char*)54015)

static unsigned char level = 192;

static int           interrupt_counter = 0;

IM2_DEFINE_ISR_WITH_BASIC(isr)
{
  unsigned char* screen_addr;
  unsigned char  i;

  /* Wait for the BASIC program to POKE our start trigger */
  if( *CONTROL_BYTE == 0 )
    return;

  /* Slow it down a bit :) */
  if( ++interrupt_counter != 3 )
    return;
  
  /* Countdown complete, reset ready for next time */
  if( --level == 1 ) {
    *CONTROL_BYTE = 0;
    level = 192;
    return;
  }

  /* This redraws the scale every interrupt, could do better :) */
  screen_addr = (unsigned char*)0x4000+0x1F;
  for( i=0; i<192; i++ )
  {
    if( i < (192-level) )
      *screen_addr = 0xC3;
    else
      *screen_addr = 0xFF;

    screen_addr = zx_saddrpdown( screen_addr );
  }

  interrupt_counter = 0;
}


int main()
{
  im2_init( (void*)0xd300 );

  *CONTROL_BYTE = 0;

  memset( (void*)0xd300, 0xd4, 257 );
  z80_bpoke( 0xd4d4, 195 );
  z80_wpoke( 0xd4d5, (unsigned int)isr );

  return 0;
}
Derek Fountain, author of the ZX Spectrum C Programmer's Getting Started Guide and various open source games, hardware and other projects, including an IF1 and ZX Microdrive emulator.
Post Reply