SearchA-ZT › TRS-80 Model III (trs80)

TRS-80 Model III (trs80)

2024 MIT Online

trs80 is a pure-JavaScript TRS-80 Model III emulator by cschweda that runs entirely in the web browser with no plugins. It implements a full Z80 CPU, the Model III memory map, I/O ports and an 8x8 keyboard matrix, and boots the real 14K Model III ROM straight to the Level II BASIC READY prompt. On emulators.org it is wired into the shared in-page debugger, so you can step the Z80, set breakpoints and watchpoints, and inspect memory live.

Visit the source repository ↗

Visit the official site ↗

Runs on: Web browser

TRS-80 Model III (trs80) Online Emulator

Play TRS-80 Model III (trs80) using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
TRS-80 Model III · Level II BASICTRS-80 Model III (trs80)TRS-80 Model IIIgreyOpen ⛶

Machines emulated

Chips

Notes

Embedding

trs80 (cschweda/trs80-emulator) is an ES-module project, not a single script. We bundle a small boot entry with esbuild into trs80-boot.js, load it as <script type="module">, and drive the machine ourselves rather than using its own UI, so the loop can be paused and single-stepped, which the debugger needs.

Boot. Construct a TRS80System from the 14K Model III ROM, point a VideoSystem at a <canvas>, answer the ROM's Cass? and Memory Size? prompts to reach BASIC, then run your own loop built on the CPU's executeInstruction():

import { TRS80System } from "@system/trs80-system.js";
import { VideoSystem } from "@peripherals/video.js";
const system = new TRS80System({ romData });   // loads the 14K Model III ROM
const video  = new VideoSystem(canvas);       // 512x192, TRS-80 green-on-black
system.bootToCassetteBasic();                 // answers Cass? / Memory Size?, lands at READY
(function loop(){
  system.runTStates(33792);              // ~1/60s at 2.0275 MHz
  if (system.memory.videoDirty){ video.renderScreen(system.memory, system.columns32); system.memory.videoDirty = false; }
  requestAnimationFrame(loop);
})();

The machine is plain objects. Everything the debugger needs is a live property, no wasm heap to reach into:

MemberKindWhat it does
cpu.executeInstruction()methodExecute exactly one Z80 instruction, advancing cpu.cycles. The single-step primitive.
cpu.registersfieldLive registers: .A .F .B .C .D .E .H .L, the index halves .IXH/.IXL/.IYH/.IYL, .SP .PC, and .I .R. Flags live in .F.
cpu.interrupt() / cpu.nmi()methodDeliver a maskable INT (the 30 Hz RTC) or an NMI (the FDC), as the system does between instructions.
memory.readByte(a) / writeByte(a,v)methodThe 64K bus: 14K ROM, keyboard matrix, 1K video RAM, 48K RAM. Reads are side-effect-free, so the hex view can poll them.
memory.rom / .videoRam / .ramfieldThe raw backing stores, shown as their own memory chips in the debugger.
keyboard.keyDown(key,code) / keyUp(code)methodChar-accurate keyboard: browser keys map to the 8x8 matrix by the character they produce, with a synthetic SHIFT.
keyboard.pressKey(row,bit)methodPress a raw matrix position, used by the on-screen keyboard.

Because the CPU, bus and RAM are ordinary JavaScript, the debugger single-steps with executeInstruction(), reads and writes registers straight off cpu.registers, and implements breakpoints and watchpoints as host-side checks around those calls, with no changes to the emulator core.

Debugger integration

Wiring trs80 into the shared in-browser debugger needed two small pieces of custom work.

1 · A boot shim that owns the loop. The upstream project runs its own requestAnimationFrame loop that cannot be paused or single-stepped from outside, which the debugger needs. So the boot entry constructs just the machine and drives it from a loop we control, stepping instruction-by-instruction when breakpoints are set:

function stepOne(){                          // the RTC / interrupt / NMI handling, then one instruction
  if (cpu.cycles >= system.nextRtcAt){ io.raiseRTC(); system.nextRtcAt += RTC_INTERVAL_TSTATES; }
  if (io.pendingInterrupt()) cpu.interrupt();
  cpu.executeInstruction();
}
function runBudget(t){ const end = cpu.cycles + t;
  while (cpu.cycles < end){ if (bps.has(cpu.registers.PC)){ running = false; return; } stepOne(); } }

2 · Bundled as an ES module. The project's modules import each other, so it is built with esbuild in ESM format and loaded with <script type="module">. Because a module is deferred and the ROM is fetched asynchronously, the boot publishes window.EMU_BOOT late, so the debugger plug-in polls for it rather than reading it once.

Techniques for deeper access. The plug-in reaches into the running machine through the emulator's own surfaces:

  • Side-effect-free reads. The hex and disassembly views scrub memory with memory.readByte; the TRS-80 bus has no read-triggered I/O, so auto-polling a view is always safe.
  • Direct state. Registers are plain properties on cpu.registers, read and written live; memory.rom, memory.videoRam and memory.ram are the raw stores.
  • Execution breakpoints. With breakpoints set, the loop checks cpu.registers.PC against a Set before each instruction and stops on a hit.
  • Write watchpoints. Wrapping memory.writeByte pauses the loop the moment a watched address is written.
  • Single instruction step. executeInstruction() advances exactly one Z80 instruction.

Everything the debugger shows, registers read and write, memory hex and disassembly, follow-PC, single-step, breakpoints and watchpoints, is built from these, with no changes to the emulator itself.

Architecture

trs80 is a readable, interpreted TRS-80 Model III. Each block is an ordinary object wired together by the system:

  • Z80CPU - a full Z80 interpreter (base, CB, ED, DD/IX, FD/IY prefixes); executeInstruction() runs one instruction and advances a T-state counter.
  • MemorySystem - the Model III map: 14K ROM at $0000, the keyboard matrix at $3800, 1K video RAM at $3C00, 48K RAM at $4000.
  • IOSystem - the port map: the mode register (32-column mode), the 30 Hz RTC interrupt latch, and the WD1793 floppy controller.
  • KeyboardMatrix - the 8x8 memory-mapped keyboard, mapped from browser keys by the character they produce.
  • VideoSystem - the 64x16 character display with the Model III character generator and 2x3 block graphics, rastered to the canvas.
  • The 14K Model III ROM, Level II BASIC and the machine's I/O services, self-hosted as a grey system ROM.

The system clocks the CPU in T-state budgets at the real 2.0275 MHz and raises the RTC heartbeat the ROM's clock and cursor services depend on. Because every component is a plain object, the whole machine state is inspectable, which is what the debugger reads each refresh.