@joezilla/8sim 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +542 -0
- package/dist/8sim.browser.js +4728 -0
- package/dist/bundles/CardBundle.d.ts +83 -0
- package/dist/bundles/CardBundle.d.ts.map +1 -0
- package/dist/bundles/CardBundle.js +41 -0
- package/dist/bundles/CardBundle.js.map +1 -0
- package/dist/bundles/kernels.d.ts +48 -0
- package/dist/bundles/kernels.d.ts.map +1 -0
- package/dist/bundles/kernels.js +132 -0
- package/dist/bundles/kernels.js.map +1 -0
- package/dist/bundles/seed/index.d.ts +24 -0
- package/dist/bundles/seed/index.d.ts.map +1 -0
- package/dist/bundles/seed/index.js +266 -0
- package/dist/bundles/seed/index.js.map +1 -0
- package/dist/bus/Bus.d.ts +21 -0
- package/dist/bus/Bus.d.ts.map +1 -0
- package/dist/bus/Bus.js +62 -0
- package/dist/bus/Bus.js.map +1 -0
- package/dist/bus/BusRegion.d.ts +8 -0
- package/dist/bus/BusRegion.d.ts.map +1 -0
- package/dist/bus/BusRegion.js +8 -0
- package/dist/bus/BusRegion.js.map +1 -0
- package/dist/bus/SnoopBus.d.ts +15 -0
- package/dist/bus/SnoopBus.d.ts.map +1 -0
- package/dist/bus/SnoopBus.js +41 -0
- package/dist/bus/SnoopBus.js.map +1 -0
- package/dist/cards/BankRamCard.d.ts +35 -0
- package/dist/cards/BankRamCard.d.ts.map +1 -0
- package/dist/cards/BankRamCard.js +56 -0
- package/dist/cards/BankRamCard.js.map +1 -0
- package/dist/cards/DazzlerCard.d.ts +42 -0
- package/dist/cards/DazzlerCard.d.ts.map +1 -0
- package/dist/cards/DazzlerCard.js +83 -0
- package/dist/cards/DazzlerCard.js.map +1 -0
- package/dist/cards/DisplaySurface.d.ts +32 -0
- package/dist/cards/DisplaySurface.d.ts.map +1 -0
- package/dist/cards/DisplaySurface.js +11 -0
- package/dist/cards/DisplaySurface.js.map +1 -0
- package/dist/cards/FdcPlusClient.d.ts +35 -0
- package/dist/cards/FdcPlusClient.d.ts.map +1 -0
- package/dist/cards/FdcPlusClient.js +130 -0
- package/dist/cards/FdcPlusClient.js.map +1 -0
- package/dist/cards/ImsaiMioCard.d.ts +36 -0
- package/dist/cards/ImsaiMioCard.d.ts.map +1 -0
- package/dist/cards/ImsaiMioCard.js +48 -0
- package/dist/cards/ImsaiMioCard.js.map +1 -0
- package/dist/cards/ImsaiSioCard.d.ts +19 -0
- package/dist/cards/ImsaiSioCard.d.ts.map +1 -0
- package/dist/cards/ImsaiSioCard.js +54 -0
- package/dist/cards/ImsaiSioCard.js.map +1 -0
- package/dist/cards/KeyboardCard.d.ts +37 -0
- package/dist/cards/KeyboardCard.d.ts.map +1 -0
- package/dist/cards/KeyboardCard.js +79 -0
- package/dist/cards/KeyboardCard.js.map +1 -0
- package/dist/cards/Mc6850Acia.d.ts +68 -0
- package/dist/cards/Mc6850Acia.d.ts.map +1 -0
- package/dist/cards/Mc6850Acia.js +132 -0
- package/dist/cards/Mc6850Acia.js.map +1 -0
- package/dist/cards/Mits2SioCard.d.ts +27 -0
- package/dist/cards/Mits2SioCard.d.ts.map +1 -0
- package/dist/cards/Mits2SioCard.js +36 -0
- package/dist/cards/Mits2SioCard.js.map +1 -0
- package/dist/cards/MitsDcddCard.d.ts +52 -0
- package/dist/cards/MitsDcddCard.d.ts.map +1 -0
- package/dist/cards/MitsDcddCard.js +294 -0
- package/dist/cards/MitsDcddCard.js.map +1 -0
- package/dist/cards/ParallelCard.d.ts +35 -0
- package/dist/cards/ParallelCard.d.ts.map +1 -0
- package/dist/cards/ParallelCard.js +32 -0
- package/dist/cards/ParallelCard.js.map +1 -0
- package/dist/cards/Port8212.d.ts +31 -0
- package/dist/cards/Port8212.d.ts.map +1 -0
- package/dist/cards/Port8212.js +47 -0
- package/dist/cards/Port8212.js.map +1 -0
- package/dist/cards/RtcCard.d.ts +30 -0
- package/dist/cards/RtcCard.d.ts.map +1 -0
- package/dist/cards/RtcCard.js +61 -0
- package/dist/cards/RtcCard.js.map +1 -0
- package/dist/cards/SerialCard.d.ts +31 -0
- package/dist/cards/SerialCard.d.ts.map +1 -0
- package/dist/cards/SerialCard.js +28 -0
- package/dist/cards/SerialCard.js.map +1 -0
- package/dist/cards/Tr1602Uart.d.ts +55 -0
- package/dist/cards/Tr1602Uart.d.ts.map +1 -0
- package/dist/cards/Tr1602Uart.js +102 -0
- package/dist/cards/Tr1602Uart.js.map +1 -0
- package/dist/cards/Usart8251.d.ts +28 -0
- package/dist/cards/Usart8251.d.ts.map +1 -0
- package/dist/cards/Usart8251.js +88 -0
- package/dist/cards/Usart8251.js.map +1 -0
- package/dist/cards/VdmCard.d.ts +27 -0
- package/dist/cards/VdmCard.d.ts.map +1 -0
- package/dist/cards/VdmCard.js +40 -0
- package/dist/cards/VdmCard.js.map +1 -0
- package/dist/clock/ImmediateClock.d.ts +8 -0
- package/dist/clock/ImmediateClock.d.ts.map +1 -0
- package/dist/clock/ImmediateClock.js +13 -0
- package/dist/clock/ImmediateClock.js.map +1 -0
- package/dist/clock/SystemClock.d.ts +45 -0
- package/dist/clock/SystemClock.d.ts.map +1 -0
- package/dist/clock/SystemClock.js +71 -0
- package/dist/clock/SystemClock.js.map +1 -0
- package/dist/cpu/Cpu8080.d.ts +34 -0
- package/dist/cpu/Cpu8080.d.ts.map +1 -0
- package/dist/cpu/Cpu8080.js +126 -0
- package/dist/cpu/Cpu8080.js.map +1 -0
- package/dist/cpu/Decoder.d.ts +12 -0
- package/dist/cpu/Decoder.d.ts.map +1 -0
- package/dist/cpu/Decoder.js +23 -0
- package/dist/cpu/Decoder.js.map +1 -0
- package/dist/cpu/Flags.d.ts +18 -0
- package/dist/cpu/Flags.d.ts.map +1 -0
- package/dist/cpu/Flags.js +33 -0
- package/dist/cpu/Flags.js.map +1 -0
- package/dist/cpu/Registers.d.ts +22 -0
- package/dist/cpu/Registers.d.ts.map +1 -0
- package/dist/cpu/Registers.js +26 -0
- package/dist/cpu/Registers.js.map +1 -0
- package/dist/cpu/instructions/alu.d.ts +3 -0
- package/dist/cpu/instructions/alu.d.ts.map +1 -0
- package/dist/cpu/instructions/alu.js +221 -0
- package/dist/cpu/instructions/alu.js.map +1 -0
- package/dist/cpu/instructions/branch.d.ts +3 -0
- package/dist/cpu/instructions/branch.d.ts.map +1 -0
- package/dist/cpu/instructions/branch.js +117 -0
- package/dist/cpu/instructions/branch.js.map +1 -0
- package/dist/cpu/instructions/control.d.ts +3 -0
- package/dist/cpu/instructions/control.d.ts.map +1 -0
- package/dist/cpu/instructions/control.js +12 -0
- package/dist/cpu/instructions/control.js.map +1 -0
- package/dist/cpu/instructions/data.d.ts +3 -0
- package/dist/cpu/instructions/data.d.ts.map +1 -0
- package/dist/cpu/instructions/data.js +137 -0
- package/dist/cpu/instructions/data.js.map +1 -0
- package/dist/cpu/instructions/io.d.ts +3 -0
- package/dist/cpu/instructions/io.d.ts.map +1 -0
- package/dist/cpu/instructions/io.js +18 -0
- package/dist/cpu/instructions/io.js.map +1 -0
- package/dist/cpu/instructions/logical.d.ts +3 -0
- package/dist/cpu/instructions/logical.d.ts.map +1 -0
- package/dist/cpu/instructions/logical.js +129 -0
- package/dist/cpu/instructions/logical.js.map +1 -0
- package/dist/cpu/instructions/rotate.d.ts +3 -0
- package/dist/cpu/instructions/rotate.d.ts.map +1 -0
- package/dist/cpu/instructions/rotate.js +34 -0
- package/dist/cpu/instructions/rotate.js.map +1 -0
- package/dist/cpu/instructions/stack.d.ts +3 -0
- package/dist/cpu/instructions/stack.d.ts.map +1 -0
- package/dist/cpu/instructions/stack.js +84 -0
- package/dist/cpu/instructions/stack.js.map +1 -0
- package/dist/cpu/status8080.d.ts +33 -0
- package/dist/cpu/status8080.d.ts.map +1 -0
- package/dist/cpu/status8080.js +73 -0
- package/dist/cpu/status8080.js.map +1 -0
- package/dist/cpu/z80/CpuZ80.d.ts +53 -0
- package/dist/cpu/z80/CpuZ80.d.ts.map +1 -0
- package/dist/cpu/z80/CpuZ80.js +168 -0
- package/dist/cpu/z80/CpuZ80.js.map +1 -0
- package/dist/cpu/z80/DecoderZ80.d.ts +26 -0
- package/dist/cpu/z80/DecoderZ80.d.ts.map +1 -0
- package/dist/cpu/z80/DecoderZ80.js +107 -0
- package/dist/cpu/z80/DecoderZ80.js.map +1 -0
- package/dist/cpu/z80/FlagsZ80.d.ts +27 -0
- package/dist/cpu/z80/FlagsZ80.d.ts.map +1 -0
- package/dist/cpu/z80/FlagsZ80.js +47 -0
- package/dist/cpu/z80/FlagsZ80.js.map +1 -0
- package/dist/cpu/z80/RegistersZ80.d.ts +60 -0
- package/dist/cpu/z80/RegistersZ80.d.ts.map +1 -0
- package/dist/cpu/z80/RegistersZ80.js +90 -0
- package/dist/cpu/z80/RegistersZ80.js.map +1 -0
- package/dist/cpu/z80/flagHelpers.d.ts +25 -0
- package/dist/cpu/z80/flagHelpers.d.ts.map +1 -0
- package/dist/cpu/z80/flagHelpers.js +136 -0
- package/dist/cpu/z80/flagHelpers.js.map +1 -0
- package/dist/cpu/z80/instructions/alu16.d.ts +4 -0
- package/dist/cpu/z80/instructions/alu16.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/alu16.js +27 -0
- package/dist/cpu/z80/instructions/alu16.js.map +1 -0
- package/dist/cpu/z80/instructions/alu8.d.ts +4 -0
- package/dist/cpu/z80/instructions/alu8.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/alu8.js +100 -0
- package/dist/cpu/z80/instructions/alu8.js.map +1 -0
- package/dist/cpu/z80/instructions/bits.d.ts +10 -0
- package/dist/cpu/z80/instructions/bits.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/bits.js +164 -0
- package/dist/cpu/z80/instructions/bits.js.map +1 -0
- package/dist/cpu/z80/instructions/block.d.ts +10 -0
- package/dist/cpu/z80/instructions/block.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/block.js +141 -0
- package/dist/cpu/z80/instructions/block.js.map +1 -0
- package/dist/cpu/z80/instructions/control.d.ts +4 -0
- package/dist/cpu/z80/instructions/control.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/control.js +62 -0
- package/dist/cpu/z80/instructions/control.js.map +1 -0
- package/dist/cpu/z80/instructions/ed.d.ts +4 -0
- package/dist/cpu/z80/instructions/ed.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/ed.js +149 -0
- package/dist/cpu/z80/instructions/ed.js.map +1 -0
- package/dist/cpu/z80/instructions/exchange.d.ts +4 -0
- package/dist/cpu/z80/instructions/exchange.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/exchange.js +37 -0
- package/dist/cpu/z80/instructions/exchange.js.map +1 -0
- package/dist/cpu/z80/instructions/io.d.ts +8 -0
- package/dist/cpu/z80/instructions/io.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/io.js +22 -0
- package/dist/cpu/z80/instructions/io.js.map +1 -0
- package/dist/cpu/z80/instructions/jump.d.ts +4 -0
- package/dist/cpu/z80/instructions/jump.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/jump.js +113 -0
- package/dist/cpu/z80/instructions/jump.js.map +1 -0
- package/dist/cpu/z80/instructions/load.d.ts +7 -0
- package/dist/cpu/z80/instructions/load.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/load.js +103 -0
- package/dist/cpu/z80/instructions/load.js.map +1 -0
- package/dist/cpu/z80/instructions/rotate.d.ts +9 -0
- package/dist/cpu/z80/instructions/rotate.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/rotate.js +48 -0
- package/dist/cpu/z80/instructions/rotate.js.map +1 -0
- package/dist/cpu/z80/instructions/stack.d.ts +4 -0
- package/dist/cpu/z80/instructions/stack.d.ts.map +1 -0
- package/dist/cpu/z80/instructions/stack.js +19 -0
- package/dist/cpu/z80/instructions/stack.js.map +1 -0
- package/dist/cpu/z80/regcodes.d.ts +22 -0
- package/dist/cpu/z80/regcodes.d.ts.map +1 -0
- package/dist/cpu/z80/regcodes.js +93 -0
- package/dist/cpu/z80/regcodes.js.map +1 -0
- package/dist/cpu/z80/types.d.ts +59 -0
- package/dist/cpu/z80/types.d.ts.map +1 -0
- package/dist/cpu/z80/types.js +2 -0
- package/dist/cpu/z80/types.js.map +1 -0
- package/dist/cpu/z80/views.d.ts +8 -0
- package/dist/cpu/z80/views.d.ts.map +1 -0
- package/dist/cpu/z80/views.js +40 -0
- package/dist/cpu/z80/views.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/IBus.d.ts +8 -0
- package/dist/interfaces/IBus.d.ts.map +1 -0
- package/dist/interfaces/IBus.js +2 -0
- package/dist/interfaces/IBus.js.map +1 -0
- package/dist/interfaces/IBusObserver.d.ts +7 -0
- package/dist/interfaces/IBusObserver.d.ts.map +1 -0
- package/dist/interfaces/IBusObserver.js +2 -0
- package/dist/interfaces/IBusObserver.js.map +1 -0
- package/dist/interfaces/IClock.d.ts +6 -0
- package/dist/interfaces/IClock.d.ts.map +1 -0
- package/dist/interfaces/IClock.js +2 -0
- package/dist/interfaces/IClock.js.map +1 -0
- package/dist/interfaces/ICpu.d.ts +46 -0
- package/dist/interfaces/ICpu.d.ts.map +1 -0
- package/dist/interfaces/ICpu.js +2 -0
- package/dist/interfaces/ICpu.js.map +1 -0
- package/dist/interfaces/IIODevice.d.ts +7 -0
- package/dist/interfaces/IIODevice.d.ts.map +1 -0
- package/dist/interfaces/IIODevice.js +2 -0
- package/dist/interfaces/IIODevice.js.map +1 -0
- package/dist/interfaces/IInterruptController.d.ts +8 -0
- package/dist/interfaces/IInterruptController.d.ts.map +1 -0
- package/dist/interfaces/IInterruptController.js +2 -0
- package/dist/interfaces/IInterruptController.js.map +1 -0
- package/dist/interfaces/IMemory.d.ts +9 -0
- package/dist/interfaces/IMemory.d.ts.map +1 -0
- package/dist/interfaces/IMemory.js +2 -0
- package/dist/interfaces/IMemory.js.map +1 -0
- package/dist/interfaces/IModule.d.ts +5 -0
- package/dist/interfaces/IModule.d.ts.map +1 -0
- package/dist/interfaces/IModule.js +2 -0
- package/dist/interfaces/IModule.js.map +1 -0
- package/dist/interfaces/IS100Card.d.ts +6 -0
- package/dist/interfaces/IS100Card.d.ts.map +1 -0
- package/dist/interfaces/IS100Card.js +2 -0
- package/dist/interfaces/IS100Card.js.map +1 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +2 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interrupt/InterruptController.d.ts +13 -0
- package/dist/interrupt/InterruptController.d.ts.map +1 -0
- package/dist/interrupt/InterruptController.js +36 -0
- package/dist/interrupt/InterruptController.js.map +1 -0
- package/dist/io/IoSpace.d.ts +9 -0
- package/dist/io/IoSpace.d.ts.map +1 -0
- package/dist/io/IoSpace.js +30 -0
- package/dist/io/IoSpace.js.map +1 -0
- package/dist/machine/MachineRunner.d.ts +54 -0
- package/dist/machine/MachineRunner.d.ts.map +1 -0
- package/dist/machine/MachineRunner.js +102 -0
- package/dist/machine/MachineRunner.js.map +1 -0
- package/dist/machine/MachineSpec.d.ts +80 -0
- package/dist/machine/MachineSpec.d.ts.map +1 -0
- package/dist/machine/MachineSpec.js +9 -0
- package/dist/machine/MachineSpec.js.map +1 -0
- package/dist/machine/buildMachine.d.ts +19 -0
- package/dist/machine/buildMachine.d.ts.map +1 -0
- package/dist/machine/buildMachine.js +122 -0
- package/dist/machine/buildMachine.js.map +1 -0
- package/dist/memory/MemoryMappedIOAdapter.d.ts +14 -0
- package/dist/memory/MemoryMappedIOAdapter.d.ts.map +1 -0
- package/dist/memory/MemoryMappedIOAdapter.js +23 -0
- package/dist/memory/MemoryMappedIOAdapter.js.map +1 -0
- package/dist/memory/Ram.d.ts +17 -0
- package/dist/memory/Ram.d.ts.map +1 -0
- package/dist/memory/Ram.js +36 -0
- package/dist/memory/Ram.js.map +1 -0
- package/dist/memory/Rom.d.ts +13 -0
- package/dist/memory/Rom.d.ts.map +1 -0
- package/dist/memory/Rom.js +25 -0
- package/dist/memory/Rom.js.map +1 -0
- package/dist/util/bits.d.ts +11 -0
- package/dist/util/bits.d.ts.map +1 -0
- package/dist/util/bits.js +35 -0
- package/dist/util/bits.js.map +1 -0
- package/dist/util/hostConsole.d.ts +2 -0
- package/dist/util/hostConsole.d.ts.map +1 -0
- package/dist/util/hostConsole.js +4 -0
- package/dist/util/hostConsole.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# 8sim — S-100 Bus Machine Emulator (Intel 8080 / Zilog Z80)
|
|
2
|
+
|
|
3
|
+
An **S-100 bus microcomputer emulator** written in TypeScript. 8sim models the machine the way the hardware was actually built: a passive backplane (`Bus`) into which you plug **cards** — a CPU board, RAM and EPROM boards, serial and floppy-controller boards. Everything sits behind well-defined interfaces, so every card is independently testable and swappable, and even the processor is just another card — pluggable between the Intel **8080** and a full Zilog **Z80**.
|
|
4
|
+
|
|
5
|
+
Assemble a machine imperatively (attach memory and cards by hand) or declaratively (describe it as a `MachineSpec` and let `buildMachine` construct it, rejecting bus collisions at build time). Boot period software — stock Altair CP/M boots unmodified on either CPU.
|
|
6
|
+
|
|
7
|
+
Runs in Node.js, browsers, Deno, and Bun — zero runtime dependencies.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **S-100 machine model**: passive `Bus` backplane, pluggable cards, 16-bit memory space + 8-bit I/O space + interrupt controller
|
|
14
|
+
- **Card catalog**: real period cards modeled as bus devices — MITS 88-2SIO, IMSAI SIO-2, IMSAI MIO, MITS 88-DCDD floppy controller, plus the component chips they're built from (8251, 6850, 8212, TR1602)
|
|
15
|
+
- **Pluggable CPU cards**: complete Intel 8080 instruction set, and a full Zilog Z80 core (validated against ZEXDOC + ZEXALL) — same bus, same constructor
|
|
16
|
+
- **Declarative machine assembly**: build a whole machine from a `MachineSpec`; overlapping memory and colliding I/O ports are rejected at build time
|
|
17
|
+
- **Card bundles**: self-describing packages (config schema + factory) for CPU, RAM, EPROM, and serial/floppy cards
|
|
18
|
+
- Memory-mapped I/O, ROM write protection, and a snooping bus for tracing/debug
|
|
19
|
+
- Interrupt controller with RST vector dispatch; accurate flags; EI/DI one-instruction delay; HLT with interrupt wake
|
|
20
|
+
- Real-time (authentic 2 MHz) and immediate-mode clocks
|
|
21
|
+
- Browser-safe: no Node.js globals in the core library
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
npm test
|
|
30
|
+
npm run build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Cpu8080, Bus, Ram, Rom, InterruptController } from './src/index.js';
|
|
39
|
+
|
|
40
|
+
// 1. Create the interrupt controller and bus
|
|
41
|
+
const pic = new InterruptController();
|
|
42
|
+
const bus = new Bus(pic);
|
|
43
|
+
|
|
44
|
+
// 2. Attach memory
|
|
45
|
+
const rom = new Rom('rom', 0x0000, new Uint8Array([/* your program */]));
|
|
46
|
+
const ram = new Ram('ram', 0x2000, 0x4000);
|
|
47
|
+
bus.attachMemory(rom);
|
|
48
|
+
bus.attachMemory(ram);
|
|
49
|
+
|
|
50
|
+
// 3. Create and run the CPU
|
|
51
|
+
const cpu = new Cpu8080(bus, pic);
|
|
52
|
+
cpu.registers.pc = 0x0000;
|
|
53
|
+
cpu.registers.sp = 0x5fff;
|
|
54
|
+
|
|
55
|
+
// Step one instruction at a time
|
|
56
|
+
const cycles = cpu.step(); // returns T-states consumed
|
|
57
|
+
|
|
58
|
+
// Or run until HLT
|
|
59
|
+
cpu.run();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
┌──────────────────────────────────────────────────┐
|
|
68
|
+
│ ICpu (Cpu8080 | CpuZ80) │
|
|
69
|
+
│ Registers, Flags, Decoder, step() / run() │
|
|
70
|
+
│ polls: IInterruptController │
|
|
71
|
+
│ accesses: IBus │
|
|
72
|
+
└────────────────────┬─────────────────────────────┘
|
|
73
|
+
│ IBus
|
|
74
|
+
┌────────────────────▼─────────────────────────────┐
|
|
75
|
+
│ Bus (SystemBus) │
|
|
76
|
+
│ memoryMap: BusRegion[] (sorted, 16-bit addr) │
|
|
77
|
+
│ ioSpace: IoSpace (8-bit port space) │
|
|
78
|
+
│ pic: IInterruptController │
|
|
79
|
+
└───────┬──────────────┬──────────────┬────────────┘
|
|
80
|
+
│ │ │
|
|
81
|
+
IMemory IIODevice IInterruptController
|
|
82
|
+
(RAM/ROM/ (port or (PIC / IRQ lines)
|
|
83
|
+
MMIO adapter) MMIO device)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The CPU holds only a reference to `IBus`. All memory, I/O, and interrupt logic is behind that interface. This lets you:
|
|
87
|
+
|
|
88
|
+
- Unit-test the CPU with a mock bus
|
|
89
|
+
- Swap any peripheral without touching the CPU
|
|
90
|
+
- Run the same code in Node or a browser
|
|
91
|
+
|
|
92
|
+
### Memory Space
|
|
93
|
+
|
|
94
|
+
`Bus` holds a sorted list of `BusRegion` entries. On each `read`/`write`, it finds the region containing the address and forwards the call as an offset into that region. Unmapped reads return `0xFF`; unmapped writes are silently ignored. ROM regions reject writes without error.
|
|
95
|
+
|
|
96
|
+
### I/O Space
|
|
97
|
+
|
|
98
|
+
`IoSpace` maps 8-bit port numbers to `IIODevice` instances. Unregistered ports return `0xFF` on read and ignore writes.
|
|
99
|
+
|
|
100
|
+
### Memory-Mapped I/O
|
|
101
|
+
|
|
102
|
+
`MemoryMappedIOAdapter` wraps any `IIODevice` as an `IMemory` region. Attach it to the bus at any base address and the device becomes accessible from both the memory and I/O address spaces simultaneously — no CPU or bus changes required.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const uart = new MyUart();
|
|
106
|
+
bus.attachIODevice(uart); // I/O port access
|
|
107
|
+
bus.attachMemory(new MemoryMappedIOAdapter(0xe000, 8, uart)); // memory access
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Bus Snooping
|
|
111
|
+
|
|
112
|
+
`SnoopBus` wraps any `IBus` and forwards every memory/I/O access to a list of `IBusObserver`s before returning — a transparent tap for tracing, logging, or building a debugger, with no changes to the CPU or the cards.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const snoop = new SnoopBus(bus);
|
|
116
|
+
snoop.attach({ onMemWrite: (addr, val) => log(`W ${addr.toString(16)}=${val.toString(16)}`) });
|
|
117
|
+
const cpu = new Cpu8080(snoop, pic);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Cards
|
|
123
|
+
|
|
124
|
+
A card is an `IS100Card` — a module with an `attach(bus)` method that wires itself onto the backplane (claiming I/O ports, memory regions, or IRQ lines). 8sim ships a catalog of real S-100 cards plus the component chips they're built from. Each is usable directly, or via its [card bundle](#card-bundles--machine-assembly) for declarative assembly.
|
|
125
|
+
|
|
126
|
+
| Card | Maker (year) | What it is |
|
|
127
|
+
|------|--------------|-----------|
|
|
128
|
+
| **88-2SIO** (`mits-88-2sio`) | MITS | Dual serial board (2× 6850 ACIA). The assumed console for nearly all Altair software — attach this to boot Altair CP/M. |
|
|
129
|
+
| **SIO-2** (`imsai-sio2`) | IMSAI | Dual 8251 serial card with a board-control port. |
|
|
130
|
+
| **MIO** (`imsai-mio`) | IMSAI | Multi-I/O board: a TR1602 UART serial port plus two 8212 parallel ports. |
|
|
131
|
+
| **88-DCDD** (`mits-88-dcdd`) | MITS | 8-inch floppy disk controller; disk I/O flows over an FDC channel (e.g. an fdcplus-web server). |
|
|
132
|
+
|
|
133
|
+
Component chips, usable standalone or as building blocks for a card:
|
|
134
|
+
|
|
135
|
+
| Chip | What it is |
|
|
136
|
+
|------|-----------|
|
|
137
|
+
| **Intel 8251** (`intel-8251`) | USART — data + control port |
|
|
138
|
+
| **Motorola 6850** (`motorola-6850`) | ACIA — status + data port |
|
|
139
|
+
| **Intel 8212** (`intel-8212`) | 8-bit parallel I/O port |
|
|
140
|
+
| **TR1602** (`tr1602-uart`) | UART — data + status port |
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { Bus, InterruptController, Mits2SioCard } from '@joezilla/8sim';
|
|
144
|
+
|
|
145
|
+
const bus = new Bus(new InterruptController());
|
|
146
|
+
const console = new Mits2SioCard('sio', { basePort: 0x10 }); // Altair console at 0x10/0x11
|
|
147
|
+
console.attach(bus); // claims ports 0x10–0x13
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## CPU
|
|
153
|
+
|
|
154
|
+
### Registers
|
|
155
|
+
|
|
156
|
+
| Register | Width | Notes |
|
|
157
|
+
|----------|-------|-------|
|
|
158
|
+
| A | 8-bit | Accumulator |
|
|
159
|
+
| B, C | 8-bit | BC pair |
|
|
160
|
+
| D, E | 8-bit | DE pair |
|
|
161
|
+
| H, L | 8-bit | HL pair; M pseudo-register = `(HL)` |
|
|
162
|
+
| SP | 16-bit | Stack pointer |
|
|
163
|
+
| PC | 16-bit | Program counter |
|
|
164
|
+
|
|
165
|
+
`Registers` exposes `bc`, `de`, `hl` as read/write pair accessors.
|
|
166
|
+
|
|
167
|
+
### Flags
|
|
168
|
+
|
|
169
|
+
Stored as an 8080 PSW byte: `S Z 0 AC 0 P 1 CY` (bit 1 is always 1).
|
|
170
|
+
|
|
171
|
+
| Flag | Bit | Meaning |
|
|
172
|
+
|------|-----|---------|
|
|
173
|
+
| S | 7 | Sign (bit 7 of result) |
|
|
174
|
+
| Z | 6 | Zero |
|
|
175
|
+
| AC | 4 | Auxiliary carry (half-carry, used by DAA) |
|
|
176
|
+
| P | 2 | Parity (even popcount) |
|
|
177
|
+
| CY | 0 | Carry / borrow |
|
|
178
|
+
|
|
179
|
+
### `step()` Execution Model
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
1. Halted? → if INTE + pending interrupt: wake and handle; else return idle cycles
|
|
183
|
+
2. pendingEI → inte = true (EI's one-instruction delay commits here)
|
|
184
|
+
3. INTE + pending? → handleInterrupt (pushes PC, jumps to RST vector)
|
|
185
|
+
4. Fetch opcode; HLT / EI / DI handled inline
|
|
186
|
+
5. Dispatch through 256-entry Decoder table → returns T-states
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Interrupts
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// Assert an IRQ line (0–7)
|
|
193
|
+
pic.assertIRQ(1);
|
|
194
|
+
|
|
195
|
+
// Enable interrupts in the CPU
|
|
196
|
+
cpu.inte = true;
|
|
197
|
+
|
|
198
|
+
// The CPU will take the interrupt at the start of the next step()
|
|
199
|
+
// after any pending EI delay resolves.
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`InterruptController.acknowledge()` returns `0xC7 | (line << 3)` — the RST *n* opcode byte. The CPU jumps to `rstByte & 0x38` (vector × 8):
|
|
203
|
+
|
|
204
|
+
| IRQ line | RST opcode | Vector |
|
|
205
|
+
|----------|-----------|--------|
|
|
206
|
+
| 0 | RST 0 | 0x0000 |
|
|
207
|
+
| 1 | RST 1 | 0x0008 |
|
|
208
|
+
| 2 | RST 2 | 0x0010 |
|
|
209
|
+
| … | … | … |
|
|
210
|
+
| 7 | RST 7 | 0x0038 |
|
|
211
|
+
|
|
212
|
+
**EI/DI behaviour:**
|
|
213
|
+
- `EI` sets `pendingEI=true`. Interrupts become enabled at the *start* of the next `step()` call (before the next instruction is fetched), matching real 8080 behaviour where one instruction executes after EI before interrupts are accepted.
|
|
214
|
+
- `DI` clears `inte` and `pendingEI` immediately.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Z80 CPU
|
|
219
|
+
|
|
220
|
+
`CpuZ80` (`src/cpu/z80/`) is a second, fully-implemented CPU core that runs on the
|
|
221
|
+
same bus, memory, I/O, and interrupt infrastructure. It covers the complete
|
|
222
|
+
documented **and** undocumented instruction set — CB/DD/ED/FD/DDCB/FDCB prefixes,
|
|
223
|
+
the `IXH/IXL/IYH/IYL` and `SLL` opcodes, the DDCB result-copy variants, and the
|
|
224
|
+
undocumented X/Y flags (with MEMPTR/WZ modeling). Correctness is validated against
|
|
225
|
+
the standard **ZEXDOC** and **ZEXALL** exercisers.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { CpuZ80, Bus, Ram, InterruptController } from './src/index.js';
|
|
229
|
+
|
|
230
|
+
const pic = new InterruptController();
|
|
231
|
+
const bus = new Bus(pic);
|
|
232
|
+
bus.attachMemory(new Ram('ram', 0, 0x10000));
|
|
233
|
+
const cpu = new CpuZ80(bus, pic); // same (bus, pic) constructor as Cpu8080
|
|
234
|
+
cpu.step(); // returns T-states
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Both cores implement the lightweight `ICpu` interface (`step/reset/run/halted/pc`)
|
|
238
|
+
and share the `(bus, pic)` constructor, so `MachineRunner` drives either without
|
|
239
|
+
changes. `examples/boot-cpm.ts` selects the core via the `CPU` env var
|
|
240
|
+
(`CPU=z80 npm run boot:cpm`); stock Altair CP/M is 8080-compatible and boots on
|
|
241
|
+
the Z80 unchanged.
|
|
242
|
+
|
|
243
|
+
**Extras beyond the 8080:** shadow registers (`EXX`, `EX AF,AF'`), `IX/IY` index
|
|
244
|
+
registers with displacement, block transfer/search/I-O ops (`LDIR`, `CPIR`,
|
|
245
|
+
`OTIR`, …), interrupt modes `IM 0/1/2` with `IFF1/IFF2`, and non-maskable
|
|
246
|
+
interrupts (`cpu.triggerNMI()`). The flags byte layout is `S Z Y H X PV N C`.
|
|
247
|
+
|
|
248
|
+
### Z80 validation ROMs
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npm run fixtures:z80 # downloads prelim/zexdoc/zexall into tests/fixtures/
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`prelim.com` runs as part of the normal test suite (~1s). The exhaustive
|
|
255
|
+
`zexdoc`/`zexall` exercisers take minutes and are gated:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
Z80_ZEX=1 npx vitest run tests/integration/z80-zex.test.ts
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Clocks & CPU Speed
|
|
264
|
+
|
|
265
|
+
### `MachineRunner`
|
|
266
|
+
|
|
267
|
+
The recommended way to run a machine in real time. Drives `cpu.step()` at a configurable speed — an authentic **2 MHz by default** — pacing simulated T-states against wall time. Software written for the 8080 (delay loops, disk timing, serial pacing) behaves correctly at 2 MHz; `'max'` runs unthrottled.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { MachineRunner } from '8sim';
|
|
271
|
+
|
|
272
|
+
const runner = new MachineRunner(cpu); // 2 MHz (stock 8080)
|
|
273
|
+
const runner = new MachineRunner(cpu, { hz: 4_000_000 }); // 4 MHz
|
|
274
|
+
const runner = new MachineRunner(cpu, { hz: 'max' }); // unthrottled
|
|
275
|
+
|
|
276
|
+
runner.start();
|
|
277
|
+
runner.setHz('max'); // change speed while running
|
|
278
|
+
console.log(runner.effectiveHz); // measured speed, for status displays
|
|
279
|
+
runner.stop();
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Pacing details: the CPU runs in ~1 ms slices of simulated time against absolute wall-time accounting (scheduler jitter self-corrects). When ahead, the runner sleeps the difference; when behind, it catches up in bounded slices (max 20 ms) so host I/O stays responsive; a host stall longer than 250 ms (GC pause, suspended tab) is forfeited rather than replayed at full speed. Browser-portable (`performance.now` + `setTimeout`); Node callers can pass a `schedule` option using `setImmediate` for higher `'max'` throughput.
|
|
283
|
+
|
|
284
|
+
### `ImmediateClock`
|
|
285
|
+
|
|
286
|
+
Counts T-states with no wall-time throttling. Suitable for tests and maximum-speed emulation.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const clock = new ImmediateClock();
|
|
290
|
+
const cycles = cpu.step();
|
|
291
|
+
clock.addCycles(cycles);
|
|
292
|
+
console.log(clock.getElapsedCycles()); // bigint
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### `SystemClock`
|
|
296
|
+
|
|
297
|
+
The pacing engine used by `MachineRunner` — a T-state counter with a target frequency and absolute drift accounting (`getAheadMs()`, `setHz()`, `resync()`). Use it directly only if you're writing your own run loop.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Implementing Cards & Peripherals
|
|
302
|
+
|
|
303
|
+
### Custom Card
|
|
304
|
+
|
|
305
|
+
An `IS100Card` bundles one or more devices and wires them onto the bus in its `attach(bus)` method — this is how the built-in cards (88-2SIO, MIO, …) are structured.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import type { IS100Card } from '@joezilla/8sim';
|
|
309
|
+
import { Bus, Usart8251 } from '@joezilla/8sim';
|
|
310
|
+
|
|
311
|
+
class ConsoleCard implements IS100Card {
|
|
312
|
+
readonly id = 'console';
|
|
313
|
+
private uart = new Usart8251('uart', 0x10, 0x11);
|
|
314
|
+
|
|
315
|
+
attach(bus: Bus): void { bus.attachIODevice(this.uart); } // claims ports 0x10/0x11
|
|
316
|
+
reset(): void { this.uart.reset(); }
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
A card can just as well claim a memory region (attach a `Ram`/`Rom`/`MemoryMappedIOAdapter`) or combine both. The devices it attaches implement the interfaces below.
|
|
321
|
+
|
|
322
|
+
### Custom I/O Device
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import type { IIODevice } from './src/interfaces/IIODevice.js';
|
|
326
|
+
|
|
327
|
+
class Timer implements IIODevice {
|
|
328
|
+
readonly id = 'timer';
|
|
329
|
+
readonly basePorts = [0x40, 0x41, 0x42, 0x43]; // 8253-style ports
|
|
330
|
+
|
|
331
|
+
ioRead(port: number): number {
|
|
332
|
+
// return timer counter byte for this port
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
ioWrite(port: number, value: number): void {
|
|
337
|
+
// configure timer
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
reset(): void { /* ... */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
bus.attachIODevice(new Timer());
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Custom Memory
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import type { IMemory } from './src/interfaces/IMemory.js';
|
|
350
|
+
|
|
351
|
+
class BankedRam implements IMemory {
|
|
352
|
+
readonly id = 'banked-ram';
|
|
353
|
+
readonly baseAddress = 0x4000;
|
|
354
|
+
readonly size = 0x4000;
|
|
355
|
+
readonly readOnly = false;
|
|
356
|
+
private banks: Uint8Array[] = [new Uint8Array(0x4000), new Uint8Array(0x4000)];
|
|
357
|
+
private activeBank = 0;
|
|
358
|
+
|
|
359
|
+
read(offset: number): number { return this.banks[this.activeBank]![offset] ?? 0xff; }
|
|
360
|
+
write(offset: number, value: number): void { this.banks[this.activeBank]![offset] = value & 0xff; }
|
|
361
|
+
reset(): void { this.banks.forEach(b => b.fill(0)); this.activeBank = 0; }
|
|
362
|
+
|
|
363
|
+
selectBank(n: number): void { this.activeBank = n; }
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Card Bundles & Machine Assembly
|
|
370
|
+
|
|
371
|
+
The Quick Start above wires a machine by hand. For declarative assembly, describe
|
|
372
|
+
the machine as data (a `MachineSpec`) and let `buildMachine` construct it — memory
|
|
373
|
+
regions attached in order, then cards, then the CPU. This is the layer host
|
|
374
|
+
applications (e.g. a machine-profile builder) target.
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import { buildMachine } from '@joezilla/8sim';
|
|
378
|
+
|
|
379
|
+
const machine = buildMachine({
|
|
380
|
+
cpuKind: 'i8080', // or 'z80'
|
|
381
|
+
clock: 'max', // or { hz: 2_000_000 }
|
|
382
|
+
resetVector: 0xff00, // program counter at power-on
|
|
383
|
+
memory: [
|
|
384
|
+
{ id: 'ram', base: 0x0000, size: 0xff00, kind: 'ram' },
|
|
385
|
+
{ id: 'boot', base: 0xff00, size: rom.length, kind: 'rom', image: rom },
|
|
386
|
+
],
|
|
387
|
+
cards: [/* CardSpec[] — see bundles below */],
|
|
388
|
+
});
|
|
389
|
+
machine.cpu.step();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Overlapping memory regions and colliding I/O ports are rejected at build time
|
|
393
|
+
(`MachineSpecError`) rather than silently last-writer-wins.
|
|
394
|
+
|
|
395
|
+
### Seed card bundles
|
|
396
|
+
|
|
397
|
+
An S-100 machine is assembled from **cards**. A `CardBundle` packages a card's
|
|
398
|
+
manifest (its configurable surface), a uniform `cardFactory`, and — depending on
|
|
399
|
+
the card — one of three **resolution outputs**:
|
|
400
|
+
|
|
401
|
+
| Output | Declared by | Cards |
|
|
402
|
+
| --- | --- | --- |
|
|
403
|
+
| **I/O device** | `claims(config)` → ports/IRQ | `imsai-sio2`, `mits-88-2sio`, `imsai-mio`, `mits-88-dcdd` (+ chip-level `intel-8251`, `motorola-6850`, `intel-8212`, `tr1602-uart`) |
|
|
404
|
+
| **Memory region** | `memory(config)` → `MemoryRegionSpec[]` | `ram-card`, `eprom-card` |
|
|
405
|
+
| **CPU** | `cpu(config)` → `{ kind, resetVector? }` | `i8080-cpu`, `z80-cpu` |
|
|
406
|
+
|
|
407
|
+
On real S-100 hardware the processor and memory are boards, so here they're cards
|
|
408
|
+
too: a **RAM/EPROM card** resolves to a memory region (hoisted into the machine's
|
|
409
|
+
memory map), and a **CPU card** is the bus master that sets the machine's CPU and
|
|
410
|
+
power-on jump. Memory and CPU cards are no-ops on the bus itself.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { seedBundles, seedBundleByName, withDefaults } from '@joezilla/8sim';
|
|
414
|
+
|
|
415
|
+
const cpu = seedBundleByName('z80-cpu')!;
|
|
416
|
+
const cfg = withDefaults(cpu.manifest, { resetVector: 0xf800 }); // validates against the schema
|
|
417
|
+
const resolved = cpu.cpu!(cfg); // → { kind: 'z80', resetVector: 0xf800 }
|
|
418
|
+
const region = seedBundleByName('ram-card')!.memory!(withDefaults(/* ... */)); // → MemoryRegionSpec[]
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`seedBundles` is the full registry; each bundle's `manifest.configSchema` drives
|
|
422
|
+
config validation (`withDefaults` throws `CardConfigError` on an out-of-range
|
|
423
|
+
value), and `manifest.kind` distinguishes a real S-100 `card` from a component
|
|
424
|
+
`chip`.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Integration Testing: cpudiag.com
|
|
429
|
+
|
|
430
|
+
The repository includes a CP/M CPU diagnostic integration test. To run it:
|
|
431
|
+
|
|
432
|
+
1. Obtain `cpudiag.com` (part of the public-domain `cpm2.asm` diagnostics suite)
|
|
433
|
+
2. Place it at `tests/fixtures/cpudiag.com`
|
|
434
|
+
3. Run `npm test`
|
|
435
|
+
|
|
436
|
+
The test loads the binary at `0x0100`, stubs the CP/M BDOS entry point at `0x0005` to capture console output, and expects the output to contain `CPU IS OPERATIONAL` before the CPU halts.
|
|
437
|
+
|
|
438
|
+
If the fixture is absent the test is silently skipped.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Examples: Interactive CP/M Boot
|
|
443
|
+
|
|
444
|
+
`examples/boot-cpm.ts` boots CP/M on the emulated 8080 (88-DSK boot PROM + 8251 console + MITS 88-DCDD floppy controller) against a live fdcplus-web server, bridging the console to your terminal for a real interactive session.
|
|
445
|
+
|
|
446
|
+
The same session runs on the **Z80** core two ways: set `CPU=z80 npm run boot:cpm`, or use the dedicated `examples/boot-cpm-z80.ts` via `npm run boot:cpm:z80`. Stock Altair CP/M is 8080-compatible, so it boots on the Z80 unchanged.
|
|
447
|
+
|
|
448
|
+
The server location and API token are **not** hardcoded — they are read from environment variables, loaded from a local `.env` file:
|
|
449
|
+
|
|
450
|
+
| Variable | Description | Default |
|
|
451
|
+
|---|---|---|
|
|
452
|
+
| `FDCPLUS_URL` | Base URL of the fdcplus-web server | `http://localhost:3000` |
|
|
453
|
+
| `FDCPLUS_TOKEN` | API token for the fdcplus-web server | *(required)* |
|
|
454
|
+
| `FDCPLUS_CLIENT_ID` | Stable client id for persistent disk writes | *(unset — writes are ephemeral)* |
|
|
455
|
+
| `CPU_HZ` | CPU speed: `2mhz`, `4mhz`, a raw Hz number, or `max` | `2mhz` |
|
|
456
|
+
|
|
457
|
+
Note on disk writes: fdcplus-web sessions are copy-on-write — writes go to a per-client "splinter", not the master disk image. Without `FDCPLUS_CLIENT_ID`, the splinter (and any files you saved) is discarded when the emulator disconnects. With a stable id, your changes persist across sessions; merge them into the master image via the server's `POST /api/drives/:id/transient/commit` endpoint.
|
|
458
|
+
|
|
459
|
+
Setup:
|
|
460
|
+
|
|
461
|
+
1. Copy the template: `cp .env.example .env`
|
|
462
|
+
2. Edit `.env` and set `FDCPLUS_TOKEN` (and `FDCPLUS_URL` if the server isn't on localhost)
|
|
463
|
+
3. Start the fdcplus-web server with a bootable disk mounted on drive 0
|
|
464
|
+
4. Run `npm run boot:cpm`
|
|
465
|
+
|
|
466
|
+
The `.env` file is gitignored so your token is never committed. The npm script loads it via Node's built-in `--env-file-if-exists` flag (requires Node ≥ 22), so no dotenv dependency is needed. Type at the `A>` prompt; press `Ctrl-]` to quit. Disk writes are flushed back to the mounted image on the server.
|
|
467
|
+
|
|
468
|
+
The live boot integration test (`tests/integration/bootdisk.live.test.ts`) uses the same variables; the vitest config loads `.env` automatically. The test skips itself when `FDCPLUS_TOKEN` is unset or the server is unreachable.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Browser Usage
|
|
473
|
+
|
|
474
|
+
Build a single-file ESM bundle:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npm run build
|
|
478
|
+
# produces dist/8sim.browser.js
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
```html
|
|
482
|
+
<script type="module">
|
|
483
|
+
import { Cpu8080, Bus, Ram, InterruptController } from './dist/8sim.browser.js';
|
|
484
|
+
|
|
485
|
+
const pic = new InterruptController();
|
|
486
|
+
const bus = new Bus(pic);
|
|
487
|
+
const ram = new Ram('ram', 0, 0x10000);
|
|
488
|
+
bus.attachMemory(ram);
|
|
489
|
+
|
|
490
|
+
const response = await fetch('myprog.bin');
|
|
491
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
492
|
+
ram.load(data, 0x0100);
|
|
493
|
+
|
|
494
|
+
const cpu = new Cpu8080(bus, pic);
|
|
495
|
+
cpu.registers.pc = 0x0100;
|
|
496
|
+
cpu.registers.sp = 0x0100;
|
|
497
|
+
cpu.run(1_000_000);
|
|
498
|
+
</script>
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Project Structure
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
src/
|
|
507
|
+
├── cpu/
|
|
508
|
+
│ ├── Cpu8080.ts — Intel 8080 CPU: step(), run(), interrupt handling
|
|
509
|
+
│ ├── Registers.ts — A B C D E H L SP PC, pair accessors
|
|
510
|
+
│ ├── Flags.ts — S Z AC P CY, PSW byte serialize/deserialize
|
|
511
|
+
│ ├── Decoder.ts — 256-entry InstructionHandler dispatch table
|
|
512
|
+
│ ├── instructions/ — one file per 8080 instruction group
|
|
513
|
+
│ └── z80/ — Zilog Z80 core
|
|
514
|
+
│ ├── CpuZ80.ts — step loop, DD/FD prefixes, IM 0/1/2 + NMI
|
|
515
|
+
│ ├── RegistersZ80.ts — main + shadow + IX/IY/I/R/WZ
|
|
516
|
+
│ ├── FlagsZ80.ts — S Z Y H X PV N C
|
|
517
|
+
│ ├── DecoderZ80.ts — main/IX/IY + CB/ED/DDCB tables
|
|
518
|
+
│ ├── flagHelpers.ts — shared flag math (X/Y aware)
|
|
519
|
+
│ └── instructions/ — one file per Z80 instruction group
|
|
520
|
+
├── bus/ — Bus (backplane), BusRegion, SnoopBus (tracing tap)
|
|
521
|
+
├── memory/ — Ram, Rom, MemoryMappedIOAdapter
|
|
522
|
+
├── io/ — IoSpace
|
|
523
|
+
├── interrupt/ — InterruptController
|
|
524
|
+
├── clock/ — ImmediateClock, SystemClock
|
|
525
|
+
├── machine/ — buildMachine, MachineSpec, MachineRunner
|
|
526
|
+
├── cards/ — S-100 card classes (88-2SIO, SIO-2, MIO, 88-DCDD, 8251, 6850, 8212, TR1602)
|
|
527
|
+
├── bundles/ — CardBundle + seed/ registry (CPU, RAM, EPROM, serial, floppy)
|
|
528
|
+
├── interfaces/ — ICpu IBus IMemory IIODevice IInterruptController IClock IModule IS100Card IBusObserver
|
|
529
|
+
└── util/bits.ts — u8 u16 signBit zeroFlag parityFlag auxCarryAdd toWord hi lo
|
|
530
|
+
tests/
|
|
531
|
+
├── cpu/ — per-instruction-group unit tests + interrupt tests
|
|
532
|
+
│ └── z80/ — Z80 registers, flags, CB/indexed/DDCB, interrupts
|
|
533
|
+
├── bus/ — bus routing, ROM protection, MMIO
|
|
534
|
+
├── memory/ — RAM, ROM, MemoryMappedIOAdapter
|
|
535
|
+
└── integration/ — cpudiag.com + Z80 prelim/zexdoc/zexall end-to-end tests
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## License
|
|
541
|
+
|
|
542
|
+
Apache License 2.0 — see [LICENSE](LICENSE) for details.
|