RP2040 ZX Spectrum emulator
The ZX2040 is a port of Andre Weissflog ZX Spectrum emulator to the Raspberry Pico RP2040, packed with a simple UI for game selection and key mapping to make it usable without a keyboard.
This project is specifically designed for the Raspberry Pico and ST77xx based displays. Our reference device is the Pimoroni Tufty RP2040 display board, but actually the code can run into any Raspberry Pico equipped with an ST77x display and five buttons connected to five different pins. The buttons work as inputs for the four gaming directions (left, right, top, bottom) and the fire button. Please refer to the hardware section for more information.
Video demo here.
pico_set_binary_type(zx copy_to_ram)
in CMakeList.txt
. There are no problems accessing the flash to load games, because the code down-clocks the CPU when loading games, and then returns at a higher overclocking speeds immediately after.The fantastic emulator I used as a base for this project was not designed for very small devices. It was rather optimized for the elegance of the implementation (you have self-contained emulated chips that are put together with the set of returned pins states) and very accurate emulation. To make it run on the Pico, I had to modify the emulator in the following ways:
With this changes, when the Pico is overclocked at 400Mhz (default of this code, with cpu voltage set to 1.3V), the emulation speed matches a real ZX Spectrum 48K. If you want to go slower (simpler to play games, and certain Picos may not run well at 400Mhz) press the right button when powering up: this will select 300Mhz.
Please note that a few of this changes are somewhat breaks the emulation accuracy of the original emulator, but they are a needed compromise with performances on the RP2040 and good frame rate. A 20 FPS emulator that runs very smoothly is a nice thing, but breaking the Z80 precise clock may mess a bit with certain games and demos. Moreover, the way we plot the video memory instantaneously N times per second is different than what the ULA does: a game may try to "follow" the CRT beam (for example removing the old sprites once it is sure the beam is over a given part). Most games are resilient to these inconsistencies with the original hardware, but when it's an issue, we resort to game specific tuning of the emulator timing parameters (see the keymap file inside the games
directory).
The ZX Spectrum was the computer where I learned to code when I was a child. Before owning the Spectrum, I used to play with my father's computer, a TI 99/4A, yet the Spectrum was my first real computer, and the one where I wrote my first decent programs. So part of this is nostalgia.
On a more practical way, I wanted an emulator that was a good fit for the Pico and specifically conceived to run with cheap displays, able to easily be adapted to different displays resolutions, MIT licensed, very easy to hack on. So that people can build, enjoy and even sell if they want battery-powered small cheap Spectrum. This can help to make the ZX Spectrum heritage last more in the future.
Finally, I needed to explore a bit more the Pico SDK and its C development experience. Recently I'm doing embedded programming, and the RP2040 is one of the most interesting MCUs out there. Writing this code was extremely helpful to better understand the platform.
I want to thank Andre Weissflog for writing the original code and let us dream again. If this project was possible it is 90% because of his work.
You need either:
and...
This project only supports ST77xx displays so far. They are cheap and widespread, and they work well and exist in different qualities: TFT, IPS, and so forth.
A note of warning: it is crucial to be able to refresh the display fast enough. SPI displays work well if they are not huge, let's say that up to 320x240 max the update latency will not be so terrible.
Parallel 8-lines ST77xx displays are much better, for instance in the Tufty 2040, using upscaling, it is possible to transfer the Spectrum CRT frame buffer to the display in something like ~14 milliseconds, so even refreshing the display ~20 times per second we have 700 milliseconds of CPU time to run the emulator itself.
320x240 displays are particularly good because the Spectrum full visible area including borders is 320x256 pixels, so when borders are enabled this is a nice view. When borders are disabled, it's even better: the bitmap resolution of the Spectrum is 256x192 pixels, it means that using 125% upscaling we match exactly the 320x240 display resolution!
The display should also be big enough if you want a nice play experience. Spectrum games were designed to be displayed in a big TV set, so certain details can be too small if very small displays are used. 2.4" is a nice size. Larger is even better.
If you build from sources:
picotool
(pip install picotool
, or alike) and the Pico SDK.device_config.h
file in the main directory. For the Pimoroni Tufty 2040 just do cp devices/tufty2040.h device_config.h
. Otherwise if you have some different board, or you made one by hand with a Pico and an ST77xx display, just check the self-commented example file under the devices
directory and create a configuration for your setup: it's easy, just define your pins and the display interface (SPI/parallel).mkdir build; cd build; cmake ..; make
.zx.uf2
file to your Pico (put it in boot mode pressing the boot button as you power up the device, then drag the file in the RPI-RP2
drive you see as a USB drive).For the Tufty 2040 there is a ready to flash UF2 file inside the uf2
directory.
If you run the emualtor just after the installation, you will see the Spectrum BASIC screen and a text telling you there are no loaded games in the emulator.
To upload games:
games
directory.keymaps.txt
file inside the games
directory. Games without a keymap defined will likely not work with the default keymap, often to start the game pressing some key is needed, also to select a joystick and so forth. To add a keymap, see the next section. Otherwise, to start more easily, just use the games for which there is already a keymap defined (see list below).The loadgames.py
will contactenate the Z80 files and the keymap file and will store it into the flash. The bundle can be stored everywhere as long as the address is a multiple of 4096 and does not overwrite the emulator program itself.
Now, if you power-up the emualtor, you will see the list of games.
A keymap maps your define buttons (pins, actually) to Spectrum buttons and joystick moves. This is a very simple keymap for Jetpac:
# Jetpack.
MATCH:JETPAC IS LOADING
l1|l
r2|r
f4|f
d|f|u Up + fire combo for simpler playing
u5|u
@10:41 Press 4 at frame 10 to select Kempstone
Lines starting with #
are just comments.
Then there is a mandatory first line called MATCH:
. This line will grep the Spectrum memory content (after loading the game) looking for strings matching the game. This way the Jetpac keymap above, will work with Z80 files obtained in different ways, they don't have to be specially named, or to exactly match a given digest or alike. If you want to create a keymap for a game, just open the Z80 file with some editor and look for some unique string to match.
Sometimes it is needed to match multiple strings to be sure that it's a given game. For example the Bombjack keymap uses this match lines:
MATCH:Elite Systems Presents
AND-MATCH:BEST BOMBERS
You can have all the AND-MATCH
statements you want.
Then, there are the actual mapping information. They are of three types:
l12
, which means map button left pin (as defined in device_config.h
) to Spectrum keys '1' and '2' (each button on your device can control up to two keys in the Spectrum). You can just write rx
if you want to associate the left button on your device to a single Spectrum key, x
in this case.xur0
, which maps two device buttons to a single Spectrum key. This is needed for games where 5 buttons are not enough, so combinations of those buttons are needed.@20:k2
means: at frame 20 press the key k
for two frames. Then release it.There are a few special things to know about mappings.
l
, r
, u
, d
, f
: left, right, up, down, fire.f~
will map the fire button to the Spectrum space.l|l
means map the device left button to the joystick left movement.x<device-button1><device-button2><spectrum-key>
. See the example above.Certain games have specific requirements about the CRT refresh. This emulator does not emulate the CRT, but writes the video memory directly on the display in one pass (even if it try to syncrhonize with vertical blank interrupt in order to do so). If you see sprites flickering, you may want to use this special directive SCANLINE-PERIOD
in the keymap. See the example below.
# Thrust. Give it a bit more redrawing time with scanline period of 170.
MATCH:>>>THRUST<<<
SCANLINE-PERIOD:170
la|l
rs|r
fim
dm|d
up|u
@20:n1 Do you want to refine keys? [N]o
Other times, like in Skool Daze, the scanline period is reduced in order to trigger the vertical blank interrupt more often and make the game faster.
This repository includes keymaps that work with the following games:
This is the only demo I tested so far:
All the code here MIT licensed, so you are free to use this emulator for commercial purposes. Feel free to sell it, put it as example in your boards or whatever you want to do with it. However please note that games you find in the wild are often copyrighted material and are not under a free license. Either use free software games with a suitable license (there are new games developed for the Spectrum every year, very cool ones too: pick the ones with a suitable license), or ask permission to the copyright owners. In any case, you will be responsible for your actions, not me :).
About the ZX Spectrum ROM included in this repository, this is copyrighted material, and the current owner is the Sky Group, so if you want to do a commercial product using this code using also the ROM, you need to contact Sky Group. This is what happened so far: