Timex.Emu is an emulator for the Timex 2048 — the author's first computer. The shared Z80 / tape / screen / keyboard modules also power a sibling ZX Spectrum 128 machine (see ZX Spectrum 128 support below).
Folder structure:
docs:
- Z80 CPU Manual
- Z80 CPU Peripherals
- Complete ROM
python:
timex.py— entry for the Timex 2048 (callsemulate.py --machine=timex2048).emulate.py— shared entry point; accepts--machine=<name>to pick a target (defaulttimex2048, alsospectrum128). All flags below work with either entry unless noted.machines/— per-machine wiring (timex2048.py,spectrum128.py).
Example command to load a user program:
python3 timex.py --program=helloworld.bin --startAt=8000 --breakAt=8000 --mapAt=8000 --hook-system
This loads user program helloworld.bin (--program) at 0x8000 (--mapAt), putting a break point at 0x8000 (--breakAt) and starting execution from 0x8000 too (--startAt). Additionally system function (i.e. print) are being hooked with python replacement.
To run with display (requires pygame-ce):
python3 timex.py
To run without display:
python3 timex.py --no-display
Keyboard mapping (PC → Timex 2048):
| PC Key | Timex 2048 |
|---|---|
| A-Z, 0-9 | Same |
| Enter | ENTER |
| Space | SPACE |
| Left/Right Shift | CAPS SHIFT |
| Left/Right Ctrl | SYMBOL SHIFT |
Common combinations:
Shift + 0— DELETE (backspace)Ctrl + P— " (double quote)Ctrl + Z— : (colon)Ctrl + N— , (comma)Ctrl + Symbol— ; (semicolon)
BASIC input modes:
- K mode (cursor shows
K) — default after ENTER. Single keypress gives keywords (e.g.P= PRINT,G= GOTO). Switches to L mode after first keyword. - L mode (cursor shows
L) — lowercase letter input. Entered automatically after typing a keyword in K mode. - C mode (cursor shows
C) — uppercase letters. Toggle with CAPS LOCK (Shift + 2). - E mode (cursor shows
E) — extended keywords. Enter by pressingShift + Ctrltogether. Then press a key (with or withoutCtrl) to get extended keywords like BEEP, INK, PAPER, etc. - G mode (cursor shows
G) — graphics characters. Enter withShift + 9.
Emulator keys:
F1— pause / resumeF2— open interactive debugger (in terminal)F3— open / close the debugger panel (live registers, disassembly and memory)F5— soft reset (clears RAM, reloads ROM, rewinds tape)F6— tape play/stop (for multi-load games)F8— save state to a .z80 v3 file (load back with--z80=state_xxxx.z80); with the debugger panel open this steps over instead, andCtrl+F8savesF11— toggle CRT scanline filterF12— save screenshot as PNGTab(hold) — turbo mode (fast-forward)Backspace(hold) — rewind time (~50 seconds buffer)Arrow keys— Kempston joystick directionsRight Alt— Kempston joystick fire
Example — typing BEEP 1,0:
- Make sure you're in K mode (press ENTER if needed)
Shift + Ctrl(enter E mode)Ctrl + Z(BEEP keyword)Space,1,Ctrl + N(comma),0Enter(execute)
Loading games from .tap files:
python3 timex.py --tape=game.tap
Then type LOAD "" (press J then Ctrl+P twice) and press Enter.
Loading BASIC listings from text files (no typing needed):
python3 timex.py --bas=program.bas
The plain-text listing is tokenized on the fly into a virtual tape (program header + data block), so you load it exactly like a .tap: type LOAD "" and press Enter. Add --bas-autorun=10 to have the program RUN from line 10 as soon as it loads. Example program.bas:
10 FOR i=1 TO 10
20 PRINT "Hello ";i
30 NEXT i
Listing rules:
- Every line starts with a line number (0–9999); lines may be in any order, blank lines are ignored.
- Keywords are case-insensitive and may be written with or without spaces (
GO TO/GOTO,GO SUB/GOSUB,DEF FN,OPEN #). - Keywords are only recognised as whole words, so a variable like
totalis not split intoTO+tal. Keep keywords separated from following letters (PRINT AT, notPRINTAT). - Text inside quotes and after
REMis kept verbatim;""inside a string is a literal quote. - Numbers get their hidden 5-byte binary form,
BIN 1010literals are supported, andDEF FNparameters get the slots the ROM expects. £and©map to the Spectrum characters; other non-ASCII characters are reported as errors with the source line.- On
--machine=spectrum128,SPECTRUMandPLAYare keywords too (use the menu's Tape Loader to load).
Loading .z80 snapshots (instant, no LOAD needed):
python3 timex.py --z80=game.z80
Additional options:
--scale=3— scale display (default 2x, use 3 or 4 for hi-res monitors)--debug— enable periodic PC/register logging to terminal--no-display— run headless (no pygame window)--attach-logger— attach the instruction logger at startup--tape-mode=trap|pulse— tape emulation strategy.trap(default) intercepts the ROMLD-BYTESroutine and injects blocks instantly.pulsestreams T-state-accurate EAR edges on port0xFEbit 6, loads in real time, and is the mode needed by turbo / custom loaders (e.g. multi-load games).
Sound: the ULA beeper (port 0xFE bit 4) is emulated and mixed to audio output via pygame — BEEP commands and in-game effects are audible.
Debugger commands (press F2 to enter):
| Command | Description |
|---|---|
ir |
8-bit registers |
ir16 |
16-bit registers |
if |
CPU flags |
d [0xADDR] |
disassemble at address (default: PC) |
m 0xADDR |
hex dump memory |
pram 0xADDR |
print single byte from RAM |
b 0xADDR |
set breakpoint |
bc 0xADDR |
clear breakpoint |
bd 0xADDR |
disable breakpoint (keep but skip) |
bl |
list breakpoints |
s |
single step |
n |
step over (skip into CALL/RST) |
c |
continue |
t |
print timing info (m-cycles, t-states) |
trace on/off |
enable/disable execution trace |
trace [n] |
show last n executed instructions |
stack [n] |
show stack entries (default 8) |
log |
attach/detach instruction logger |
? |
debugger help |
q / exit |
quit |
Debugger panel (press F3):
The panel docks beside the picture — the window grows to make room — and keeps updating while the emulator runs, so you can watch registers and memory change during play. It shows registers and flags, disassembly around PC, a hex view with bytes that changed since the last frame lit up, and the breakpoint / watchpoint list.
| Key | Action |
|---|---|
F7 |
step one instruction |
F8 |
step over (CALL/RST run to completion, stopping when the stack unwinds even if the routine returns elsewhere) |
F4 |
run to the disassembly cursor |
F9 |
run |
F10 |
break |
Ctrl+F8 |
save state while the panel is open |
Tab |
cycle pane: disassembly → memory → list → registers |
| arrows, PgUp/PgDn | move within the focused pane |
g |
go to an address |
b |
toggle a breakpoint at the disassembly cursor |
w |
watch the byte under the memory cursor |
e |
edit the byte, or the selected register (only while stopped) |
Esc |
cancel a prompt |
? |
show the key list (any key closes it); works while the emulator runs too |
While the panel is open and the emulator is running, ordinary keys still reach the emulated machine, so a game keeps playing; press ` to give the keys to the panel and Esc to hand them back. When execution is stopped, keys always drive the panel.
Watchpoints break as soon as a watched address is written, reporting the instruction and the old and new value, e.g. watch: write 5C78 0D -> 0E @8A42. Watching costs nothing until a watchpoint is set: the memory wrapper is installed only while one exists. Measured on the Timex ROM at 200 frames: 45.1 fps with no panel, 44.0 fps with the panel open and live, 41.5 fps with a watchpoint set.
The F2 terminal debugger is unchanged and still available; breakpoints hit while the panel is open stop in the panel instead of the terminal.
rom:
- Binary file containing ROM of actual machine
tests:
- a suite of unit tests
- running tests:
cd tests && python3 -m unittest discover(the tests add../pythonto the path, so run them from insidetests/).
- running tests:
Note
The zexall / zexdoc instruction exercisers live in tests/zex.py. They are not picked up by unittest discover (the file name doesn't match its default test*.py pattern), and each is skipped unless the matching environment variable is set. Run them from the tests/ directory — they load ./zexall.com / ./zexdoc.com relative to it:
ZEXALL=True python3 -m unittest zex.tests_zex.test_zexall
ZEXDOC=True python3 -m unittest zex.tests_zex.test_zexdoc
Expect a full run to take hours: every test group drives millions of instructions through the Python CPU core, printing OK or ERROR per group as it goes. The test fails if any group reports ERROR or the run doesn't reach Tests complete.
To run only some of the 67 groups, set ZEX_GROUPS to a regular expression matched against the group names (case-insensitive):
ZEXDOC=True ZEX_GROUPS='\(<ix,iy>\+1\)' python3 -m unittest zex.tests_zex.test_zexdoc
ZEXDOC=True ZEX_GROUPS='^neg' python3 -m unittest zex.tests_zex.test_zexdoc
List the group names with python3 zex_groups.py zexdoc.com. Selection works by rewriting the exerciser's own table of test pointers in memory before it starts — no test code or expected CRC is touched, so a group that passes in a subset run passes exactly as it would in a full one.
The emulator also runs as a ZX Spectrum 128 (sometimes called "Spectrum 128K" or, with Amstrad's +2, the grey-case machine). Most of the codebase is machine-agnostic — the CPU, screen rendering, keyboard, tape, and snapshot loader are shared. A dedicated Spectrum128Machine adds banked memory (8 × 16K RAM pages + 2 × 16K ROM banks), port 0x7FFD paging, and the 3.5469 MHz frame timing.
Launching:
python3 emulate.py --machine=spectrum128 --scale=3
ROM layout — drop the 128K ROM at rom/128.rom, either as:
- a single 32K file (bank 0 first = editor/menu ROM, bank 1 second = 48K BASIC), or
- a 16K file for bank 0 only; in that case a sibling
rom/128.1.romis loaded as bank 1 if present.
The standard Sinclair 128K ROM and the Amstrad +2 ROM both work. The +2A/+3 (black case, four ROMs + extra port 0x1FFD) is not supported.
Menu navigation (the "128 BASIC / Calculator / Tape Loader / Tape Tester / 48 BASIC" screen):
Shift + 7— cursor upShift + 6— cursor downEnter— select
Loading games:
.tapfiles work the same as on Timex (--tape=... --tape-mode=pulsefor custom loaders)..baslistings work too (--bas=program.bas), withSPECTRUMandPLAYrecognised as 128K keywords..z80snapshots of 128K games work via--z80=<file>. The loader detects 128K hardware (v2 hw=3|4, v3 hw=4|5|6), fans pages into the right banks, applies the port0x7FFDlatch from byte 35, and restores the AY registers from bytes 38-54 so music carries on where it left off.
Current status:
- Boots to the 128 menu, 128 BASIC, and 48 BASIC.
- Runs 128K snapshots (e.g. Commando).
- AY-3-8912 sound on ports
0xFFFD/0xBFFD: three tone channels, mixer, logarithmic amplitude, 17-bit LFSR noise, and full envelope generator (all 16 shapes). Registers read back through0xFFFD. Ports are decoded by address lines like the real machine (paging on A15=0/A1=0, AY on A15=1/A1=0), so mirror addresses such as0x3FFDandOUT (n),Awork too.F5silences the AY along with the reset. F8saves a 128K.z80v3 snapshot: all 8 RAM pages, the port0x7FFDpaging latch and the AY registers. Pages are written uncompressed, so a save is ~128K and loads here or in other emulators.
Screenshots from games running in the emulator. Press F12 in-emulator to save a PNG (written to the current working directory as screenshot_<frame>.png); move captures into screenshots/ and reference them below.
| Game | Screenshot |
|---|---|
| Bruce Lee | ![]() |
| Knight Lore | ![]() |
| Commando | ![]() |
| Bomb Jack | ![]() |



