@fugloy/emu 0.2.0 → 0.3.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/README.md +124 -8
- package/package.json +1 -1
- package/src/cpu/z80cpu.js +9 -2
- package/src/debug/debugger.js +20 -9
- package/src/debug/label-file.js +10 -4
- package/src/debug/state.js +69 -10
- package/src/devices/cassette.js +5 -1
- package/src/devices/disk-image.js +635 -248
- package/src/devices/fdc-wd1771.js +180 -0
- package/src/devices/fdc-wd1793.js +838 -239
- package/src/devices/interrupt-latch.js +76 -0
- package/src/devices/key-queue.js +22 -5
- package/src/devices/video.js +6 -3
- package/src/index.js +3 -2
- package/src/loaders/load.js +169 -5
- package/src/machine/drive-select.js +42 -0
- package/src/machine/io.js +238 -15
- package/src/machine/machine.js +53 -15
- package/src/machine/memory.js +107 -24
- package/src/machine/models.js +123 -0
- package/src/machine/rom-hashes.js +14 -7
- package/src/machine/trs80-system.js +90 -33
package/src/machine/io.js
CHANGED
|
@@ -7,20 +7,43 @@
|
|
|
7
7
|
* 0xEC-0xEF read: acknowledge/clear the RTC interrupt
|
|
8
8
|
* write: mode register (bit 1 = cassette motor,
|
|
9
9
|
* bit 2 = 32-column mode, bit 3 = alt charset)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 0xE4-0xE7 read: NMI status, active-low (bit 7 = FDC INTRQ)
|
|
11
|
+
* write: NMI mask (bit 7 = FDC INTRQ raises NMI)
|
|
12
|
+
* 0xF0-0xF3 WD1793 floppy controller; with no disk mounted anywhere it
|
|
13
|
+
* is absent and reads float to 0xFF
|
|
14
|
+
* 0xF4 drive select (write): bits 0-3 drive, bit 4 side, bit 6
|
|
15
|
+
* WAIT, bit 7 MFM
|
|
12
16
|
* 0xF8-0xFB line printer (read: status, write: data byte)
|
|
13
17
|
* 0xFF cassette port (write bits 0-1 = output level,
|
|
14
|
-
* read bit 7 =
|
|
18
|
+
* read: bit 7 = 500-baud pulse latch, bit 0 = 1500-baud
|
|
19
|
+
* level, bits 5-1 = port 0xEC's latch, bit 6 high)
|
|
15
20
|
*
|
|
16
21
|
* The RTC heartbeat interrupt fires at 30 Hz; its latch bit is RTC_IRQ_BIT.
|
|
17
22
|
* The keyboard is memory-mapped at 0x3800-0x3BFF (see MemorySystem), not
|
|
18
23
|
* an I/O port.
|
|
24
|
+
*
|
|
25
|
+
* The Model I has only port 0xFF (cassette, sound, and the video mode; its
|
|
26
|
+
* own handler, initializeModel1CassettePort): its other devices are
|
|
27
|
+
* memory-mapped by the Expansion Interface at 0x37E0-0x37EF
|
|
28
|
+
* (src/machine/models.js), which MemorySystem hands to `readExpansion`,
|
|
29
|
+
* `peekExpansion`, and `writeExpansion` here, so every other port floats to
|
|
30
|
+
* 0xFF. With the Expansion Interface (`expansion`, the default) the Model I
|
|
31
|
+
* has the interrupt latch at 0x37E0 (`latch`, devices/interrupt-latch.js),
|
|
32
|
+
* the drive select at 0x37E1, the printer at 0x37E8, and the WD1771 at
|
|
33
|
+
* 0x37EC-0x37EF (devices/fdc-wd1771.js, its INTRQ on the latch's bit 6);
|
|
34
|
+
* without it `latch` is null and nothing interrupts. The controller object
|
|
35
|
+
* exists on both models, clocked at the model's CPU clock (a WD1793 on the
|
|
36
|
+
* Model III, a WD1771 on the Model I); the Model I does not reach it through
|
|
37
|
+
* ports, and without the Expansion Interface not at all.
|
|
19
38
|
*/
|
|
20
39
|
|
|
21
40
|
import { CassetteSystem } from "../devices/cassette.js";
|
|
22
41
|
import { FDC1793 } from "../devices/fdc-wd1793.js";
|
|
42
|
+
import { FDC1771 } from "../devices/fdc-wd1771.js";
|
|
23
43
|
import { Printer } from "../devices/printer.js";
|
|
44
|
+
import { InterruptLatch } from "../devices/interrupt-latch.js";
|
|
45
|
+
import { MODEL_III } from "./models.js";
|
|
46
|
+
import { decodeModel1Select, decodeModel3Select } from "./drive-select.js";
|
|
24
47
|
|
|
25
48
|
export const RTC_IRQ_BIT = 0x04;
|
|
26
49
|
// Cassette-edge interrupt sources (from the ROM's ISR at 0x35C2: E0
|
|
@@ -37,10 +60,32 @@ export const NMI_INTRQ_BIT = 0x80;
|
|
|
37
60
|
// Mode register (port 0xEC write) bits
|
|
38
61
|
export const MODE_32COL_BIT = 0x04; // 32-character display mode
|
|
39
62
|
|
|
63
|
+
// The Model I's port 0xFF output latch (Z59), D0-D3: "D0 and D1 ... are used
|
|
64
|
+
// to output data that is recorded on tape during a CSAVE function. D2 ...
|
|
65
|
+
// controls the status of the Recorder's motor. D3 ... controls the status of
|
|
66
|
+
// MODESEL*" (TRS-80 Micro Computer Technical Reference Handbook, 2nd ed.,
|
|
67
|
+
// 1982, p. 45, "OUTSIG*"). On the Model I `modeRegister` holds this latch.
|
|
68
|
+
export const M1_MOTOR_BIT = 0x04;
|
|
69
|
+
export const M1_MODESEL_BIT = 0x08; // 32-character display mode
|
|
70
|
+
|
|
71
|
+
export { decodeModel1Select, decodeModel3Select } from "./drive-select.js";
|
|
72
|
+
|
|
40
73
|
export class IOSystem {
|
|
41
|
-
|
|
74
|
+
/**
|
|
75
|
+
* @param {{ model?: import('./models.js').Model, expansion?: boolean }} [opts] the model whose
|
|
76
|
+
* ports to install (the Model III by default), and on the Model I whether the Expansion
|
|
77
|
+
* Interface is fitted (by default it is)
|
|
78
|
+
*/
|
|
79
|
+
constructor({ model = MODEL_III, expansion = true } = {}) {
|
|
80
|
+
this.model = model;
|
|
81
|
+
// The Expansion Interface's interrupt latch at 0x37E0: the one object the timer (bit 7) and
|
|
82
|
+
// the disk controller (bit 6, emu phase 6 Task 3) raise and clear their bits in.
|
|
83
|
+
/** @type {InterruptLatch | null} */
|
|
84
|
+
this.latch = model.id === 1 && expansion ? new InterruptLatch() : null;
|
|
42
85
|
this.cassette = new CassetteSystem();
|
|
43
|
-
|
|
86
|
+
// A Model I recording decodes in Model III T-states (cassette.js)
|
|
87
|
+
if (model.id !== 3) this.cassette.timeScale = MODEL_III.clockHz / model.clockHz;
|
|
88
|
+
this.fdc = model.id === 3 ? new FDC1793({ cpuHz: model.clockHz }) : new FDC1771({ cpuHz: model.clockHz, latch: this.latch });
|
|
44
89
|
this.printer = new Printer();
|
|
45
90
|
this.keyboardBuffer = []; // legacy internal buffer (not a Model III port)
|
|
46
91
|
this.portHandlers = new Map();
|
|
@@ -52,7 +97,8 @@ export class IOSystem {
|
|
|
52
97
|
// NMI mask (port 0xE4 write): bit 7 = FDC INTRQ triggers NMI
|
|
53
98
|
this.nmiMask = 0;
|
|
54
99
|
|
|
55
|
-
// Mode register (port 0xEC write)
|
|
100
|
+
// Mode register (port 0xEC write); on the Model I, the port 0xFF
|
|
101
|
+
// output latch, D0-D3 (M1_MOTOR_BIT, M1_MODESEL_BIT)
|
|
56
102
|
this.modeRegister = 0;
|
|
57
103
|
|
|
58
104
|
// Fired when a mode-register write changes the value (the display
|
|
@@ -74,8 +120,21 @@ export class IOSystem {
|
|
|
74
120
|
this.onSoundWrite = null;
|
|
75
121
|
// Called when the timeline restarts (state restore): the Machine clears its sound ring.
|
|
76
122
|
this.onSoundReset = null;
|
|
123
|
+
// The Model III WAIT (port F4 bit 6): called with a function that runs the controller to
|
|
124
|
+
// its next DRQ or INTRQ and returns the T-states that took, for the CPU to wait them; wired
|
|
125
|
+
// by TRS80System. Unwired, port F4 does not wait.
|
|
126
|
+
this.onWait = null;
|
|
77
127
|
|
|
78
|
-
|
|
128
|
+
if (model.id === 3) {
|
|
129
|
+
this.initializeModelIIIPorts();
|
|
130
|
+
this.initializeCassettePort();
|
|
131
|
+
} else {
|
|
132
|
+
this.initializeModel1CassettePort();
|
|
133
|
+
// The Model I interrupts through the Expansion Interface's latch alone, which has no mask
|
|
134
|
+
// (none without the interface); the Model III's pendingInterrupt below is left as it was.
|
|
135
|
+
const latch = this.latch;
|
|
136
|
+
this.pendingInterrupt = latch === null ? () => false : () => latch.bits !== 0;
|
|
137
|
+
}
|
|
79
138
|
}
|
|
80
139
|
|
|
81
140
|
initializeModelIIIPorts() {
|
|
@@ -114,6 +173,10 @@ export class IOSystem {
|
|
|
114
173
|
|
|
115
174
|
// 0xE4-0xE7: disk NMI mask (write) / status (read, active-low).
|
|
116
175
|
// With no controller "present" (no disk mounted) the port floats.
|
|
176
|
+
// Bit 7 is the FDC's INTRQ, read live. MAME's trs80m3 driver also
|
|
177
|
+
// names bit 6 (motor timeout, its own NMI enable) and bit 5 (the reset
|
|
178
|
+
// button); neither is modeled, and none of this is yet checked against
|
|
179
|
+
// the Model III Technical Reference Manual (spec §5).
|
|
117
180
|
for (let port = 0xe4; port <= 0xe7; port++) {
|
|
118
181
|
this.portHandlers.set(port, {
|
|
119
182
|
read: () => {
|
|
@@ -157,11 +220,17 @@ export class IOSystem {
|
|
|
157
220
|
},
|
|
158
221
|
});
|
|
159
222
|
|
|
160
|
-
// 0xF4: drive select / side / density latch
|
|
223
|
+
// 0xF4: drive select / side / density latch. Bit 6 asserts WAIT
|
|
224
|
+
// until the controller raises DRQ or INTRQ (MAME's trs80m3: only when
|
|
225
|
+
// both are low); the ROM's boot read and the DOSes' INI loops rely on
|
|
226
|
+
// it to take each byte as it arrives.
|
|
161
227
|
this.portHandlers.set(0xf4, {
|
|
162
228
|
read: () => 0xff,
|
|
163
229
|
write: (v) => {
|
|
164
|
-
if (fdcPresent())
|
|
230
|
+
if (!fdcPresent()) return;
|
|
231
|
+
const sel = decodeModel3Select(v);
|
|
232
|
+
this.fdc.select(sel.drive, sel.side, sel.mfm);
|
|
233
|
+
if (sel.wait && this.onWait) this.onWait(() => this.fdc.waitForDrq());
|
|
165
234
|
},
|
|
166
235
|
});
|
|
167
236
|
|
|
@@ -174,7 +243,10 @@ export class IOSystem {
|
|
|
174
243
|
write: (v) => this.printer.write(v),
|
|
175
244
|
});
|
|
176
245
|
}
|
|
246
|
+
}
|
|
177
247
|
|
|
248
|
+
/** Port 0xFF on the Model III. */
|
|
249
|
+
initializeCassettePort() {
|
|
178
250
|
// 0xFF: cassette data port. Reads return the input latch in bit 7
|
|
179
251
|
// (set by tape transitions while the motor runs); ANY write clears
|
|
180
252
|
// the latch — that's the Model III's latch circuit, and it's how
|
|
@@ -190,11 +262,15 @@ export class IOSystem {
|
|
|
190
262
|
// does IN A,(0xFF) / AND 0x01 / CP E per edge);
|
|
191
263
|
// bit 7 — a rising-edge PULSE LATCH (the 500-baud reader
|
|
192
264
|
// polls it with RLA and resets it by OUT (0xFF),A).
|
|
265
|
+
// Bits 5-1 read port 0xEC's latch back (DISWAIT, ENEXTIO, ENALTSET,
|
|
266
|
+
// MODSEL, CASMOTORON), and bit 6 is undefined: Model III Technical
|
|
267
|
+
// Reference Manual p. 22 (CASIN*). Undriven, bit 6 floats high, as
|
|
268
|
+
// an absent port's bits do; trs80gp (and trsjs) read it as 1 too.
|
|
193
269
|
// Reading also acks the edge interrupt sources: the ISR reads
|
|
194
270
|
// 0xE0 then 0xFF and nothing else.
|
|
195
271
|
this.intLatch &= ~(CASSETTE_RISE_IRQ_BIT | CASSETTE_FALL_IRQ_BIT);
|
|
196
272
|
const level = this.cassette.tapeLevel(cycles);
|
|
197
|
-
return (this.cassette.pulseLatch ? 0x80 : 0x00) | (level ? 0x01 : 0x00);
|
|
273
|
+
return (this.cassette.pulseLatch ? 0x80 : 0x00) | 0x40 | (this.modeRegister & 0x3e) | (level ? 0x01 : 0x00);
|
|
198
274
|
},
|
|
199
275
|
write: (value) => {
|
|
200
276
|
const cycles = this.getCycles ? this.getCycles() : 0;
|
|
@@ -212,6 +288,156 @@ export class IOSystem {
|
|
|
212
288
|
});
|
|
213
289
|
}
|
|
214
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Port 0xFF on the Model I. The port is the Z59 output latch and one input
|
|
293
|
+
* flip-flop (Technical Reference Handbook, 2nd ed., p. 45, "OUTSIG*",
|
|
294
|
+
* and pp. 50-51, "INSIG*"):
|
|
295
|
+
* write D0-D1 cassette output level (the only sound source; logged for
|
|
296
|
+
* the sound driver and m.sound, and recorded while the
|
|
297
|
+
* motor runs, as on the Model III)
|
|
298
|
+
* write D2 the cassette motor relay
|
|
299
|
+
* write D3 MODESEL: 32 characters a line when set
|
|
300
|
+
* any write resets the cassette flip-flop ("set by cassette data and
|
|
301
|
+
* reset by OUTSIG*", p. 50)
|
|
302
|
+
* read D7 the cassette flip-flop: the Level II ROM's bit reader
|
|
303
|
+
* (MDL1LEV2.Z80, CTBIT at $0241) polls it with RLA and resets
|
|
304
|
+
* it through CTSTAT ($021E), an OUT of the shadow copy CAST
|
|
305
|
+
* The other read bits are MAME's (trs80 driver, port_ff_r: D6 is MODESEL
|
|
306
|
+
* inverted, D0-D5 read 1); the handbook says only that INSIG* enables the
|
|
307
|
+
* flip-flop's buffer, so they are unverified. The deck sets the flip-flop
|
|
308
|
+
* on a rising edge of the tape signal (cassette.js); MAME takes the
|
|
309
|
+
* falling edge, which for the 500-baud pulses moves nothing a reader sees.
|
|
310
|
+
*/
|
|
311
|
+
initializeModel1CassettePort() {
|
|
312
|
+
this.portHandlers.set(0xff, {
|
|
313
|
+
read: () => {
|
|
314
|
+
const cycles = this.getCycles ? this.getCycles() : 0;
|
|
315
|
+
this.cassette.tapeLevel(cycles); // play the tape up to now
|
|
316
|
+
return (
|
|
317
|
+
(this.cassette.pulseLatch ? 0x80 : 0x00) |
|
|
318
|
+
(this.modeRegister & M1_MODESEL_BIT ? 0x00 : 0x40) |
|
|
319
|
+
0x3f
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
write: (value) => {
|
|
323
|
+
const cycles = this.getCycles ? this.getCycles() : 0;
|
|
324
|
+
if (this.onSoundWrite) this.onSoundWrite(cycles, value);
|
|
325
|
+
const motor = (value & M1_MOTOR_BIT) !== 0;
|
|
326
|
+
if (motor !== this.cassette.motorOn) {
|
|
327
|
+
this.cassette.control(motor ? 1 : 0, cycles);
|
|
328
|
+
}
|
|
329
|
+
const level = value & 0x03;
|
|
330
|
+
if (level !== this.cassetteOut) {
|
|
331
|
+
this.cassetteOut = level;
|
|
332
|
+
if (this.getCycles && this.soundLog.length < this.SOUND_LOG_LIMIT) {
|
|
333
|
+
this.soundLog.push({ t: cycles, level });
|
|
334
|
+
}
|
|
335
|
+
this.cassette.recordTransition(cycles, level);
|
|
336
|
+
}
|
|
337
|
+
const latch = value & 0x0f;
|
|
338
|
+
const changed = (latch ^ this.modeRegister) & M1_MODESEL_BIT;
|
|
339
|
+
this.modeRegister = latch;
|
|
340
|
+
if (changed && this.onModeWrite) this.onModeWrite(this.modeRegister);
|
|
341
|
+
this.cassette.clearPulseLatch(cycles);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A CPU read of the Model I's Expansion Interface addresses 0x37E0-0x37EF
|
|
348
|
+
* (MemorySystem calls it only on a Model I with the Expansion Interface):
|
|
349
|
+
* 0x37E0-0x37E3 the interrupt latch, clearing its clock bit
|
|
350
|
+
* (interrupt-latch.js; the mirrors are MAME's, unverified)
|
|
351
|
+
* 0x37E4-0x37E7 0xFF (the cassette select at 0x37E4 is write-only, and a
|
|
352
|
+
* second deck is not modelled)
|
|
353
|
+
* 0x37E8-0x37EB the printer's status: the Level II ROM's PRTAVL at $05D1
|
|
354
|
+
* reads 0x37E8 and takes (status AND F0H) = 30H as ready
|
|
355
|
+
* (MDL1LEV2.Z80); the mirrors are MAME's (unverified)
|
|
356
|
+
* 0x37EC-0x37EF the WD1771: status, track, sector, data (CONSTANTS.Z80,
|
|
357
|
+
* "Memory Mapped Hardware, Model 1"); a status read clears
|
|
358
|
+
* INTRQ and with it the latch's disk bit
|
|
359
|
+
* With no disk mounted in any drive the controller is taken as absent, as on
|
|
360
|
+
* the Model III, and 0x37EC-0x37EF read 0xFF: the ROM's test at $0696-$069C
|
|
361
|
+
* (status + 1 < 2, that is $FF or $00: no disk controller) then sends it to
|
|
362
|
+
* Level II BASIC instead of a boot that would wait for a disk forever.
|
|
363
|
+
* @param {number} address
|
|
364
|
+
*/
|
|
365
|
+
readExpansion(address) {
|
|
366
|
+
if (address <= 0x37e3) return this.latch.read();
|
|
367
|
+
if (address >= 0x37ec) {
|
|
368
|
+
if (!this.fdc.anyDiskMounted()) return 0xff;
|
|
369
|
+
switch (address) {
|
|
370
|
+
case 0x37ec:
|
|
371
|
+
return this.fdc.readStatus();
|
|
372
|
+
case 0x37ed:
|
|
373
|
+
return this.fdc.readTrackReg();
|
|
374
|
+
case 0x37ee:
|
|
375
|
+
return this.fdc.readSectorReg();
|
|
376
|
+
default:
|
|
377
|
+
return this.fdc.readData();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (address >= 0x37e8) return this.printer.status();
|
|
381
|
+
return 0xff;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* What `readExpansion` would return, with no side effect (the latch keeps
|
|
386
|
+
* its clock bit, the controller its INTRQ and DRQ).
|
|
387
|
+
* @param {number} address
|
|
388
|
+
*/
|
|
389
|
+
peekExpansion(address) {
|
|
390
|
+
if (address <= 0x37e3) return this.latch.peek();
|
|
391
|
+
if (address >= 0x37ec) {
|
|
392
|
+
if (!this.fdc.anyDiskMounted()) return 0xff;
|
|
393
|
+
if (address === 0x37ec) return this.fdc.peekStatus();
|
|
394
|
+
if (address === 0x37ed) return this.fdc.trackReg;
|
|
395
|
+
if (address === 0x37ee) return this.fdc.sectorReg;
|
|
396
|
+
return this.fdc.dataReg;
|
|
397
|
+
}
|
|
398
|
+
if (address >= 0x37e8) return this.printer.status();
|
|
399
|
+
return 0xff;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A CPU write to 0x37E0-0x37EF:
|
|
404
|
+
* 0x37E0-0x37E3 the drive select (decodeModel1Select; written at 0x37E1,
|
|
405
|
+
* the mirrors are MAME's, unverified). The latch is the
|
|
406
|
+
* Expansion Interface's, not the controller's, so it takes
|
|
407
|
+
* a write with no disk mounted too, and a disk mounted
|
|
408
|
+
* later finds the drive the program last selected
|
|
409
|
+
* 0x37E8-0x37EB a byte to the printer (the ROM's `LD (37E8H),A` at $05BB)
|
|
410
|
+
* 0x37EC-0x37EF the WD1771: command, track, sector, data
|
|
411
|
+
* The controller's registers ignore writes while no disk is mounted
|
|
412
|
+
* (absent, as above).
|
|
413
|
+
* @param {number} address
|
|
414
|
+
* @param {number} value
|
|
415
|
+
*/
|
|
416
|
+
writeExpansion(address, value) {
|
|
417
|
+
if (address <= 0x37e3) {
|
|
418
|
+
this.fdc.select(decodeModel1Select(value).drive, 0, false);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (address >= 0x37ec) {
|
|
422
|
+
if (!this.fdc.anyDiskMounted()) return;
|
|
423
|
+
switch (address) {
|
|
424
|
+
case 0x37ec:
|
|
425
|
+
this.fdc.writeCommand(value);
|
|
426
|
+
return;
|
|
427
|
+
case 0x37ed:
|
|
428
|
+
this.fdc.writeTrackReg(value);
|
|
429
|
+
return;
|
|
430
|
+
case 0x37ee:
|
|
431
|
+
this.fdc.writeSectorReg(value);
|
|
432
|
+
return;
|
|
433
|
+
default:
|
|
434
|
+
this.fdc.writeData(value);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (address >= 0x37e8) this.printer.write(value);
|
|
439
|
+
}
|
|
440
|
+
|
|
215
441
|
readPort(port) {
|
|
216
442
|
port &= 0xff;
|
|
217
443
|
const handler = this.portHandlers.get(port);
|
|
@@ -261,17 +487,14 @@ export class IOSystem {
|
|
|
261
487
|
|
|
262
488
|
/** Clear interrupt state (system reset). Mounted disks stay mounted. */
|
|
263
489
|
reset() {
|
|
490
|
+
this.latch?.reset();
|
|
264
491
|
this.intLatch = 0;
|
|
265
492
|
this.intMask = 0;
|
|
266
493
|
this.nmiMask = 0;
|
|
267
494
|
this.modeRegister = 0;
|
|
268
495
|
this.cassetteOut = 0;
|
|
269
496
|
this.soundLog = [];
|
|
270
|
-
this.fdc.
|
|
271
|
-
this.fdc.pending = null;
|
|
272
|
-
this.fdc.buffer = null;
|
|
273
|
-
this.fdc.status = 0;
|
|
274
|
-
this.fdc.commandType = 1;
|
|
497
|
+
this.fdc.reset();
|
|
275
498
|
}
|
|
276
499
|
|
|
277
500
|
readKeyboard() {
|
package/src/machine/machine.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The spec §3 `Machine`: the public face of the emulator, over a TRS80System.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The spec §3 `Machine`: the public face of the emulator, over a TRS80System. It builds the
|
|
3
|
+
* Model III with 48K, or (emu phase 6) the Model I with 4, 16, 32, or 48K, as the model
|
|
4
|
+
* descriptions in models.js lay them out; execution, state access, ROM identification, the
|
|
5
|
+
* devices, the loaders, and the debugger live here.
|
|
5
6
|
*/
|
|
6
7
|
import { TRS80System } from './trs80-system.js';
|
|
7
8
|
import { ROM_HASHES } from './rom-hashes.js';
|
|
9
|
+
import { modelOf, ramProblem, romModelProblem } from './models.js';
|
|
8
10
|
import { sha256Hex } from './sha256.js';
|
|
9
11
|
import { Video } from '../devices/video.js';
|
|
10
12
|
import { Sound } from '../devices/sound.js';
|
|
11
13
|
import { loadBas } from '../loaders/bas.js';
|
|
12
|
-
import { cassetteDeck, loadCas, loadCmd } from '../loaders/load.js';
|
|
14
|
+
import { cassetteDeck, diskDrives, loadCas, loadCmd, loadDsk } from '../loaders/load.js';
|
|
13
15
|
import { Debugger } from '../debug/debugger.js';
|
|
14
16
|
import { KeyQueue, romKeyReturn } from '../devices/key-queue.js';
|
|
15
17
|
|
|
@@ -41,12 +43,15 @@ export class Machine {
|
|
|
41
43
|
keyboard;
|
|
42
44
|
/**
|
|
43
45
|
* @type {{ bas(bytes: Uint8Array): ReturnType<typeof loadBas>, cmd(bytes: Uint8Array): ReturnType<typeof loadCmd>,
|
|
44
|
-
* cas(bytes: Uint8Array, opts?: { baud?: 500 | 1500, via?: 'rom' | 'fast' }): ReturnType<typeof loadCas
|
|
46
|
+
* cas(bytes: Uint8Array, opts?: { baud?: 500 | 1500, via?: 'rom' | 'fast' }): ReturnType<typeof loadCas>,
|
|
47
|
+
* dsk(bytes: Uint8Array, opts?: { drive?: number, boot?: boolean }): ReturnType<typeof loadDsk> }}
|
|
45
48
|
* Program loaders; each returns a manifest of where the bytes land and why.
|
|
46
49
|
*/
|
|
47
50
|
load;
|
|
48
51
|
/** @type {ReturnType<typeof cassetteDeck>} The cassette deck: insert a .CAS at a baud, rewind, read the motor relay. */
|
|
49
52
|
cassette;
|
|
53
|
+
/** @type {ReturnType<typeof diskDrives>} The four floppy drives: mount a JV1, JV3, or DMK image, unmount, list what is in each, export one as DMK. */
|
|
54
|
+
fdc;
|
|
50
55
|
/** @type {Debugger} Breakpoints, watchpoints, hooks, coverage, the instruction trace, snapshots, labels, and the label file (spec §7, §8). */
|
|
51
56
|
debug;
|
|
52
57
|
|
|
@@ -62,18 +67,32 @@ export class Machine {
|
|
|
62
67
|
#stopped = false;
|
|
63
68
|
|
|
64
69
|
/**
|
|
65
|
-
* @param {{ model?: 3, ram?: 48, roms: { level2: Uint8Array },
|
|
70
|
+
* @param {{ model?: 1 | 3, ram?: 4 | 16 | 32 | 48, roms: { level2: Uint8Array }, expansion?: boolean, lowercase?: boolean,
|
|
71
|
+
* log?: (level: string, text: string) => void }} opts
|
|
72
|
+
* `ram` defaults to 48 (the Model III takes only 48 for now); `expansion` (the Expansion Interface, default
|
|
73
|
+
* true) and `lowercase` (the lowercase modification, default false) are Model I options
|
|
66
74
|
*/
|
|
67
|
-
constructor({ model = 3, ram
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
constructor({ model = 3, ram, roms, expansion, lowercase, log } = /** @type {any} */ ({})) {
|
|
76
|
+
const desc = modelOf(model);
|
|
77
|
+
const kb = ram ?? desc.defaultRam;
|
|
78
|
+
const badRam = ramProblem(desc, kb);
|
|
79
|
+
if (badRam !== null) throw new Error(badRam);
|
|
80
|
+
for (const [name, value] of /** @type {const} */ ([['expansion', expansion], ['lowercase', lowercase]])) {
|
|
81
|
+
if (value === undefined) continue;
|
|
82
|
+
if (desc.id !== 1) throw new Error(`${name} is a Model I option; the ${desc.name} has no ${name === 'expansion' ? 'Expansion Interface' : 'lowercase modification (its lowercase is built in)'}.`);
|
|
83
|
+
if (typeof value !== 'boolean') throw new Error(`${name} must be true or false, not ${String(value)}.`);
|
|
70
84
|
}
|
|
71
85
|
const level2 = roms?.level2;
|
|
72
86
|
if (!ArrayBuffer.isView(level2) || level2.BYTES_PER_ELEMENT !== 1) throw new Error('Machine needs roms.level2, the ROM image as a Uint8Array');
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
// A ROM recognised as the other model's is refused by name before its size is checked, so the
|
|
89
|
+
// sentence says which model it needs rather than only that its length is wrong.
|
|
75
90
|
const sha256 = sha256Hex(level2);
|
|
76
|
-
|
|
91
|
+
const recognized = ROM_HASHES[sha256] ?? null;
|
|
92
|
+
const problem = romModelProblem(recognized, desc.id);
|
|
93
|
+
if (problem !== null) throw new Error(problem);
|
|
94
|
+
this.system = new TRS80System({ romData: level2, log, model: desc, ram: kb, expansion: expansion ?? true, lowercase: lowercase ?? false });
|
|
95
|
+
this.rom = { bytes: level2.length, sha256, recognized };
|
|
77
96
|
|
|
78
97
|
const sys = this.system;
|
|
79
98
|
const cpu = sys.cpu;
|
|
@@ -95,7 +114,8 @@ export class Machine {
|
|
|
95
114
|
this.mem = {
|
|
96
115
|
read(addr, len) {
|
|
97
116
|
const out = new Uint8Array(len);
|
|
98
|
-
|
|
117
|
+
// Peeked: reading the Model I's interrupt latch here does not clear it, as a CPU read would.
|
|
118
|
+
for (let k = 0; k < len; k++) out[k] = sys.memory.peekByte((addr + k) & 0xffff);
|
|
99
119
|
return out;
|
|
100
120
|
},
|
|
101
121
|
write(addr, bytes) {
|
|
@@ -109,7 +129,7 @@ export class Machine {
|
|
|
109
129
|
this.sound = new Sound();
|
|
110
130
|
sys.io.onSoundWrite = (cycle, value) => this.sound.record(cycle, value);
|
|
111
131
|
sys.io.onSoundReset = () => this.sound.clear();
|
|
112
|
-
this.keyboard = new KeyQueue(sys.keyboard, () => sys.cpu.cycles);
|
|
132
|
+
this.keyboard = new KeyQueue(sys.keyboard, () => sys.cpu.cycles, sys.clockHz);
|
|
113
133
|
this.debug = new Debugger(this);
|
|
114
134
|
// Each load records its manifest and the bytes' hash for the label file (the last one wins).
|
|
115
135
|
const record = (/** @type {any} */ manifest, /** @type {Uint8Array} */ bytes) => {
|
|
@@ -120,14 +140,30 @@ export class Machine {
|
|
|
120
140
|
bas: (bytes) => record(loadBas(this, bytes), bytes),
|
|
121
141
|
cmd: (bytes) => record(loadCmd(this, bytes), bytes),
|
|
122
142
|
cas: (bytes, opts) => record(loadCas(this, bytes, opts), bytes),
|
|
143
|
+
dsk: (bytes, opts) => record(loadDsk(this, bytes, opts), bytes),
|
|
123
144
|
};
|
|
124
145
|
this.cassette = cassetteDeck(this);
|
|
146
|
+
this.fdc = diskDrives(this);
|
|
125
147
|
this.io = {
|
|
126
148
|
in: (port) => sys.io.readPort(port),
|
|
127
149
|
out: (port, value) => sys.io.writePort(port, value),
|
|
128
150
|
};
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
/** @returns {1 | 3} The model built: 1 (the Model I) or 3 (the Model III). */
|
|
154
|
+
get model() {
|
|
155
|
+
return this.system.model.id;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The model's CPU clock in Hz: 2,027,520 on the Model III, 1,774,080 on the Model I (models.js).
|
|
160
|
+
* T-state counts (`cycles`, `run`'s budget, `m.sound` stamps) are in this clock's states.
|
|
161
|
+
* @returns {number}
|
|
162
|
+
*/
|
|
163
|
+
get clockHz() {
|
|
164
|
+
return this.system.clockHz;
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
/** Total T-states since the last reset. */
|
|
132
168
|
get cycles() {
|
|
133
169
|
return this.system.cpu.cycles;
|
|
@@ -157,8 +193,10 @@ export class Machine {
|
|
|
157
193
|
* delivery, before the instruction executes) by both `step` and `run`: deliver the matrix key
|
|
158
194
|
* events now due, then, with a fed character waiting and the PC at the ROM's keyboard scan
|
|
159
195
|
* ($002B) or wait-for-key ($0049), return from that routine with the character as the ROM
|
|
160
|
-
* would (spec §6, "Keyboard-scan hook").
|
|
161
|
-
*
|
|
196
|
+
* would (spec §6, "Keyboard-scan hook"). The Model I Level II ROM has the same two entries with
|
|
197
|
+
* the same contract (romKeyReturn), so the hook serves both models. That return stands in for
|
|
198
|
+
* the boundary's instruction, as one RET (10 T-states); a halted CPU idles instead, as it does
|
|
199
|
+
* for any instruction, so the next boundary is at the caller's return address.
|
|
162
200
|
* Between draining and the keyboard hook, a breakpoint hit (only while `debug.active`, and
|
|
163
201
|
* never in `step`) stops the boundary: nothing executes. Last, with memory accesses watched or
|
|
164
202
|
* hooked, the debugger is told an instruction starts at PC, so its fetches are not taken for
|