@fugloy/emu 0.0.0-stage → 0.2.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 +21 -0
- package/NOTICE.md +27 -0
- package/README.md +119 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3343 -0
- package/src/debug/debugger.js +777 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +423 -0
- package/src/debug/state.js +214 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +259 -0
- package/src/devices/disk-image.js +578 -0
- package/src/devices/fdc-wd1793.js +466 -0
- package/src/devices/key-queue.js +261 -0
- package/src/devices/keyboard.js +176 -0
- package/src/devices/printer.js +44 -0
- package/src/devices/sound.js +53 -0
- package/src/devices/video.js +66 -0
- package/src/index.js +22 -0
- package/src/loaders/bas.js +107 -0
- package/src/loaders/cas-format.js +213 -0
- package/src/loaders/cmd-format.js +88 -0
- package/src/loaders/handoff.js +29 -0
- package/src/loaders/load.js +135 -0
- package/src/machine/io.js +290 -0
- package/src/machine/machine.js +295 -0
- package/src/machine/memory.js +207 -0
- package/src/machine/rom-hashes.js +14 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +232 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III Memory Management System
|
|
3
|
+
*
|
|
4
|
+
* Real Model III address map (non-disk machine):
|
|
5
|
+
* 0x0000-0x37FF 14K ROM (write-protected)
|
|
6
|
+
* 0x3800-0x3BFF keyboard matrix (memory-mapped, read-only;
|
|
7
|
+
* low 8 address bits select rows, A8/A9 undecoded)
|
|
8
|
+
* 0x3C00-0x3FFF 1K video RAM (64 cols x 16 rows)
|
|
9
|
+
* 0x4000-0xFFFF 48K RAM
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ROM_LIMIT = 0x3800;
|
|
13
|
+
const KEYBOARD_LIMIT = 0x3c00;
|
|
14
|
+
const RAM_BASE = 0x4000;
|
|
15
|
+
|
|
16
|
+
export class MemorySystem {
|
|
17
|
+
constructor() {
|
|
18
|
+
// ROM storage is 16K so padded 16K images can be loaded verbatim,
|
|
19
|
+
// but only 0x0000-0x37FF is ever mapped as ROM.
|
|
20
|
+
this.rom = new Uint8Array(0x4000);
|
|
21
|
+
|
|
22
|
+
// 48K RAM (0x4000 - 0xFFFF)
|
|
23
|
+
this.ram = new Uint8Array(0xc000);
|
|
24
|
+
|
|
25
|
+
// 1K video RAM (0x3C00 - 0x3FFF)
|
|
26
|
+
this.videoRam = new Uint8Array(0x400);
|
|
27
|
+
|
|
28
|
+
// Set on every video RAM write so the renderer can skip idle frames
|
|
29
|
+
this.videoDirty = false;
|
|
30
|
+
|
|
31
|
+
// Keyboard matrix peripheral: { read(addrLow8) -> row bits } or null
|
|
32
|
+
this.keyboard = null;
|
|
33
|
+
|
|
34
|
+
this.romLoaded = false;
|
|
35
|
+
|
|
36
|
+
// Kept for API compatibility with existing callers
|
|
37
|
+
this.VIDEO_RAM_START = 0x3c00;
|
|
38
|
+
this.VIDEO_RAM_END = 0x3fff;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get memory statistics
|
|
43
|
+
*/
|
|
44
|
+
getStats() {
|
|
45
|
+
return {
|
|
46
|
+
romSize: this.rom.length,
|
|
47
|
+
ramSize: this.ram.length,
|
|
48
|
+
totalSize: this.rom.length + this.ram.length,
|
|
49
|
+
romLoaded: this.romLoaded,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load a ROM image: the real 14K (0x3800) Model III ROM, or a 16K
|
|
55
|
+
* (0x4000) padded image of it. Anything else is a wrong file — reject
|
|
56
|
+
* it rather than guessing.
|
|
57
|
+
*/
|
|
58
|
+
loadROM(romData) {
|
|
59
|
+
if (!romData || romData.length === 0) {
|
|
60
|
+
throw new Error("ROM data is required");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (romData.length !== 0x3800 && romData.length !== 0x4000) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid ROM size: ${romData.length} bytes. Expected 14KB (0x3800) or a 16KB (0x4000) padded image`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.rom.fill(0);
|
|
70
|
+
this.rom.set(romData, 0);
|
|
71
|
+
|
|
72
|
+
this.romLoaded = true;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read a byte from memory
|
|
78
|
+
* Addresses are masked to 16 bits (0x0000 - 0xFFFF)
|
|
79
|
+
*/
|
|
80
|
+
readByte(address) {
|
|
81
|
+
address = address & 0xffff;
|
|
82
|
+
|
|
83
|
+
if (address < ROM_LIMIT) {
|
|
84
|
+
return this.rom[address];
|
|
85
|
+
}
|
|
86
|
+
if (address < KEYBOARD_LIMIT) {
|
|
87
|
+
// Keyboard matrix: reading with no keys down yields 0x00
|
|
88
|
+
return this.keyboard ? this.keyboard.read(address & 0xff) : 0x00;
|
|
89
|
+
}
|
|
90
|
+
if (address < RAM_BASE) {
|
|
91
|
+
return this.videoRam[address - KEYBOARD_LIMIT];
|
|
92
|
+
}
|
|
93
|
+
return this.ram[address - RAM_BASE];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write a byte to memory
|
|
98
|
+
* ROM and the keyboard region ignore writes; video RAM and RAM accept them.
|
|
99
|
+
*/
|
|
100
|
+
writeByte(address, value) {
|
|
101
|
+
address = address & 0xffff;
|
|
102
|
+
value = value & 0xff;
|
|
103
|
+
|
|
104
|
+
if (address < KEYBOARD_LIMIT) {
|
|
105
|
+
return; // ROM / keyboard matrix: not writable
|
|
106
|
+
}
|
|
107
|
+
if (address < RAM_BASE) {
|
|
108
|
+
this.videoRam[address - KEYBOARD_LIMIT] = value;
|
|
109
|
+
this.videoDirty = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.ram[address - RAM_BASE] = value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read a 16-bit word (little-endian)
|
|
117
|
+
*/
|
|
118
|
+
readWord(address) {
|
|
119
|
+
address = address & 0xffff;
|
|
120
|
+
const low = this.readByte(address);
|
|
121
|
+
const high = this.readByte((address + 1) & 0xffff);
|
|
122
|
+
return (high << 8) | low;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Write a 16-bit word (little-endian)
|
|
127
|
+
*/
|
|
128
|
+
writeWord(address, value) {
|
|
129
|
+
address = address & 0xffff;
|
|
130
|
+
value = value & 0xffff;
|
|
131
|
+
const low = value & 0xff;
|
|
132
|
+
const high = (value >> 8) & 0xff;
|
|
133
|
+
this.writeByte(address, low);
|
|
134
|
+
this.writeByte((address + 1) & 0xffff, high);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load a program into RAM
|
|
139
|
+
* @param {Uint8Array|Array} program - Program data
|
|
140
|
+
* @param {number} address - Load address (default: 0x4200)
|
|
141
|
+
* @returns {number} The address where the program was loaded
|
|
142
|
+
*/
|
|
143
|
+
loadProgram(program, address = 0x4200) {
|
|
144
|
+
if (!program || program.length === 0) {
|
|
145
|
+
return address;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Convert to Uint8Array if needed
|
|
149
|
+
let programData;
|
|
150
|
+
if (program instanceof Uint8Array) {
|
|
151
|
+
programData = program;
|
|
152
|
+
} else if (Array.isArray(program)) {
|
|
153
|
+
programData = new Uint8Array(program);
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error("Program must be Uint8Array or Array");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
address = address & 0xffff;
|
|
159
|
+
|
|
160
|
+
// Check if program fits in memory
|
|
161
|
+
if (address < RAM_BASE) {
|
|
162
|
+
throw new Error("Cannot load program into ROM area");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const endAddress = (address + programData.length - 1) & 0xffff;
|
|
166
|
+
if (endAddress < address) {
|
|
167
|
+
// Wrapped around - program too large
|
|
168
|
+
throw new Error("Program exceeds available memory");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Load program into RAM
|
|
172
|
+
for (let i = 0; i < programData.length; i++) {
|
|
173
|
+
const addr = (address + i) & 0xffff;
|
|
174
|
+
this.writeByte(addr, programData[i]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return address;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clear all RAM (set to 0x00)
|
|
182
|
+
*/
|
|
183
|
+
clearRAM() {
|
|
184
|
+
this.ram.fill(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Clear video RAM to spaces (what the ROM's CLS leaves behind)
|
|
189
|
+
*/
|
|
190
|
+
clearVideoRAM() {
|
|
191
|
+
this.videoRam.fill(0x20);
|
|
192
|
+
this.videoDirty = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get a view of the entire memory space (for debugging)
|
|
197
|
+
* Returns a Uint8Array representing the full 64K address space
|
|
198
|
+
*/
|
|
199
|
+
getMemoryView() {
|
|
200
|
+
const view = new Uint8Array(0x10000);
|
|
201
|
+
view.set(this.rom.subarray(0, ROM_LIMIT), 0);
|
|
202
|
+
// Keyboard region reads as idle (0x00) in the debug view
|
|
203
|
+
view.set(this.videoRam, KEYBOARD_LIMIT);
|
|
204
|
+
view.set(this.ram, RAM_BASE);
|
|
205
|
+
return view;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known Model III ROM images by SHA-256 (lowercase hex), so the Machine can report which ROM it
|
|
3
|
+
* was given (spec §5: the core validates by length and a known-hash table). Hashes are facts
|
|
4
|
+
* about the images; the images themselves are never committed or bundled.
|
|
5
|
+
*
|
|
6
|
+
* Source for both: github.com/kiwisincebirth/TRS-80-ROMS, folder test-roms/, hashed locally
|
|
7
|
+
* 2026-10-17. `Model3-RevC-2EF8.bin` is the stock Model III Rev C ROM (14,336 bytes) and the
|
|
8
|
+
* image the repository's fugloy.mcp.json names as roms.model3; `Model3-RevC-FreHD.bin` is the
|
|
9
|
+
* same ROM patched to boot a FreHD hard-disk emulator.
|
|
10
|
+
*/
|
|
11
|
+
export const ROM_HASHES = Object.freeze({
|
|
12
|
+
beeb3741175a8b8706cc384d5b2534ebf32e383662864054e5a0ce4d861a43bb: 'model3-rev-c-2ef8',
|
|
13
|
+
ad6de30c6aece2d6a2243ee90a2959295d5c46b82650b27f46f1ecf8b34b2361: 'model3-rev-c-frehd',
|
|
14
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 (FIPS 180-4) in plain JavaScript, because the core may use neither Node's crypto nor
|
|
3
|
+
* the browser's asynchronous Web Crypto. A copy of @fugloy/disasm's `src/hash.ts` (emu must not
|
|
4
|
+
* depend on disasm); identifies ROM images against ROM_HASHES.
|
|
5
|
+
*/
|
|
6
|
+
const K = new Uint32Array([
|
|
7
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
8
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
9
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
10
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
11
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
12
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
13
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
14
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
18
|
+
|
|
19
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Uint8Array} bytes
|
|
23
|
+
* @returns {Uint8Array} the 32-byte digest
|
|
24
|
+
*/
|
|
25
|
+
export function sha256(bytes) {
|
|
26
|
+
const len = bytes.length;
|
|
27
|
+
const padded = new Uint8Array(((len + 9 + 63) >> 6) << 6);
|
|
28
|
+
padded.set(bytes);
|
|
29
|
+
padded[len] = 0x80;
|
|
30
|
+
const view = new DataView(padded.buffer);
|
|
31
|
+
const bits = len * 8;
|
|
32
|
+
view.setUint32(padded.length - 8, Math.floor(bits / 0x100000000));
|
|
33
|
+
view.setUint32(padded.length - 4, bits >>> 0);
|
|
34
|
+
|
|
35
|
+
const h = Uint32Array.from(H0);
|
|
36
|
+
const w = new Uint32Array(64);
|
|
37
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
38
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
|
|
39
|
+
for (let i = 16; i < 64; i++) {
|
|
40
|
+
const w15 = w[i - 15];
|
|
41
|
+
const w2 = w[i - 2];
|
|
42
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
43
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
44
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
45
|
+
}
|
|
46
|
+
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
|
|
47
|
+
for (let i = 0; i < 64; i++) {
|
|
48
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
49
|
+
const ch = (e & f) ^ (~e & g);
|
|
50
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
|
|
51
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
52
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
53
|
+
const t2 = (S0 + maj) >>> 0;
|
|
54
|
+
hh = g; g = f; f = e;
|
|
55
|
+
e = (d + t1) >>> 0;
|
|
56
|
+
d = c; c = b; b = a;
|
|
57
|
+
a = (t1 + t2) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
h[0] = (h[0] + a) >>> 0; h[1] = (h[1] + b) >>> 0; h[2] = (h[2] + c) >>> 0; h[3] = (h[3] + d) >>> 0;
|
|
60
|
+
h[4] = (h[4] + e) >>> 0; h[5] = (h[5] + f) >>> 0; h[6] = (h[6] + g) >>> 0; h[7] = (h[7] + hh) >>> 0;
|
|
61
|
+
}
|
|
62
|
+
const out = new Uint8Array(32);
|
|
63
|
+
const outView = new DataView(out.buffer);
|
|
64
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i]);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {Uint8Array} bytes
|
|
70
|
+
* @returns {string} the digest as 64 lowercase hex digits
|
|
71
|
+
*/
|
|
72
|
+
export function sha256Hex(bytes) {
|
|
73
|
+
return Array.from(sha256(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
74
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III System
|
|
3
|
+
*
|
|
4
|
+
* Owns and wires the machine: Z80 CPU, memory map, I/O ports, and the
|
|
5
|
+
* keyboard matrix. Runs the CPU in T-state-budgeted slices at the real
|
|
6
|
+
* 2.02752 MHz clock and raises the 30 Hz RTC heartbeat interrupt the
|
|
7
|
+
* ROM's clock/cursor services depend on.
|
|
8
|
+
*
|
|
9
|
+
* Rendering is the caller's concern: read `memory.videoRam` (or
|
|
10
|
+
* `screenText()`) and consult `memory.videoDirty`. Keyboard input goes
|
|
11
|
+
* through `system.keyboard` (press and release keys directly).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Z80CPU } from "../cpu/z80cpu.js";
|
|
15
|
+
import { MemorySystem } from "./memory.js";
|
|
16
|
+
import { IOSystem, MODE_32COL_BIT } from "./io.js";
|
|
17
|
+
import { KeyboardMatrix } from "../devices/keyboard.js";
|
|
18
|
+
|
|
19
|
+
export const CPU_CLOCK_HZ = 2027520;
|
|
20
|
+
export const RTC_HZ = 30;
|
|
21
|
+
export const RTC_INTERVAL_TSTATES = CPU_CLOCK_HZ / RTC_HZ; // 67,584
|
|
22
|
+
|
|
23
|
+
export class TRS80System {
|
|
24
|
+
constructor({ romData, log }) {
|
|
25
|
+
this.memory = new MemorySystem();
|
|
26
|
+
this.io = new IOSystem();
|
|
27
|
+
this.keyboard = new KeyboardMatrix();
|
|
28
|
+
this.cpu = new Z80CPU();
|
|
29
|
+
if (log !== undefined) {
|
|
30
|
+
this.cpu.log = log;
|
|
31
|
+
this.io.cassette.log = log;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.memory.keyboard = this.keyboard;
|
|
35
|
+
|
|
36
|
+
this.cpu.readMemory = (addr) => this.memory.readByte(addr);
|
|
37
|
+
this.cpu.writeMemory = (addr, value) => this.memory.writeByte(addr, value);
|
|
38
|
+
this.cpu.readPort = (port) => this.io.readPort(port);
|
|
39
|
+
this.cpu.writePort = (port, value) => this.io.writePort(port, value);
|
|
40
|
+
|
|
41
|
+
// A mode-register change (e.g. the ROM handling CHR$(23)) redraws
|
|
42
|
+
// the same video RAM in a different layout — force a repaint.
|
|
43
|
+
this.io.onModeWrite = () => {
|
|
44
|
+
this.memory.videoDirty = true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Cassette-out sound transitions are stamped in CPU T-states
|
|
48
|
+
this.io.getCycles = () => this.cpu.cycles;
|
|
49
|
+
|
|
50
|
+
this.memory.loadROM(romData);
|
|
51
|
+
|
|
52
|
+
this.nextRtcAt = RTC_INTERVAL_TSTATES;
|
|
53
|
+
this.reset();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hardware reset (the orange button): CPU restarts at 0x0000, interrupt
|
|
58
|
+
* latches clear, screen blanks. RAM contents survive, as on the real
|
|
59
|
+
* machine.
|
|
60
|
+
*/
|
|
61
|
+
reset() {
|
|
62
|
+
this.cpu.reset();
|
|
63
|
+
this.io.reset();
|
|
64
|
+
this.keyboard.reset();
|
|
65
|
+
this.memory.clearVideoRAM();
|
|
66
|
+
this.nextRtcAt = this.cpu.cycles + RTC_INTERVAL_TSTATES;
|
|
67
|
+
this._nmiWasPending = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute instructions until `budget` T-states have elapsed, raising
|
|
72
|
+
* the RTC heartbeat on schedule and delivering pending interrupts
|
|
73
|
+
* between instructions. Returns the T-states actually consumed (the
|
|
74
|
+
* final instruction may overshoot slightly).
|
|
75
|
+
*/
|
|
76
|
+
runTStates(budget) {
|
|
77
|
+
const start = this.cpu.cycles;
|
|
78
|
+
const target = start + budget;
|
|
79
|
+
|
|
80
|
+
while (this.cpu.cycles < target) {
|
|
81
|
+
this.stepInstruction();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.cpu.cycles - start;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One instruction boundary, the only place the machine advances: raise
|
|
89
|
+
* the RTC heartbeat when its T-state is due, deliver a pending
|
|
90
|
+
* interrupt or NMI edge, then call `beforeExecute` (if given) and
|
|
91
|
+
* execute one instruction, ticking the disk controller by its cost.
|
|
92
|
+
* `runTStates` and the Machine facade both step through here, so the
|
|
93
|
+
* RTC keeps one schedule however the machine is driven. Returns the
|
|
94
|
+
* T-states consumed, an interrupt response included.
|
|
95
|
+
* @param {() => boolean | 'stop' | void} [beforeExecute] called after interrupt delivery, with PC at the instruction about to run; returning true means it stood in for that instruction, which then does not execute; returning 'stop' means no instruction executes at all (a debugger break)
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
stepInstruction(beforeExecute) {
|
|
99
|
+
const start = this.cpu.cycles;
|
|
100
|
+
if (this.cpu.cycles >= this.nextRtcAt) {
|
|
101
|
+
this.io.raiseRTC();
|
|
102
|
+
this.nextRtcAt += RTC_INTERVAL_TSTATES;
|
|
103
|
+
}
|
|
104
|
+
if (this.io.pendingInterrupt()) {
|
|
105
|
+
this.cpu.interrupt();
|
|
106
|
+
}
|
|
107
|
+
// FDC INTRQ -> NMI, edge-triggered so one assertion fires one NMI
|
|
108
|
+
const nmiNow = this.io.nmiPending();
|
|
109
|
+
if (nmiNow && !this._nmiWasPending) {
|
|
110
|
+
this.cpu.nmi();
|
|
111
|
+
}
|
|
112
|
+
this._nmiWasPending = nmiNow;
|
|
113
|
+
|
|
114
|
+
// beforeExecute returning true has executed the boundary's instruction itself (the
|
|
115
|
+
// Machine's ROM keyboard-scan hook stands in for a RET), so the CPU does not run one.
|
|
116
|
+
// Returning 'stop' runs nothing and the disk controller does not tick: like the normal path,
|
|
117
|
+
// it never ticks for an interrupt response's T-states. The return value is those T-states.
|
|
118
|
+
const before = this.cpu.cycles;
|
|
119
|
+
if (beforeExecute !== undefined) {
|
|
120
|
+
const r = beforeExecute();
|
|
121
|
+
if (r === 'stop') {
|
|
122
|
+
return before - start;
|
|
123
|
+
}
|
|
124
|
+
if (r !== true) this.cpu.executeInstruction();
|
|
125
|
+
} else {
|
|
126
|
+
this.cpu.executeInstruction();
|
|
127
|
+
}
|
|
128
|
+
this.io.fdc.tick(this.cpu.cycles - before);
|
|
129
|
+
return this.cpu.cycles - start;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* True when the machine is in 32-character mode (mode register bit 2,
|
|
134
|
+
* set by the ROM for CHR$(23), cleared by CLS). The ROM lays 32-column
|
|
135
|
+
* text at even video addresses; hardware shows them double-wide.
|
|
136
|
+
*/
|
|
137
|
+
get columns32() {
|
|
138
|
+
return (this.io.modeRegister & MODE_32COL_BIT) !== 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Mount a .dsk image (DiskImage) in drive 0-3. */
|
|
142
|
+
mountDisk(driveNumber, image) {
|
|
143
|
+
this.io.fdc.attachDrive(driveNumber, image);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Eject every drive and reboot into cassette BASIC, answering the
|
|
148
|
+
* ROM's Cass? and Memory Size? prompts. Cassette-world loaders (the
|
|
149
|
+
* library, .cas tapes, paste-BASIC) need the machine at READY — under
|
|
150
|
+
* a booted DOS their typed RUN goes to the DOS prompt instead. Runs
|
|
151
|
+
* ~3.5 emulated seconds inline, a blink of wall clock.
|
|
152
|
+
* @returns {boolean} true when the READY prompt was reached
|
|
153
|
+
*/
|
|
154
|
+
bootToCassetteBasic() {
|
|
155
|
+
for (let drive = 0; drive < 4; drive++) {
|
|
156
|
+
this.ejectDisk(drive);
|
|
157
|
+
}
|
|
158
|
+
this.reset();
|
|
159
|
+
const pressEnter = () => {
|
|
160
|
+
this.keyboard.keyDown("Enter", "bootdance");
|
|
161
|
+
this.runTStates(200000);
|
|
162
|
+
this.keyboard.keyUp("bootdance");
|
|
163
|
+
this.runTStates(100000);
|
|
164
|
+
};
|
|
165
|
+
this.runSeconds(1);
|
|
166
|
+
pressEnter();
|
|
167
|
+
this.runSeconds(0.5);
|
|
168
|
+
pressEnter();
|
|
169
|
+
this.runSeconds(2);
|
|
170
|
+
return this.screenText().join("\n").includes("READY");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ejectDisk(driveNumber) {
|
|
174
|
+
this.io.fdc.ejectDrive(driveNumber);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Convenience for tests: run N emulated seconds. */
|
|
178
|
+
runSeconds(seconds) {
|
|
179
|
+
return this.runTStates(Math.round(seconds * CPU_CLOCK_HZ));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Type text through the keyboard matrix at emulated speed ("turbo
|
|
184
|
+
* paste"): each key is held long enough for the ROM's scan/debounce,
|
|
185
|
+
* but the wall-clock cost is only the emulation time (~3 ms/char).
|
|
186
|
+
* '\n' presses ENTER. Returns the number of characters that had no
|
|
187
|
+
* matrix mapping and were skipped.
|
|
188
|
+
*
|
|
189
|
+
* After ENTER the ROM tokenizes and stores the line (cost grows with
|
|
190
|
+
* program size) before it scans the keyboard again — give it room or
|
|
191
|
+
* the first character of the next line gets eaten. The default suits
|
|
192
|
+
* the small built-in classics; pass a larger `enterTStates` for
|
|
193
|
+
* multi-KB listings (a 5.7 KB program needs ~1M).
|
|
194
|
+
*/
|
|
195
|
+
typeText(text, { enterTStates = 400000 } = {}) {
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
let seq = 0;
|
|
198
|
+
for (const ch of text) {
|
|
199
|
+
const key = ch === "\n" ? "Enter" : ch;
|
|
200
|
+
const code = `typeText:${seq++}`;
|
|
201
|
+
if (!this.keyboard.keyDown(key, code)) {
|
|
202
|
+
skipped++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
this.runTStates(70000); // held across several scans + debounce
|
|
206
|
+
this.keyboard.keyUp(code);
|
|
207
|
+
this.runTStates(ch === "\n" ? enterTStates : 50000);
|
|
208
|
+
}
|
|
209
|
+
return skipped;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The 64x16 screen as strings, for tests and status displays.
|
|
214
|
+
* Control bytes 0x00-0x1F display as their 0x40-0x5F fold (Model III
|
|
215
|
+
* character generator); block-graphics bytes render as '.'.
|
|
216
|
+
*/
|
|
217
|
+
screenText() {
|
|
218
|
+
const rows = [];
|
|
219
|
+
for (let row = 0; row < 16; row++) {
|
|
220
|
+
let line = "";
|
|
221
|
+
for (let col = 0; col < 64; col++) {
|
|
222
|
+
let byte = this.memory.videoRam[row * 64 + col];
|
|
223
|
+
if (byte < 0x20) {
|
|
224
|
+
byte += 0x40;
|
|
225
|
+
}
|
|
226
|
+
line += byte >= 0x20 && byte < 0x80 ? String.fromCharCode(byte) : ".";
|
|
227
|
+
}
|
|
228
|
+
rows.push(line);
|
|
229
|
+
}
|
|
230
|
+
return rows;
|
|
231
|
+
}
|
|
232
|
+
}
|