@fugloy/emu 0.0.0-stage → 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/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +235 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3350 -0
- package/src/debug/debugger.js +788 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +429 -0
- package/src/debug/state.js +273 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +263 -0
- package/src/devices/disk-image.js +965 -0
- package/src/devices/fdc-wd1771.js +180 -0
- package/src/devices/fdc-wd1793.js +1065 -0
- package/src/devices/interrupt-latch.js +76 -0
- package/src/devices/key-queue.js +278 -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 +69 -0
- package/src/index.js +23 -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 +299 -0
- package/src/machine/drive-select.js +42 -0
- package/src/machine/io.js +513 -0
- package/src/machine/machine.js +333 -0
- package/src/machine/memory.js +290 -0
- package/src/machine/models.js +123 -0
- package/src/machine/rom-hashes.js +21 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +289 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III I/O System
|
|
3
|
+
*
|
|
4
|
+
* Non-disk machine port map:
|
|
5
|
+
* 0xE0-0xE3 read: interrupt latch, active-low (pending bit reads 0)
|
|
6
|
+
* write: interrupt mask (bit set = source enabled)
|
|
7
|
+
* 0xEC-0xEF read: acknowledge/clear the RTC interrupt
|
|
8
|
+
* write: mode register (bit 1 = cassette motor,
|
|
9
|
+
* bit 2 = 32-column mode, bit 3 = alt charset)
|
|
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
|
|
16
|
+
* 0xF8-0xFB line printer (read: status, write: data byte)
|
|
17
|
+
* 0xFF cassette port (write bits 0-1 = output level,
|
|
18
|
+
* read: bit 7 = 500-baud pulse latch, bit 0 = 1500-baud
|
|
19
|
+
* level, bits 5-1 = port 0xEC's latch, bit 6 high)
|
|
20
|
+
*
|
|
21
|
+
* The RTC heartbeat interrupt fires at 30 Hz; its latch bit is RTC_IRQ_BIT.
|
|
22
|
+
* The keyboard is memory-mapped at 0x3800-0x3BFF (see MemorySystem), not
|
|
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.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { CassetteSystem } from "../devices/cassette.js";
|
|
41
|
+
import { FDC1793 } from "../devices/fdc-wd1793.js";
|
|
42
|
+
import { FDC1771 } from "../devices/fdc-wd1771.js";
|
|
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";
|
|
47
|
+
|
|
48
|
+
export const RTC_IRQ_BIT = 0x04;
|
|
49
|
+
// Cassette-edge interrupt sources (from the ROM's ISR at 0x35C2: E0
|
|
50
|
+
// bit 0 dispatches to a handler expecting one input level, bit 1 the
|
|
51
|
+
// other; each reads the level back from port 0xFF bit 0). The 1500-baud
|
|
52
|
+
// reader masks in rises only (0xE0 write = 0x01) and counts full
|
|
53
|
+
// cycles between interrupts.
|
|
54
|
+
export const CASSETTE_RISE_IRQ_BIT = 0x01;
|
|
55
|
+
export const CASSETTE_FALL_IRQ_BIT = 0x02;
|
|
56
|
+
|
|
57
|
+
// NMI mask/status bits (port 0xE4)
|
|
58
|
+
export const NMI_INTRQ_BIT = 0x80;
|
|
59
|
+
|
|
60
|
+
// Mode register (port 0xEC write) bits
|
|
61
|
+
export const MODE_32COL_BIT = 0x04; // 32-character display mode
|
|
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
|
+
|
|
73
|
+
export class IOSystem {
|
|
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;
|
|
85
|
+
this.cassette = new CassetteSystem();
|
|
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 });
|
|
89
|
+
this.printer = new Printer();
|
|
90
|
+
this.keyboardBuffer = []; // legacy internal buffer (not a Model III port)
|
|
91
|
+
this.portHandlers = new Map();
|
|
92
|
+
|
|
93
|
+
// Interrupt latch (pending sources) and mask (enabled sources)
|
|
94
|
+
this.intLatch = 0;
|
|
95
|
+
this.intMask = 0;
|
|
96
|
+
|
|
97
|
+
// NMI mask (port 0xE4 write): bit 7 = FDC INTRQ triggers NMI
|
|
98
|
+
this.nmiMask = 0;
|
|
99
|
+
|
|
100
|
+
// Mode register (port 0xEC write); on the Model I, the port 0xFF
|
|
101
|
+
// output latch, D0-D3 (M1_MOTOR_BIT, M1_MODESEL_BIT)
|
|
102
|
+
this.modeRegister = 0;
|
|
103
|
+
|
|
104
|
+
// Fired when a mode-register write changes the value (the display
|
|
105
|
+
// reads bit 2, so the renderer must repaint even though video RAM
|
|
106
|
+
// itself didn't change). Wired by TRS80System.
|
|
107
|
+
this.onModeWrite = null;
|
|
108
|
+
|
|
109
|
+
// Cassette output level (port 0xFF write, bits 0-1)
|
|
110
|
+
this.cassetteOut = 0;
|
|
111
|
+
|
|
112
|
+
// Cassette-out transitions for the sound driver: {t, level} stamped
|
|
113
|
+
// in CPU T-states. getCycles is wired by TRS80System; the driver
|
|
114
|
+
// drains the log every frame. Capped so an undrained log (sound off,
|
|
115
|
+
// headless run) cannot grow without bound.
|
|
116
|
+
this.getCycles = null;
|
|
117
|
+
this.soundLog = [];
|
|
118
|
+
this.SOUND_LOG_LIMIT = 100000;
|
|
119
|
+
// Raw port 0xFF write sink (cycles, value): the Machine's sound ring.
|
|
120
|
+
this.onSoundWrite = null;
|
|
121
|
+
// Called when the timeline restarts (state restore): the Machine clears its sound ring.
|
|
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;
|
|
127
|
+
|
|
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
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
initializeModelIIIPorts() {
|
|
141
|
+
// 0xE0-0xE3: interrupt latch (read, active-low) / mask (write)
|
|
142
|
+
for (let port = 0xe0; port <= 0xe3; port++) {
|
|
143
|
+
this.portHandlers.set(port, {
|
|
144
|
+
read: () => ~this.intLatch & 0xff,
|
|
145
|
+
write: (value) => {
|
|
146
|
+
this.intMask = value & 0xff;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 0xEC-0xEF: RTC acknowledge (read) / mode register (write)
|
|
152
|
+
for (let port = 0xec; port <= 0xef; port++) {
|
|
153
|
+
this.portHandlers.set(port, {
|
|
154
|
+
read: () => {
|
|
155
|
+
this.intLatch &= ~RTC_IRQ_BIT;
|
|
156
|
+
return 0xff;
|
|
157
|
+
},
|
|
158
|
+
write: (value) => {
|
|
159
|
+
const changed = this.modeRegister !== (value & 0xff);
|
|
160
|
+
this.modeRegister = value & 0xff;
|
|
161
|
+
// Mode register bit 1 drives the cassette motor relay;
|
|
162
|
+
// CassetteSystem.control expects the motor state in bit 0.
|
|
163
|
+
this.cassette.control(
|
|
164
|
+
(value >> 1) & 0x01,
|
|
165
|
+
this.getCycles ? this.getCycles() : 0
|
|
166
|
+
);
|
|
167
|
+
if (changed && this.onModeWrite) {
|
|
168
|
+
this.onModeWrite(this.modeRegister);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 0xE4-0xE7: disk NMI mask (write) / status (read, active-low).
|
|
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).
|
|
180
|
+
for (let port = 0xe4; port <= 0xe7; port++) {
|
|
181
|
+
this.portHandlers.set(port, {
|
|
182
|
+
read: () => {
|
|
183
|
+
if (!this.fdc.anyDiskMounted()) return 0xff;
|
|
184
|
+
let value = 0xff;
|
|
185
|
+
if (this.fdc.intrq) value &= ~NMI_INTRQ_BIT;
|
|
186
|
+
return value;
|
|
187
|
+
},
|
|
188
|
+
write: (value) => {
|
|
189
|
+
this.nmiMask = value & 0xff;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 0xF0-0xF3: WD1793 FDC. The controller presents as absent (floating
|
|
195
|
+
// bus) until a disk is mounted — that float is what sends the boot
|
|
196
|
+
// ROM to cassette BASIC on a driveless machine.
|
|
197
|
+
const fdcPresent = () => this.fdc.anyDiskMounted();
|
|
198
|
+
this.portHandlers.set(0xf0, {
|
|
199
|
+
read: () => (fdcPresent() ? this.fdc.readStatus() : 0xff),
|
|
200
|
+
write: (v) => {
|
|
201
|
+
if (fdcPresent()) this.fdc.writeCommand(v);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
this.portHandlers.set(0xf1, {
|
|
205
|
+
read: () => (fdcPresent() ? this.fdc.readTrackReg() : 0xff),
|
|
206
|
+
write: (v) => {
|
|
207
|
+
if (fdcPresent()) this.fdc.writeTrackReg(v);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
this.portHandlers.set(0xf2, {
|
|
211
|
+
read: () => (fdcPresent() ? this.fdc.readSectorReg() : 0xff),
|
|
212
|
+
write: (v) => {
|
|
213
|
+
if (fdcPresent()) this.fdc.writeSectorReg(v);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
this.portHandlers.set(0xf3, {
|
|
217
|
+
read: () => (fdcPresent() ? this.fdc.readData() : 0xff),
|
|
218
|
+
write: (v) => {
|
|
219
|
+
if (fdcPresent()) this.fdc.writeData(v);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
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.
|
|
227
|
+
this.portHandlers.set(0xf4, {
|
|
228
|
+
read: () => 0xff,
|
|
229
|
+
write: (v) => {
|
|
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());
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 0xF8-0xFB: line printer. Status reads ready; data writes land on
|
|
238
|
+
// the printout. Note io.reset() leaves the printout alone — paper
|
|
239
|
+
// already printed survives a reboot on the real machine too.
|
|
240
|
+
for (let port = 0xf8; port <= 0xfb; port++) {
|
|
241
|
+
this.portHandlers.set(port, {
|
|
242
|
+
read: () => this.printer.status(),
|
|
243
|
+
write: (v) => this.printer.write(v),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Port 0xFF on the Model III. */
|
|
249
|
+
initializeCassettePort() {
|
|
250
|
+
// 0xFF: cassette data port. Reads return the input latch in bit 7
|
|
251
|
+
// (set by tape transitions while the motor runs); ANY write clears
|
|
252
|
+
// the latch — that's the Model III's latch circuit, and it's how
|
|
253
|
+
// the ROM's read routines re-arm between pulses. Output level
|
|
254
|
+
// changes are the machine's only sound source, so they're logged
|
|
255
|
+
// for the audio driver, and they double as the recording the deck
|
|
256
|
+
// can decode back into a .cas.
|
|
257
|
+
this.portHandlers.set(0xff, {
|
|
258
|
+
read: () => {
|
|
259
|
+
const cycles = this.getCycles ? this.getCycles() : 0;
|
|
260
|
+
// Two input bits, straight from the ROM's own readers:
|
|
261
|
+
// bit 0 — the tape signal's live LEVEL (the 1500-baud ISR
|
|
262
|
+
// does IN A,(0xFF) / AND 0x01 / CP E per edge);
|
|
263
|
+
// bit 7 — a rising-edge PULSE LATCH (the 500-baud reader
|
|
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.
|
|
269
|
+
// Reading also acks the edge interrupt sources: the ISR reads
|
|
270
|
+
// 0xE0 then 0xFF and nothing else.
|
|
271
|
+
this.intLatch &= ~(CASSETTE_RISE_IRQ_BIT | CASSETTE_FALL_IRQ_BIT);
|
|
272
|
+
const level = this.cassette.tapeLevel(cycles);
|
|
273
|
+
return (this.cassette.pulseLatch ? 0x80 : 0x00) | 0x40 | (this.modeRegister & 0x3e) | (level ? 0x01 : 0x00);
|
|
274
|
+
},
|
|
275
|
+
write: (value) => {
|
|
276
|
+
const cycles = this.getCycles ? this.getCycles() : 0;
|
|
277
|
+
const level = value & 0x03;
|
|
278
|
+
if (this.onSoundWrite) this.onSoundWrite(cycles, value);
|
|
279
|
+
if (level !== this.cassetteOut) {
|
|
280
|
+
this.cassetteOut = level;
|
|
281
|
+
if (this.getCycles && this.soundLog.length < this.SOUND_LOG_LIMIT) {
|
|
282
|
+
this.soundLog.push({ t: cycles, level });
|
|
283
|
+
}
|
|
284
|
+
this.cassette.recordTransition(cycles, level);
|
|
285
|
+
}
|
|
286
|
+
this.cassette.clearPulseLatch(cycles);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
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
|
+
|
|
441
|
+
readPort(port) {
|
|
442
|
+
port &= 0xff;
|
|
443
|
+
const handler = this.portHandlers.get(port);
|
|
444
|
+
return handler?.read ? handler.read() : 0xff;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
writePort(port, value) {
|
|
448
|
+
port &= 0xff;
|
|
449
|
+
value &= 0xff;
|
|
450
|
+
const handler = this.portHandlers.get(port);
|
|
451
|
+
if (handler?.write) {
|
|
452
|
+
handler.write(value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Raise the 30 Hz real-time clock interrupt line. */
|
|
457
|
+
raiseRTC() {
|
|
458
|
+
this.intLatch |= RTC_IRQ_BIT;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** True when an enabled interrupt source is pending. */
|
|
462
|
+
pendingInterrupt() {
|
|
463
|
+
// The cassette edge is a live source: advance the pulse deck to
|
|
464
|
+
// "now" so an edge that just played raises the latch bit without
|
|
465
|
+
// waiting for a port access. Guarded, so machines without a tape
|
|
466
|
+
// playing pay one property check.
|
|
467
|
+
if (this.cassette.pulseTape && this.cassette.motorOn && this.getCycles) {
|
|
468
|
+
const edges = this.cassette.consumeEdges(this.getCycles());
|
|
469
|
+
if (edges.rise) this.intLatch |= CASSETTE_RISE_IRQ_BIT;
|
|
470
|
+
if (edges.fall) this.intLatch |= CASSETTE_FALL_IRQ_BIT;
|
|
471
|
+
}
|
|
472
|
+
return (this.intLatch & this.intMask) !== 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** True when the FDC's INTRQ should raise an NMI. */
|
|
476
|
+
nmiPending() {
|
|
477
|
+
return this.fdc.intrq && (this.nmiMask & NMI_INTRQ_BIT) !== 0;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Hand the accumulated cassette-out transitions to the sound driver. */
|
|
481
|
+
drainSound() {
|
|
482
|
+
if (this.soundLog.length === 0) return this.soundLog;
|
|
483
|
+
const log = this.soundLog;
|
|
484
|
+
this.soundLog = [];
|
|
485
|
+
return log;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Clear interrupt state (system reset). Mounted disks stay mounted. */
|
|
489
|
+
reset() {
|
|
490
|
+
this.latch?.reset();
|
|
491
|
+
this.intLatch = 0;
|
|
492
|
+
this.intMask = 0;
|
|
493
|
+
this.nmiMask = 0;
|
|
494
|
+
this.modeRegister = 0;
|
|
495
|
+
this.cassetteOut = 0;
|
|
496
|
+
this.soundLog = [];
|
|
497
|
+
this.fdc.reset();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
readKeyboard() {
|
|
501
|
+
return this.keyboardBuffer.length > 0 ? this.keyboardBuffer.shift() : 0x00;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
addKey(keyCode) {
|
|
505
|
+
if (this.keyboardBuffer.length < 256) {
|
|
506
|
+
this.keyboardBuffer.push(keyCode & 0xff);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
clearKeyboardBuffer() {
|
|
511
|
+
this.keyboardBuffer = [];
|
|
512
|
+
}
|
|
513
|
+
}
|