Skip to content

s5emu: System-5 emulator and debugger

An emulator of Ken Rother's System-5 1802 computer for debugging software. It runs the real ROMs and ELF/OS disks, with a terminal debugger and a browser debugger that share one command language. Pure Python 3, standard library only.

cd software/s5emu
python3 -m s5emu

That opens the terminal debugger; type c to run. You get the HBIOS monitor prompt TEST>.

python3 -m s5emu --ui web

That opens the emulator in a browser at http://127.0.0.1:8802/.

In a browser, and in a GitHub Codespace

The browser page has two tabs. Machine is the System-5 to use: the 64 x 16 screen (click it and type; paste works too), Run / Pause, Reset, Boot ELF/OS (it types @FF00 and Enter at the monitor's prompt), the emulated clock (as fast as possible, 2 MHz, 1.76 MHz, 500 kHz) with the rate it actually reaches, the 1861's picture when the 1861 is on, and the IO panel card when it is fitted (--panel A; web-start.sh fits it unless S5_PANEL=none): its LEDs and TIL311s, the eight toggles, INPUT, RUN, LOAD and the Q light, to toggle a program in as on a COSMAC ELF. Debugger is the debugger: registers, the code at PC with breakpoints, memory and the command line.

The easy way to start it, with a CF card image of ELF/OS 0.2.6 made the first time (tools/mkelfos.py, 15 MB like the real 16 MB card, in disks/, never committed):

./web-start.sh --fg

Without --fg it runs in the background (./web-start.sh --stop stops it). S5_CLOCK=1.76m and S5_PORT=8803 change the clock and the port, and S5_ROM=roms/hbios-1.0.5.rom the ROM (the default is HBIOS 1.0.4, the ROM in the machine, but 1.0.5 in a GitHub Codespace; the line under the screen says which ROM runs).

In a GitHub Codespace, nothing to install: on github.com, Code -> Codespaces -> Create codespace on main. The repository's .devcontainer/ makes the CF card image when the Codespace is created and starts the emulator every time it starts; the page opens in a browser tab. CODESPACE.md is the guide.

Options for the page: --run starts the machine running at once; --clock 500k|1.76m|2m|max sets the emulated clock (a machine cycle is 8 clocks, so this is --speed in clocks); --web-host 0.0.0.0 listens on every interface instead of this computer only. The debugger command speed changes the clock while it runs.

The page talks to the emulator through one small object (emu: state, command, key, mem), over HTTP to s5emu/web.py. A page that runs the emulator itself in the browser sets window.S5_EMU before the page's script and needs no server: that is the next step (BACKLOG.md).

What is emulated

Card Model
CPU RCA 1802, every instruction with cycle counts, interrupts and DMA-out. 0x68 stops as an illegal op (1806 prefix). 4K EPROM at F000 (--rom-base). At reset it is also seen at 0000 until the first access with A15 high: the CPU card's VMA remap, which the ROM's first LDI F0 / PHI R0 relies on.
Memory 16 × 4K blocks, each jumpered RAM / EPROM / open (--jumpers RRRRRRR-EEEEEEE- is the default: RAM 0000–6FFF, 7000 open for video, EPROM 8000–EFFF). Open blocks float to FF; EPROM ignores writes.
6845 video 2K video RAM at 7000–77FF, CRTC address/data at 7800/7801. Start address and cursor are honoured; 64 × 16 as HBIOS sets it.
IO card Keyboard on port 1, with EF3 = key waiting. 1861: INP 4 = on, OUT 4 = off (--pixie-port); 262 lines × 14 cycles per frame; interrupt at line 78; 128 lines × 8 DMA bytes; EF1 frame markers.
IO panel card (--panel A; designed 2026-10-30, not built) Port 4: OUT 4 latches the byte the 8 LEDs and two TIL311s show, INP 4 reads the 8 toggles; INPUT is EF1 while held; RUN off resets and holds the 1802, LOAD on pauses it, both = LOAD mode (each INPUT press stores the toggles at M(R0), R0 counts on); after LOAD mode RUN runs RAM from 0000 (CPU Rev D's LOADED flag), otherwise Reset boots the ROM. The 1861 comes off port 4. The debugger's panel command sets it all. --hw --panel A runs the card from its netlist instead (with --cpu-rev D).
IDE/CF Mike Riley's 8-bit IDE: OUT 2 selects a register, port 3 is data. Commands read 20, write 30, identify EC and set-features EF, LBA28. Backed by --disk FILE; the save command writes it back.
Bus driver --buslink opens a pty that speaks the BUSDRV-B firmware protocol, so software/busdrv/s5busdrv.py --port <pty> drives the emulated machine as it would the real card. Taking the bus is refused while the emulated CPU runs.

This is a behavioural model of each card. For the hardware-faithful mode, see the next section.

VIDEO-G (--video g)

python3 -m s5emu --video g --disk elfos.img

System-5 with the VIDEO-G card (hardware/video-g/DESIGN.md, not built yet) in place of the 6845 card and the IO card (s5emu/videog.py, a behavioural model). The ROM defaults to roms/hbios-video-g.rom, the VIDEO-G build of HBIOS (firmware/hbios/video-g: the 6845 on OUT 5/6, 64 x 25, a 24-row scroll). Boot ELF/OS with @FF00 as before.

What Model
Text 2K at 7000-77FF, 64 x 25 (6845 R1/R6), cursor from R14/R15
Graphics window 7800-783F = pixel line Y (OUT 7) of page CPAGE, bit 7 of byte 0 = x 0; repeated to 7FFF
Pages two of 16K (512 x 256); NTSC shows lines 0-199. At power-up they hold noise (seeded)
Port 4 OUT = control (TEXT-OFF, GFX-ON, XOR, DPAGE, CPAGE, MOUSE-ON; on the GFX board, so nothing without it); INP = status (KEY-READY, MOUSE-READY, VSYNC and DE from the CPU clock, MONITOR, GFX-PRESENT, ID 10)
Ports 5/6, 7, 1 6845 address/data (INP 6 reads R14-R17); Y register / mouse byte; the keyboard (EF3)
Keys ASCII; F1-F12 = 80-8B; Home, Up, PgUp, Left, keypad 5, Right, End, Down, PgDn, Ins, Del = 90-9A, Shift + 40h (videog.KEYS)
Mouse video.mouse_packet(dx, dy, buttons) queues a PS/2 packet, only while MOUSE-ON; the latch keeps its byte after MOUSE-ON goes off (read INP 7 once before turning it back on)
Options --monitor mda (IBM 5151: 512 x 256, MONITOR = 1), --no-gfx (the TEXT board alone: the window and port 7 float)

Commands: png FILE [screen|gfx|text] [SCALE] writes the picture as a PNG (green on black; screen combines text and graphics as the control register says, with the 6845 Rev B character ROM firmware/char-rom-fonts/goodfont); vg shows the control register, Y register and status. screen shows the 25 text rows.

Found with it: the ELF/OS boot loader writes the sector numbers to port 4 while it loads the kernel, which leaves VIDEO-G's control register at 0E (graphics on); see firmware/hbios/video-g/README.md.

Hardware-faithful mode (--hw)

python3 -m s5emu --hw

Every memory cycle goes onto a modelled bus. Each card says whether it drives the data lines, and two drivers at once are logged as contention. The details are in s5emu/hw.py.

Card How it is modelled
Memory Gate level, from the card's KiCad netlist (hardware/memory/kicad/rev-*/reports/netlist.net): 4050 buffers, 4042 latches clocked by TPA, 74LS32 bank selects, 74LS151 block muxes, then the CE/OE/WE and address pins of each chip.
6845 (Rev B, the built card) Gate level, from its netlist (hardware/video-6845/kicad/rev-b): 74LS373 address latch, a 74LS85 comparing A12-A15 with jumper SV1 (gated by -VMA), then the IDT7134 dual-port RAM's bus port (A11 = 0) or the 6845's register interface (A11 = 1, E = TPB, RS = A0). The character generator and dot clock are not simulated; the netlist proves they cannot reach the bus path. --video-rev none = the old behavioural model.
IDE/CF (Rev A) Gate level, from its netlist (hardware/ide-cf/kicad/rev-a): each INP/OUT runs in four phases (TPA, TPB) through the 74LS138 port decode, the 74LS75 register latch and the NAND-latch INP window; the drive (the CF image) sees DIOR/DIOW and DA0-2/CS0/CS1 on its 40-pin header. Jumpers SV1 = port 2 (select), SV2 = port 3 (data). --ide-rev none = behavioural.
IO panel card (--panel A, 2026-10-30, not built) Gate level, from its netlist (hardware/io-panel/kicad/rev-a): the 74HCT138 port 4 decode, the 74HCT374 OUT latch (the LEDs and TIL311s), the 74HCT244 INP enable (its drive played from its OE), INPUT on -EF1, RUN/LOAD on -RESET/-PAUSE, the REQ/ACK/DRIVE flip-flops: one DMA IN per INPUT press in LOAD mode. The CPU core gained dma_in(). Shell: panel [sw HEX \| input \| run on\|off \| load on\|off]. Use with --cpu-rev D: toggle a program in (RUN off, LOAD on, panel sw/panel input per byte), LOAD off, RUN on, and it runs from 0000 (tests/test_hw.py IOPanel).
CPU card Rev D (--cpu-rev D, 2026-10-30, not built) Gate level, from its netlist (hardware/cpu/kicad/rev-d): 74HCT373 latch (transparent on TPA), the boot flip-flop cleared by CLEAR (= -RESET through two 74HCT14s) and set by the first A15 edge, LOADED (set by LOAD mode, cleared by the RESET/RUN switch, which a machine reset plays), the 74HCT20 F000 window, -VMA and the EPROM enable from a 74HCT00. Jumpers as for the 2005 IO card: JP2 EF1/EF2, JP3 -EF -> EF3. ccard.mode(reset, pause) holds -RESET/-PAUSE as a debug board would. HBIOS and ELF/OS boot on it with no bus events (tests/test_hw.py CPURevD, Boot).
CPU card (Rev A) Gate level, from its netlist (hardware/cpu/kicad/rev-a), as drawn: 74LS373 address latch, 74LS138 EPROM window (SV2/SV4 from --rom-base), the 74LS74 boot flip-flop (cleared by -RESET, set by the first access to the EPROM window, not by any A15 access), SV5 -VMA mode (--vma remap\|boot\|norom), the 74LS05 EPROM enable, and JP3/JP4 EF routing (1861 = EF1, keyboard = EF3). The 1802 and the CM1232 reset chip are the emulator's own. The built board's red-wire rework is not modelled yet. --cpu-rev none = behavioural.
IO (1861, keyboard) (Rev A) Gate level, from its netlist (hardware/io-1861-keyboard/kicad/rev-a): the same 74LS138 port decode and INP/OUT strobes as the IDE card; jumper SV2 = the keyboard's CDP1852 port (1), SV6 = the 1861's (4), with a 74LS02 making DISPON (INP) and DISPOFF (OUT). The PIC16F84 + PAK-VI keyboard path and the 1861's DMA timing stay behavioural; the netlist proves the PIC side cannot reach the bus path. --io-rev none = behavioural.

Every card on the bus now comes from its netlist; jumper settings are assumptions until Ken confirms them from the boards (bus card lists them). A memory cycle runs in the 1802's three phases (TPA high with the high byte, TPA falls, the low byte with -MRD/-MRW and TPB). Each netlist card is proved at load: every net that can change a bus device's control or address pin is driven only by modelled logic, and the data lines reach only bus devices; otherwise the card refuses to build.

--mem-rev:

Value Card
D (default) The card of record for hardware emulation (Ken, 2026-10-06): the Rev D KiCad master in hardware/memory/kicad/rev-d.
C Rev C (2005).
B-built Ken's Rev B board as photographed on 2026-10-05: RAM in IC1, EPROM in IC5, and the red-wire rework putting the 74LS32 bank selects on both !CE pins. The rework is netlist edits (lift/wire) in MEMORY_REVS, so a close-up photo can correct it.
B-drawn Rev B as drawn, every socket filled.

Jumpers (--jumpers) work on the netlist too: the card tries grounding each header net and records which chip and 4K block it enables (bus card lists them).

Debugger additions:

Command What it does
bus The contention log: fight (drivers disagree), double (two drivers, same value), multi-write (a write lands in two places), undefined (a chip control pin is X)
bus map Who answers a read in each 4K block, right now
bus card The memory card: netlist, rework, empty sockets, jumpers
bus stop fight,double / bus stop off Stop the run on those events (--bus-stop at start)

What it shows so far (HBIOS 1.0.4, 2026-10-06):

Card Result
B-drawn Stops on the very first fetch: the CPU EPROM and all four memory chips drive the bus (REVIEW.md finding 1).
B-built Boots to test>. The RAM in IC1 also answers 7000-7FFF, the 6845 card's block: every video RAM and CRTC write lands in both, and video RAM reads are double-driven (same value, so no fight yet).
C, D Boot, with no bus events. With memory D and the 6845 card from their netlists, ELF/OS boots and runs FREE with no bus events (about 400 s).

It runs about 40-65 thousand instructions a second, about half the speed of a real 1802; the decode cache makes the netlist cost a one-off per address.

Debugger commands (both front ends, -c and --script)

Command What it does
s [N] Step N instructions
n Step over a call (SEP R4 + address, i.e. SCRT CALL, or a plain SEP Rn)
c Run (Esc or Pause stops)
until ADDR Run to ADDR
b ADDR, bd ADDR / bd all, bl Set, delete, list breakpoints. ADDR can be hex, $hex, 0x.., ..h, a symbol, R3 or pc.
w r\|w\|i\|o ADDR, wd, wl Watch a memory read/write or an I/O port in/out
r Registers
set R3=F000 / set D=12 Change a register
m ADDR [LEN] Memory dump
poke ADDR BYTE... Write memory
d [ADDR] [N] Disassemble, with symbols and SCRT CALL/RETN
trace [N] The last N instructions executed
stack SCRT call stack
screen The 6845 screen as text
key TEXT Type on the keyboard (\n = Enter, \e = Esc)
load FILE [ADDR] Load Intel hex or binary into RAM
rom FILE, romhi FILE Load the CPU-card or memory-card EPROM
sym FILE Load symbols from an rcasm or A18 listing
disk FILE, save Attach a disk image; write it back
pixie 1861 status
link Path of the bus-driver pty
view text\|pixie\|auto Terminal only: which picture the screen pane shows (auto shows the 1861 while it's on)

Booting ELF/OS

python3 tools/mkrom.py
python3 tools/mkelfos.py elfos.img --mb 16
python3 -m s5emu --disk elfos.img

  1. tools/mkrom.py builds roms/hbios-1.0.4.rom: HBIOS plus Mike Riley's BIOS 1.0.4. It's already in roms/.
  2. tools/mkelfos.py makes a bootable disk: FSGEN, then kernel 0.2.6 build 135, then Mike's programs in / and /BIN.
  3. At TEST> type @FF00 (BIOS boot) and Enter, then press Enter once more. You get Elf/OS Ready and $.

Notes: - Type commands in capitals (DIR, FREE): disktool uppercases names when it copies them. - The BIOS 0.9 ROM can't run this kernel. firmware/hbios/v0.9/sec1.rom has no f_isalnum at FF78, so every command is "File not found". - Found while bringing this up: ELF/OS DIR prints 4 names × 16 characters, exactly the 64-column width, and HBIOS loses the first name of the next row at the wrap. So every 5th name is missing. That's in HBIOS, not the emulator.

Speed and tests

The emulator runs about 0.4–0.7 million instructions per second (a real 1802 at 1.79 MHz runs about 0.11 million). --speed 223750 throttles it to real time in machine cycles.

python3 tests/test_cpu.py
python3 tests/test_machine.py
python3 tests/test_hw.py

  • test_cpu.py: 14 instruction tests.
  • test_hw.py: Rev B's chip-select fault, Rev C's fix, jumpers found from the netlist, the B-built rework, and booting each card.
  • test_machine.py: the real HBIOS monitor boots, keyboard echo, memory jumpers, CRTC cursor, an 1861 DMA frame byte for byte, IDE read/write, and ELF/OS booting from a fresh disk and running FREE.
  • test_web.py: the browser page's HTTP API as the page uses it: boots running, a typed key echoes, the clock changes, both kinds of state.
  • test_videog.py: the VIDEO-G model (status, window, pages, CRTC ports, reset, VSYNC, PNG) and the VIDEO-G HBIOS (boot screen, the 24-row line feed and its 47 ms).

Files

File What it is
s5emu/cpu.py The 1802
s5emu/machine.py System-5 cards and memory map
s5emu/disasm.py Disassembler
s5emu/debugger.py Breakpoints, watches, trace, symbols, SCRT stack
s5emu/shell.py The command language
s5emu/tui.py Terminal debugger (curses; needs a terminal of at least 110 × 34)
s5emu/web.py, s5emu/web/index.html The browser page: the Machine and Debugger tabs, and the server behind them
web-start.sh, CODESPACE.md, ../../.devcontainer/ Starting the page (in the background, with a CF card image made the first time), and the GitHub Codespace
s5emu/buslink.py BUSDRV-B protocol on a pty
s5emu/videog.py --video g: the VIDEO-G card and its machine, the PNG writer
s5emu/netlist.py, s5emu/logic.py, s5emu/hw.py --hw: KiCad netlist reader with reworks, gate models, the bus and the per-revision memory cards
tools/mkrom.py, tools/mkelfos.py ROM and disk builders
roms/hbios-1.0.4.rom / .hex Built ROM
roms/hbios-1.0.5.rom / .hex HBIOS v1.0.5 (firmware/hbios/v1.0.5/build.py): v1.0.4 plus the monitor commands H, B, D (disassembler) and Q1 / Q0; not burned
roms/hbios-video-g.rom / .hex HBIOS for VIDEO-G (firmware/hbios/video-g/build.py)