A modern-day ZX Spectrum OS rewritten from scratch in ARM assembly (aarch64) to run natively on Raspberry Pi 400. Based on the Spectrum 128K.
Originally targetted at the Raspberry Pi 3 Model B, with the introduction of the Raspberry Pi 400, the author decided to adapt the project to target the newer model. The USB hardware is easier to work with, and requires support for only a single keyboard.
For now, the code continues to work on a Raspberry Pi 3 Model B, however, the
keyboard interpretation routines will not work on this model when they are
written. The unit tests are designed to run under QEMU in the CI, which only
supports the Raspberry Pi 3 Model B (qemu-system-aarch64 -M raspi3b
). If at
some point QEMU starts supporting the Raspberry Pi 400 or Raspberry Pi 4 Model
B, the CI may be adapted to use it, and support removed for the Raspberry Pi 3
Model B.
In addition to the Raspberry Pi 400, it is likely (although not tested by the author) due to hardware similarity, that the ZX Spectrum +4 will also run on the Raspberry Pi 4 Model B, together with the standard Raspberry Pi USB Keyboard.
If you are not familiar with the ZX Spectrum computer from the 1980's, there are some excellent emulators around, such as Retro Virtual Machine, and even some very good online emulators that you can run directly in your browser.
For more information about the history and development of this project, see https://github.com/spectrum4/notes.
Please note this project is very much in its infancy.
Only a handful of routines have been ported/implemented so far.
The Spectrum +4 is intentionally incompatible with the original Spectrum. If you wish to run original ZX Spectrum software, there are some excellent emulators available such as Retro Virtual Machine, or for the Raspberry Pi see ZXBaremulator, as well as new compatible hardware.
The idea behind this project is to imagine what Sinclair/Amstrad might have developed for their next Spectrum, if Raspberry Pi 400 hardware had been available at the time, rather than to reproduce what they already had.
It is also an excuse for me to learn bare metal programming and aarch64 assembly. :-)
With that in mind, the following sections outline the differences between Spectrum +4 and the original ZX Spectrum 128K.
In order to retain the spirit of the original Spectrum computers, the video display preserves many of the original Spectrum principles:
- The original colour palette
- The original character cell restrictions
- Each character cell is limited to a single 3 bit paper and 3 bit ink colour
- Each character cell may be
BRIGHT
or not - Each character cell may
FLASH
or not
- A solid border colour around the edges of the display
However, the screen geometry has changed a little:
- Each character cell is now 16x16 pixels, rather than 8x8 pixels
- The screen now is 108 character cells wide instead of 32
- The screen now is 60 character cells high instead of 24
The border thickness is equivalent to the original Spectrum 128K in terms of equivalent character cell counts:
- Left border: 6 characters wide
- Right border: 6 characters wide
- Top border: 8 characters high
- Bottom border: 7 characters high
This matches the original screen geometry (which had 8 pixels per character):
In order to enforce the video restrictions, updates to the display file and
attributes file explicitly call the poke_address
routine, which syncs the
VideoCore IV GPU firmware framebuffer with the display file and attributes
file. I believe it should be possible to trap memory writes directly to the
display file and attributes file, and call poke_address
from the exception
handler. Some information about trapping memory writes is
here. This particular
guide is focussed on EL2 handling of EL1 memory accesses, but the same
principles should apply with EL1 handling of EL0 memory accesses. The MMU
is enabled, and so are interrupts. At the moment the interrupt routine doesn't
do anything. It is fired by a timer.
The display file and attributes file differ from the Spectrum 128K as follows:
- Each vertical screen third is now 20 character cells high rather than 8
- Each cell now has 16 pixel rows instead of 8
- A single pixel row of a character cell now requires 2 bytes instead of 1
The mechanics of converting memory addresses to (x, y)
coordinates is no
longer a matter of simple bit manipulation. This is a consequence of the
dimensions no longer being powers of 2. However, sequencially updating bytes in
the display and attributes file simulates the original screen loading
mechanics, which is nice to have preserved.
The Raspberry Pi 400 has 4GB RAM, which is considerably more than the Spectrum 128K. Using the 64 bit instruction set means that most of the memory paging routines in the original Spectrum can be mostly ignored and don't require translation. It also means that there is much more space available for BASIC programs, machine code routines, and RAM disk storage.
- Spectrum +4 is loaded at physical ARM address 0x00000000
- The RAM Disk is initially set to 256MB
- The HEAP is initially set to 256MB
- Spectrum +4 runs at EL1
- MMU is enabled
- Kernel virtual addresses map 1:1 with physical addresses, but with upper 16 bits set
- EL3 data cache is enabled
- EL3 instruction cache is enabled
- Timer interrupts configured and enabled, but keyboard routines not yet written
- All Spectrum +4 routines are written in aarch64 assembly (GNU assembler syntax).
- The Spectrum +4 source code is under the
/src/spectrum4
directory. - The build and test directives live in the various
Tup*
files scattered throughout the repository (tup build system is used - see Building) - As much as possible, the naming of system variables and routines, and the
ordering of routines, match the disassembly in the
src/spectrum128k/roms/romX.s
files. - Each ported routine contains a comment giving the label of the associated
Spectrum 128K routine that it was ported from. The label is an
L
followed by the 16 bit hexadecimal address of the original Spectrum 128K routine.
To build/test everything, simply run ./tup-under-docker.sh
. This requires
that docker
is installed on your system. It simply
calls tup
inside a docker container that
contains all required build/test dependencies. Run ./tup-under-docker.sh -h
to see additional options.
If you prefer to use a native toolchain, or cannot run docker containers on your host then see Building Without Docker.
After building using one of the methods above, the following distribution files/directories will be updated:
- Directory
src/spectrum4/dist
contains a debug and release version of Spectrum +4. The debug version is the same as the release version, but additionally logs debug messages to Mini UART, and performs a short demo on start up. - Directory
src/spectrum128k/tests
contains Spectrum 128K casette tape images (.tzx files) and casette tape audio samples (.wav files) for running the Spectrum 128K unit tests under a Spectrum 128K emulator (such as FUSE or Retro Virtual Machine) or on a real Spectrum 128K machine. - Directory
src/spectrum4/targets
contains Raspberry Pi 400 kernel images for running individual test suites, e.g. under QEMU, or on a real Raspberry Pi.
In order to run the debug and/or release builds of Spectrum +4 on an actual Raspbery Pi, either copy the contents of the given distribution directory to a formatted SD card, or serve the directory contents over TFTP to a suitably configured Raspberry Pi that has been configured to network boot. The github.com/spectrum4/notes project has some information about that type of setup, if you are interested.
Alternatively, to run Spectrum +4 under QEMU instead of a real Raspberry Pi, run something like:
$ qemu-system-aarch64 -full-screen -M raspi3b -kernel \
src/spectrum4/targets/debug.elf -serial null -serial stdio
At the time of writing, qemu-system-aarch64
does not have Rasperry Pi 4 Model
B / Raspberry Pi 400 target.
Note, you will likely need QEMU version 5.2.0 or later. Also note that the .elf
file is passed to the -kernel
option, rather than the .img
file, in order that
QEMU loads the kernel at address 0x0 rather than 0x80000, which seems not to be
possible when passing the .img
file directly.