Emulating the ZX Spectrum's Display

The Spectrum was a popular 8-bit home computer from the early 1980s. Its graphics used a 256x192 pixel bitmap, each pixel is one bit. The colors were generated from a lower-resolution attribute map: each 8x8 tile of pixels had a foreground color and a background color. This was encoded as two three-bit color fields in each attribute byte. Hence the total storage for the display is:

Using the Gameduino's coprocessor, it is possible to convert from Spectrum video data to Gameduino background characters on-the-fly. As the raster moves down the screen the coprocessor converts each line of graphics.

In this demo sketch, the Arduino is loading various games' raw screen dumps into Gameduino memory. All of the graphics conversion work is being done by the Gameduino coprocessor.

#include <SPI.h>
#include <GD.h>

#include "spectrum.h"
#include "spectrum_data.h"

void setup()
{
  GD.begin();
  GD.microcode(spectrum_code, sizeof(spectrum_code));
  GD.uncompress(0x7000, spectrum_tables);

  // fill screen with TRANSPARENT
  GD.fill(RAM_PIC, 0xff, 64 * 64);
  GD.wr16(RAM_PAL + 8 * 255, TRANSPARENT);
  GD.wr16(BG_COLOR, RGB(128, 0, 0));  // dark red border

  // paint the 256x192 window as alternating lines of
  // chars 0-31 and 32-63
  for (byte y = 0; y < 24; y += 2) {
    for (byte x = 0; x < 32; x++) {
      GD.wr(RAM_PIC + 64 * (y + 6) + (9 + x), x);
      GD.wr(RAM_PIC + 64 * (y + 7) + (9 + x), 32 + x);
    }
  }

}

#define PAUSE delay(3000)

void loop()
{
  GD.uncompress(0x4000, screen_mm1);  PAUSE;
  GD.uncompress(0x4000, screen_mm2);  PAUSE;
  GD.uncompress(0x4000, screen_aa0);  PAUSE;
  GD.uncompress(0x4000, screen_aa1);  PAUSE;
  GD.uncompress(0x4000, screen_jp0);  PAUSE;
  GD.uncompress(0x4000, screen_jp1);  PAUSE;
  GD.uncompress(0x4000, screen_kl0);  PAUSE;
  GD.uncompress(0x4000, screen_kl1);  PAUSE;
}

The hard work is all done in the coprocessor program. In the main loop, it waits for every 8th video line, then starts converting the next line of characters. It unpacks the pixel data and the attribute data into a line of 32 characters. Because it can reuse the characters on alternating lines, the total usage is only 64 characters.

The 6K Spectrum video data is loaded into Gameduino RAM starting at 0x4000 - the area normally used for sprites. In addition a lookup table at 0x7000 speeds up the color conversion.

start-microcode spectrum

\ Interface:
\ 4000-57FF Spectrum bitmap
\ 5800-5AFF Spectrum attributes
\ 7000 attribute lookup: 256 bytes.  64 colors of (paper, ink)
\ 7100 pixel stretch, 16 bytes.

: 1+    d# 1 + ;
: 0=    d# 0 = ;
: 4*    d# 4 * ;
: 64mod h# 3f and ;

: copy1 ( src dst -- src' dst' ) \ copy one byte
    over c@
    over c!
    1+
;fallthru
: n1+  ( a b -- a+1 b )
    swap 1+ swap ;

\ copy attrs for line y
\ dst is RAM_PAL or RAM_PAL+256

: attrcopy ( y -- )
    dup 4* h# 5800 + swap            ( src y )
    h# 8 and d# 32 * RAM_PAL +       ( src dst ) 
    begin
        over c@ 64mod 4* h# 7000 + swap \ fetch and lookup attribute
        copy1 copy1 d# 4 + copy1 copy1 nip
        n1+
        dup h# ff and 0=
    until
    drop drop
;

: stretch! ( dst a -- dst' ) \ expand 4 bit graphic a, write to dst
    h# f and
    h# 7100 + c@
    over c! 1+
    ;

: byte ( src dst -- src' dst' )
    over c@ swap    ( src a dst )
                    
    over d# 4 rshift stretch!
    swap stretch!  ( src dst' )
    swap h# 100 + swap      \ down 1 line in spectrum video memory
;

: byte4
    byte byte byte byte ;

: pixelcopy ( y -- y )
    dup 64mod 4* h# 4000 +
    over h# c0 and d# 32 * +    ( y src )
    begin
        dup
        dup 64mod d# 16 * RAM_CHR +
        byte4 byte4
        drop drop
        1+
        dup h# 1f and 0=
    until drop
;

\ Spectrum memory layout is a bit twisted
\ line 0      4000, 4001, 4002
\      1      4100, 4101
\             ...
\      8      4020
\             ...
\      56     40e0, ...             40ff
\             ...
\      63     47e0                  47ff
\      64     4800
\      65     4900
\             ...
\      191    57e0

\ at line  0 can start work on 4020
\ at line  8 can start work on 4040
\ at line 16 can start work on 4060
\         56                   4800
\
\ So in general, at line Y can start work on converting from:
\ 4000 + (((Y+8) & 38) * 4) + (((y+8) & c0) * 32)

: main
    d# 0
    begin
        begin dup d# 48 + YLINE c@ = until
        d# 8 + h# ff and
        dup attrcopy
        pixelcopy
    again
;

end-microcode