Skip to content
pawlosPublic

About

An attempt to write a Timex 2048 emulator in python.

Topics

Resources

Stars

4 stars

Watchers

3 watching

Forks

Latest commit

 

History

684 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Timex.Emu

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 (calls emulate.py --machine=timex2048).
  • emulate.py — shared entry point; accepts --machine=<name> to pick a target (default timex2048, also spectrum128). 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 pressing Shift + Ctrl together. Then press a key (with or without Ctrl) to get extended keywords like BEEP, INK, PAPER, etc.
  • G mode (cursor shows G) — graphics characters. Enter with Shift + 9.

Emulator keys:

  • F1 — pause / resume
  • F2 — 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, and Ctrl+F8 saves
  • F11 — toggle CRT scanline filter
  • F12 — save screenshot as PNG
  • Tab (hold) — turbo mode (fast-forward)
  • Backspace (hold) — rewind time (~50 seconds buffer)
  • Arrow keys — Kempston joystick directions
  • Right Alt — Kempston joystick fire

Example — typing BEEP 1,0:

  1. Make sure you're in K mode (press ENTER if needed)
  2. Shift + Ctrl (enter E mode)
  3. Ctrl + Z (BEEP keyword)
  4. Space, 1, Ctrl + N (comma), 0
  5. Enter (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 total is not split into TO + tal. Keep keywords separated from following letters (PRINT AT, not PRINTAT).
  • Text inside quotes and after REM is kept verbatim; "" inside a string is a literal quote.
  • Numbers get their hidden 5-byte binary form, BIN 1010 literals are supported, and DEF FN parameters 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, SPECTRUM and PLAY are 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 ROM LD-BYTES routine and injects blocks instantly. pulse streams T-state-accurate EAR edges on port 0xFE bit 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 ../python to the path, so run them from inside 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.

ZX Spectrum 128 support

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.rom is 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 up
  • Shift + 6 — cursor down
  • Enter — select

Loading games:

  • .tap files work the same as on Timex (--tape=... --tape-mode=pulse for custom loaders).
  • .bas listings work too (--bas=program.bas), with SPECTRUM and PLAY recognised as 128K keywords.
  • .z80 snapshots 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 port 0x7FFD latch 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 through 0xFFFD. 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 as 0x3FFD and OUT (n),A work too. F5 silences the AY along with the reset.
  • F8 saves a 128K .z80 v3 snapshot: all 8 RAM pages, the port 0x7FFD paging latch and the AY registers. Pages are written uncompressed, so a save is ~128K and loads here or in other emulators.

Screenshots

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 Bruce Lee
Knight Lore Knight Lore
Commando Commando
Bomb Jack Bomb Jack

Links

Hits-of-Code

About

An attempt to write a Timex 2048 emulator in python.

Topics

Resources

Stars

4 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages