@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model I Expansion Interface's interrupt latch at $37E0: one object, owned by the Model I's
|
|
3
|
+
* I/O decode (`IOSystem.latch`, null on the Model III and on a Model I built with
|
|
4
|
+
* `expansion: false`), that every interrupting device of the Expansion Interface raises its bit in.
|
|
5
|
+
*
|
|
6
|
+
* - Bit 7 is the real-time clock: the Expansion Interface's 40 Hz pulse (a "25-millisecond pulse
|
|
7
|
+
* on the interrupt line", "this 40-times-per-second pulse": Kitsz, The Custom TRS-80 and Other
|
|
8
|
+
* Mysteries, p. 136; MAME's trs80 driver, `IRQ_M1_RTC` 0x80 and its 40 Hz periodic interrupt).
|
|
9
|
+
* TRS80System raises it on schedule (`raise(TIMER_BIT)`).
|
|
10
|
+
* - Bit 6 is the disk controller's INTRQ (MAME, `IRQ_M1_FDC` 0x40). The WD1771 (emu phase 6,
|
|
11
|
+
* Task 3) drives it with `raise(DISK_BIT)` and `clear(DISK_BIT)` as its INTRQ rises and falls;
|
|
12
|
+
* reading the controller's status at $37EC is what drops it.
|
|
13
|
+
*
|
|
14
|
+
* Any bit set holds the CPU's INT line low; the Z80 takes it in IM 1 (or IM 0 with $FF on the bus)
|
|
15
|
+
* at $0038 whenever its interrupts are enabled. There is no mask: every source interrupts.
|
|
16
|
+
*
|
|
17
|
+
* Reading $37E0 (and its mirrors $37E1-$37E3, as MAME maps them; the mirrors are unverified) returns
|
|
18
|
+
* the latch and clears the clock bit: "reading 37EC (disk controller status) followed by reading
|
|
19
|
+
* 37E0 (real-time clock) resets a flip-flop used to signal input from disk controller and real-time
|
|
20
|
+
* clock, respectively. This must be done each time or the interrupt will continue to be 'on'"
|
|
21
|
+
* (Kitsz, p. 137). The disk bit is left for the status read to clear. Bits 0-5 read 0
|
|
22
|
+
* (unverified; MAME and xtrs return only the sources).
|
|
23
|
+
*
|
|
24
|
+
* The Level II ROM never reads it: its interrupt vector at $4012 is `EI / RET`
|
|
25
|
+
* (MDL1LEV2.Z80, INITR at $06D2), and Level II BASIC runs with interrupts disabled (DI at $0000 and
|
|
26
|
+
* no EI outside that vector), so the clock bit simply stays set until a DOS or a program enables
|
|
27
|
+
* interrupts with a handler that reads $37E0.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The real-time clock's bit. */
|
|
31
|
+
export const TIMER_BIT = 0x80;
|
|
32
|
+
/** The disk controller's INTRQ bit. */
|
|
33
|
+
export const DISK_BIT = 0x40;
|
|
34
|
+
|
|
35
|
+
export class InterruptLatch {
|
|
36
|
+
/** @type {number} The pending sources (TIMER_BIT, DISK_BIT). */
|
|
37
|
+
bits = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set source bits: the INT line goes (or stays) low.
|
|
41
|
+
* @param {number} mask
|
|
42
|
+
*/
|
|
43
|
+
raise(mask) {
|
|
44
|
+
this.bits |= mask & 0xff;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Clear source bits.
|
|
49
|
+
* @param {number} mask
|
|
50
|
+
*/
|
|
51
|
+
clear(mask) {
|
|
52
|
+
this.bits &= ~mask & 0xff;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True while any source holds the INT line. */
|
|
56
|
+
get pending() {
|
|
57
|
+
return this.bits !== 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A CPU read of $37E0: the latch, after which the clock bit is cleared. */
|
|
61
|
+
read() {
|
|
62
|
+
const value = this.bits;
|
|
63
|
+
this.bits &= ~TIMER_BIT;
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The latch as a read would return it, clearing nothing (for the debugger and `m.mem.read`). */
|
|
68
|
+
peek() {
|
|
69
|
+
return this.bits;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Power-on: nothing pending. */
|
|
73
|
+
reset() {
|
|
74
|
+
this.bits = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/devices/key-queue.js
CHANGED
|
@@ -74,9 +74,18 @@ const KDCB = 0x4015;
|
|
|
74
74
|
* `code` in A, verified against the ROM listing (spec §6, "Keyboard-scan hook"): DE = $4015 (the
|
|
75
75
|
* KDCB), BC, HL, IX, IY and the alternate set unchanged, F as the routine's last flag-setting
|
|
76
76
|
* instruction leaves it (`CP 01H` at $3100 for $002B, `OR A` at $004C for $0049), and the return
|
|
77
|
-
* address popped.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* address popped.
|
|
78
|
+
*
|
|
79
|
+
* The Model I Level II ROM keeps the same contract (MDL1LEV2.Z80, kiwisincebirth/TRS-80-ROMS at
|
|
80
|
+
* 736bcba): `_KBD` at $002B loads DE with the KDCB ($4015) and goes through `_GET` ($0013) and
|
|
81
|
+
* `CIO` ($03C2), which saves BC, HL, IX, and that DE and restores them at `CIORTN` ($03DD), to the
|
|
82
|
+
* keyboard driver `KBDDRV` ($03E3, the KDCB's driver address at $4016), whose path for a key ends
|
|
83
|
+
* with `CP $01` at $0453 (the BREAK test) and `RET NZ`, or for BREAK `RST 28H` (a RET under
|
|
84
|
+
* Level II, $400C) and `RET`; `_KEY` at $0049 is `CALL _KBD / OR A / RET NZ / JR _KEY`. So the
|
|
85
|
+
* stand-in is the same on both models (phase 6 plan, Ruling 6). It also does what the CPU does at
|
|
86
|
+
* the end of any instruction: clears `afterPrefix` and counts a pending EI down. It costs what a
|
|
87
|
+
* RET costs, 10 T-states and one refresh-counter step; the routine's RAM side effects (its
|
|
88
|
+
* key-rollover buffer at $4036, the repeat counters) are not made.
|
|
80
89
|
* @param {import('../cpu/z80cpu.js').Z80CPU} cpu
|
|
81
90
|
* @param {number} pc
|
|
82
91
|
* @param {number} code
|
|
@@ -113,6 +122,8 @@ export class KeyQueue {
|
|
|
113
122
|
#matrix;
|
|
114
123
|
/** @type {() => number} */
|
|
115
124
|
#now;
|
|
125
|
+
/** @type {number} */
|
|
126
|
+
#clockHz;
|
|
116
127
|
/** @type {KeyEvent[]} Matrix events in time order. */
|
|
117
128
|
#events = [];
|
|
118
129
|
/** @type {number} Index of the next undelivered matrix event. */
|
|
@@ -130,9 +141,15 @@ export class KeyQueue {
|
|
|
130
141
|
* @param {import('./keyboard.js').KeyboardMatrix} matrix
|
|
131
142
|
* @param {() => number} now the current T-state
|
|
132
143
|
*/
|
|
133
|
-
|
|
144
|
+
/**
|
|
145
|
+
* @param {import('./keyboard.js').KeyboardMatrix} matrix
|
|
146
|
+
* @param {() => number} now the CPU's T-state count
|
|
147
|
+
* @param {number} [clockHz] the CPU clock `type`'s cps counts in (the Model III's by default)
|
|
148
|
+
*/
|
|
149
|
+
constructor(matrix, now, clockHz = CPU_CLOCK_HZ) {
|
|
134
150
|
this.#matrix = matrix;
|
|
135
151
|
this.#now = now;
|
|
152
|
+
this.#clockHz = clockHz;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
/**
|
|
@@ -173,7 +190,7 @@ export class KeyQueue {
|
|
|
173
190
|
if (mode === 'hook') return this.feed(str);
|
|
174
191
|
if (mode !== 'matrix') throw new Error(`type: mode must be 'matrix' or 'hook', not ${String(mode)}`);
|
|
175
192
|
if (!(cps > 0) || !Number.isFinite(cps)) throw new Error(`type: cps must be a positive number, not ${String(cps)}`);
|
|
176
|
-
const period = Math.round(
|
|
193
|
+
const period = Math.round(this.#clockHz / cps);
|
|
177
194
|
const hold = Math.round(period / 2);
|
|
178
195
|
const keys = Array.from(str, (ch) => this.#mapped(ch, 'type'));
|
|
179
196
|
this.#compact();
|
package/src/devices/video.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* 32
|
|
4
|
-
* double-wide. It reads the live video RAM; it draws
|
|
2
|
+
* The video as a text readout: 16 lines over video RAM at $3C00-$3FFF, 64 columns, or 32 in
|
|
3
|
+
* 32-column mode (on the Model III, port $EC bit 2; on the Model I, port $FF bit 3, MODESEL), where
|
|
4
|
+
* the hardware shows the even-address bytes double-wide. It reads the live video RAM; it draws
|
|
5
|
+
* nothing. The Model I's video RAM sits at the same addresses (models.js); the stock machine has no
|
|
6
|
+
* bit 6 there, so a byte reads back, and shows, with bit 6 made from bits 5 and 7 (memory.js, which
|
|
7
|
+
* cites the Technical Reference Handbook), unless the lowercase modification is fitted.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import { fromBase64 } from '../debug/state.js';
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fugloy/emu — headless, deterministic TRS-80 Model III emulator core.
|
|
2
|
+
* @fugloy/emu — headless, deterministic TRS-80 Model III and Model I emulator core.
|
|
3
3
|
* Extracted in emu phase 0 from cschweda/trs80-emulator at 11612849 (v1.6.0); see NOTICE.md.
|
|
4
4
|
*/
|
|
5
5
|
export { Machine } from './machine/machine.js';
|
|
6
6
|
export { Debugger, COVERAGE } from './debug/debugger.js';
|
|
7
7
|
export { LABEL_FILE_VERSION, COVERAGE_FIELDS, encodeRle, decodeRle, parseLabelFile, mergeLabelFiles } from './debug/label-file.js';
|
|
8
8
|
export { ROM_HASHES } from './machine/rom-hashes.js';
|
|
9
|
+
export { clockHz } from './machine/models.js';
|
|
9
10
|
export { TRS80System, CPU_CLOCK_HZ, RTC_HZ, RTC_INTERVAL_TSTATES } from './machine/trs80-system.js';
|
|
10
11
|
export { Z80CPU } from './cpu/z80cpu.js';
|
|
11
12
|
export { MemorySystem } from './machine/memory.js';
|
|
@@ -14,7 +15,7 @@ export { KeyboardMatrix } from './devices/keyboard.js';
|
|
|
14
15
|
export { CassetteSystem } from './devices/cassette.js';
|
|
15
16
|
export { casToPulses, pulsesToBytes, ensureLeader } from './devices/cassette-tape.js';
|
|
16
17
|
export { FDC1793, parseWriteTrackStream } from './devices/fdc-wd1793.js';
|
|
17
|
-
export { DiskImage } from './devices/disk-image.js';
|
|
18
|
+
export { DiskImage, JV1_MODEL1_SD_TRACKS } from './devices/disk-image.js';
|
|
18
19
|
export { Printer } from './devices/printer.js';
|
|
19
20
|
export { parseCas, fastLoadBasic, fastLoadSystem } from './loaders/cas-format.js';
|
|
20
21
|
export { parseBas } from './loaders/bas.js';
|
package/src/loaders/load.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { parseCmd, fastLoadCmd } from './cmd-format.js';
|
|
11
11
|
import { parseCas, fastLoadBasic, fastLoadSystem } from './cas-format.js';
|
|
12
12
|
import { casToPulses } from '../devices/cassette-tape.js';
|
|
13
|
+
import { CPU_CLOCK_HZ } from '../machine/trs80-system.js';
|
|
14
|
+
import { DiskImage, JV1_MODEL1_SD_TRACKS } from '../devices/disk-image.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @typedef {{ addr: number, len: number, why: string, predicted?: true }} ManifestBlock
|
|
@@ -17,14 +19,43 @@ import { casToPulses } from '../devices/cassette-tape.js';
|
|
|
17
19
|
* @typedef {{ format: 'cmd', name: string, entry: number, blocks: ManifestBlock[] }} CmdManifest
|
|
18
20
|
* @typedef {{ format: 'cas', kind: 'basic' | 'system', name: string, entry?: number, blocks: ManifestBlock[],
|
|
19
21
|
* baud: 500 | 1500 | null, via: 'rom' | 'fast' }} CasManifest
|
|
22
|
+
* @typedef {{ drive: number, container: 'jv1' | 'jv3' | 'dmk', tracks: number, sides: number,
|
|
23
|
+
* sectorsPerTrack?: number, writeProtected: boolean }} DriveInfo
|
|
24
|
+
* `sectorsPerTrack` is present only when every track side holds the same number of sectors
|
|
25
|
+
* @typedef {{ format: 'dsk', container: 'jv1' | 'jv3' | 'dmk', drive: number, boot: boolean, blocks: ManifestBlock[] }} DskManifest
|
|
26
|
+
* `blocks` is empty: mounting writes nothing to memory (what the ROM or a DOS reads lands later)
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
const TXTTAB_PTR = 0x40a4;
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
/**
|
|
32
|
+
* @param {unknown} baud
|
|
33
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
34
|
+
*/
|
|
35
|
+
function checkBaud(baud, machine) {
|
|
36
|
+
if (machine.model === 1) {
|
|
37
|
+
// The Level II ROM reads only 500 baud (MDL1LEV2.Z80, CTBIT at $0241: one clock pulse and an
|
|
38
|
+
// optional data pulse a bit cell); 1500 baud is the Model III's.
|
|
39
|
+
if (baud !== 500) throw new Error(`The Model I reads cassettes at 500 baud only (1500 is the Model III's rate), not ${String(baud)}.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
25
42
|
if (baud !== 500 && baud !== 1500) throw new Error(`cassette baud must be 500 or 1500 (the Model III's L and H rates), not ${String(baud)}`);
|
|
26
43
|
}
|
|
27
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The pulse tape for `bytes` at `baud`, in T-states of this machine's clock. casToPulses times the
|
|
47
|
+
* tape in Model III T-states; a tape plays in real time, so on the Model I (a slower clock) every
|
|
48
|
+
* time is scaled by the clocks' ratio, which keeps the 500-baud bit cell at its 2 ms.
|
|
49
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
50
|
+
* @param {Uint8Array} bytes
|
|
51
|
+
* @param {500 | 1500} baud
|
|
52
|
+
*/
|
|
53
|
+
function pulsesFor(machine, bytes, baud) {
|
|
54
|
+
const times = casToPulses(tapeForBaud(bytes, baud), baud);
|
|
55
|
+
const scale = machine.system.clockHz / CPU_CLOCK_HZ;
|
|
56
|
+
return scale === 1 ? times : times.map((t) => Math.round(t * scale));
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
/**
|
|
29
60
|
* The tape as the deck plays it at `baud`: the file's own leader and sync (00s or 55s, then A5
|
|
30
61
|
* or 7F, any of which may be missing) replaced by 255 leader bytes and the sync byte of that
|
|
@@ -56,8 +87,8 @@ export function cassetteDeck(machine) {
|
|
|
56
87
|
* @param {{ baud?: 500 | 1500 }} [opts]
|
|
57
88
|
*/
|
|
58
89
|
insert(bytes, { baud = 500 } = {}) {
|
|
59
|
-
checkBaud(baud);
|
|
60
|
-
deck.insertPulseTape(
|
|
90
|
+
checkBaud(baud, machine);
|
|
91
|
+
deck.insertPulseTape(pulsesFor(machine, bytes, baud));
|
|
61
92
|
},
|
|
62
93
|
/** Back to the start of the tape. */
|
|
63
94
|
rewind() {
|
|
@@ -103,7 +134,7 @@ export function loadCmd(machine, bytes) {
|
|
|
103
134
|
*/
|
|
104
135
|
export function loadCas(machine, bytes, { baud = 500, via = 'rom' } = {}) {
|
|
105
136
|
if (via !== 'rom' && via !== 'fast') throw new Error(`load.cas: via must be 'rom' or 'fast', not ${String(via)}`);
|
|
106
|
-
if (via === 'rom') checkBaud(baud);
|
|
137
|
+
if (via === 'rom') checkBaud(baud, machine);
|
|
107
138
|
const parsed = parseCas(bytes);
|
|
108
139
|
const system = machine.system;
|
|
109
140
|
|
|
@@ -121,7 +152,7 @@ export function loadCas(machine, bytes, { baud = 500, via = 'rom' } = {}) {
|
|
|
121
152
|
if (via === 'rom') blocks = blocks.map((b) => ({ ...b, predicted: /** @type {true} */ (true) }));
|
|
122
153
|
|
|
123
154
|
if (via === 'rom') {
|
|
124
|
-
system.io.cassette.insertPulseTape(
|
|
155
|
+
system.io.cassette.insertPulseTape(pulsesFor(machine, bytes, baud));
|
|
125
156
|
} else if (parsed.kind === 'system') {
|
|
126
157
|
fastLoadSystem(system, parsed);
|
|
127
158
|
} else {
|
|
@@ -133,3 +164,136 @@ export function loadCas(machine, bytes, { baud = 500, via = 'rom' } = {}) {
|
|
|
133
164
|
const tail = { blocks, baud: via === 'rom' ? baud : null, via };
|
|
134
165
|
return parsed.kind === 'system' ? { ...head, entry: parsed.entry, ...tail } : { ...head, ...tail };
|
|
135
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* What a mounted disk looks like from outside: its container and geometry, as `DiskImage` parsed
|
|
170
|
+
* them, and whether writes are refused.
|
|
171
|
+
* @param {number} drive
|
|
172
|
+
* @param {DiskImage} disk
|
|
173
|
+
* @returns {DriveInfo}
|
|
174
|
+
*/
|
|
175
|
+
function driveInfo(drive, disk) {
|
|
176
|
+
const container = /** @type {'jv1' | 'jv3' | 'dmk'} */ (disk.format);
|
|
177
|
+
const tracks = disk.trackCount();
|
|
178
|
+
const sides = disk.sideCount();
|
|
179
|
+
/** @type {Set<number>} */
|
|
180
|
+
const counts = new Set();
|
|
181
|
+
if (container === 'jv1') counts.add(disk.sectorsPerTrack);
|
|
182
|
+
else {
|
|
183
|
+
for (let t = 0; t < tracks; t++) for (let side = 0; side < sides; side++) counts.add(disk.sectorsOn(t, side).length);
|
|
184
|
+
}
|
|
185
|
+
const info = { drive, container, tracks, sides };
|
|
186
|
+
const [spt] = counts;
|
|
187
|
+
return counts.size === 1 && spt > 0 ? { ...info, sectorsPerTrack: spt, writeProtected: disk.writeProtected } : { ...info, writeProtected: disk.writeProtected };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function checkDrive(op, drive) {
|
|
191
|
+
if (!Number.isInteger(drive) || drive < 0 || drive > 3) throw new Error(`${op}: drive must be 0, 1, 2, or 3, not ${String(drive)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The four floppy drives of the Model III's WD1793 controller, or of the Model I Expansion
|
|
196
|
+
* Interface's WD1771 (spec §3, §6), over `TRS80System.mountDisk` and `ejectDisk`. With no disk in
|
|
197
|
+
* any drive the controller's ports (on the Model I its addresses $37EC-$37EF) float (read $FF),
|
|
198
|
+
* which is what sends the ROM to cassette BASIC (Level II BASIC on the Model I) at reset, so a
|
|
199
|
+
* machine with nothing mounted boots as it did before disks existed. A Model I built without the
|
|
200
|
+
* Expansion Interface has no disk controller, and refuses a mount. On a Model I a 10-sector JV1
|
|
201
|
+
* may have 35 to 42, 77, or 80 tracks (disk-image.js, JV1_MODEL1_SD_TRACKS).
|
|
202
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
203
|
+
*/
|
|
204
|
+
export function diskDrives(machine) {
|
|
205
|
+
const system = machine.system;
|
|
206
|
+
const model1 = system.model.id === 1;
|
|
207
|
+
return {
|
|
208
|
+
/**
|
|
209
|
+
* Put a disk in a drive, replacing what was there. Bytes are copied into a new `DiskImage`
|
|
210
|
+
* (the container is detected from the content: DMK, then JV3, then JV1 by size); a
|
|
211
|
+
* `DiskImage` is mounted as it is, so writes the machine makes show in it.
|
|
212
|
+
* @param {number} drive 0..3
|
|
213
|
+
* @param {Uint8Array | DiskImage} image
|
|
214
|
+
* @param {{ writeProtect?: boolean }} [opts] `writeProtect` overrides the image's own flag (the DMK or JV3 header's); a passed `DiskImage` has its `writeProtected` set
|
|
215
|
+
* @returns {DriveInfo}
|
|
216
|
+
*/
|
|
217
|
+
mount(drive, image, { writeProtect } = {}) {
|
|
218
|
+
checkDrive('fdc.mount', drive);
|
|
219
|
+
if (writeProtect !== undefined && typeof writeProtect !== 'boolean') throw new Error(`fdc.mount: writeProtect must be true or false, not ${String(writeProtect)}`);
|
|
220
|
+
if (model1 && system.io.latch === null) {
|
|
221
|
+
throw new Error('fdc.mount: this Model I has no Expansion Interface, so no disk controller; construct the Machine with expansion: true (the default) to mount a disk');
|
|
222
|
+
}
|
|
223
|
+
let disk;
|
|
224
|
+
if (image instanceof DiskImage) disk = image;
|
|
225
|
+
else if (ArrayBuffer.isView(image) && image.BYTES_PER_ELEMENT === 1) {
|
|
226
|
+
try {
|
|
227
|
+
disk = new DiskImage(/** @type {Uint8Array} */ (image), `drive ${drive}`, model1 ? { jv1SdTracks: JV1_MODEL1_SD_TRACKS } : {});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
throw new Error(`fdc.mount: ${/** @type {Error} */ (e).message}`);
|
|
230
|
+
}
|
|
231
|
+
} else throw new Error('fdc.mount: the image must be a Uint8Array of a JV1, JV3, or DMK file, or a DiskImage');
|
|
232
|
+
if (writeProtect !== undefined) disk.writeProtected = writeProtect;
|
|
233
|
+
system.mountDisk(drive, disk);
|
|
234
|
+
return driveInfo(drive, disk);
|
|
235
|
+
},
|
|
236
|
+
/**
|
|
237
|
+
* Take the disk out of a drive.
|
|
238
|
+
* @param {number} drive 0..3
|
|
239
|
+
* @returns {boolean} true when there was a disk to take out
|
|
240
|
+
*/
|
|
241
|
+
unmount(drive) {
|
|
242
|
+
checkDrive('fdc.unmount', drive);
|
|
243
|
+
const had = system.io.fdc.drives[drive] !== null;
|
|
244
|
+
system.ejectDisk(drive);
|
|
245
|
+
return had;
|
|
246
|
+
},
|
|
247
|
+
/**
|
|
248
|
+
* The four drives, in order: each one's mount info, or null when empty. Read from the
|
|
249
|
+
* controller each time, so it follows a snapshot restore.
|
|
250
|
+
* @returns {(DriveInfo | null)[]}
|
|
251
|
+
*/
|
|
252
|
+
get drives() {
|
|
253
|
+
return system.io.fdc.drives.map((/** @type {DiskImage | null} */ d, i) => (d === null ? null : driveInfo(i, d)));
|
|
254
|
+
},
|
|
255
|
+
/**
|
|
256
|
+
* The disk in a drive as DMK bytes (spec §6: read JV1, JV3, and DMK; write DMK only), with
|
|
257
|
+
* every write the machine has made to it: a DMK as it now stands, a JV1 or JV3 laid out as
|
|
258
|
+
* raw tracks with each sector's ID, density, data mark, and CRCs (`DiskImage.toDMK`). The
|
|
259
|
+
* bytes mount back with the same sectors.
|
|
260
|
+
* @param {number} drive 0..3
|
|
261
|
+
* @returns {Uint8Array}
|
|
262
|
+
*/
|
|
263
|
+
export(drive) {
|
|
264
|
+
checkDrive('fdc.export', drive);
|
|
265
|
+
const disk = system.io.fdc.drives[drive];
|
|
266
|
+
if (disk === null) throw new Error(`fdc.export: drive ${drive} is empty`);
|
|
267
|
+
return disk.toDMK();
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Load a disk image: mount it, and with `boot` reset the machine so the ROM boots from drive 0
|
|
274
|
+
* (the Model III ROM reads track 0, sector 1 to $4300 and jumps there, ROM $34D9-$3516; the Model I
|
|
275
|
+
* Level II ROM reads track 0, sector 0 to $4200 and jumps there, MDL1LEV2.Z80 BOOT at $069F-$06C9,
|
|
276
|
+
* after its PAUSE at $06AF, 65,536 passes of 26 T-states, about a second). Nothing is in memory
|
|
277
|
+
* when this returns, so the manifest's `blocks` is empty. Booting needs a disk in drive 0, this one
|
|
278
|
+
* or one mounted before. A reset clears the keyboard queue; without `boot` it is left alone.
|
|
279
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
280
|
+
* @param {Uint8Array} bytes
|
|
281
|
+
* @param {{ drive?: number, boot?: boolean }} [opts]
|
|
282
|
+
* @returns {DskManifest}
|
|
283
|
+
*/
|
|
284
|
+
export function loadDsk(machine, bytes, { drive = 0, boot = true } = {}) {
|
|
285
|
+
checkDrive('load.dsk', drive);
|
|
286
|
+
if (typeof boot !== 'boolean') throw new Error(`load.dsk: boot must be true or false, not ${String(boot)}`);
|
|
287
|
+
if (!(ArrayBuffer.isView(bytes) && bytes.BYTES_PER_ELEMENT === 1)) throw new Error('load.dsk: bytes must be a Uint8Array of a JV1, JV3, or DMK file');
|
|
288
|
+
if (boot && drive !== 0 && machine.system.io.fdc.drives[0] === null) {
|
|
289
|
+
throw new Error(`load.dsk: the ROM boots from drive 0, which is empty; mount a disk there first, or load into drive 0, or pass boot: false`);
|
|
290
|
+
}
|
|
291
|
+
let info;
|
|
292
|
+
try {
|
|
293
|
+
info = machine.fdc.mount(drive, bytes);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw new Error(/** @type {Error} */ (e).message.replace(/^fdc\.mount/, 'load.dsk'));
|
|
296
|
+
}
|
|
297
|
+
if (boot) machine.reset();
|
|
298
|
+
return { format: 'dsk', container: info.container, drive, boot, blocks: [] };
|
|
299
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each model's drive-select latch, decoded into the controller's neutral `select(drive, side,
|
|
3
|
+
* mfm)` (devices/fdc-wd1793.js). A module of its own so the controllers and the I/O decode
|
|
4
|
+
* (machine/io.js, which re-exports these) can import it without an import cycle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decode a Model III port F4 write: bits 0-3 select drive 0-3 (the lowest set bit wins; none set
|
|
9
|
+
* selects no drive, -1), bit 4 the side, bit 6 WAIT, bit 7 MFM (1) or FM (0); bit 5, write
|
|
10
|
+
* precompensation, does nothing here. Bits from MAME's trs80m3 driver and the ROM's use, not yet
|
|
11
|
+
* checked against the Model III Technical Reference Manual (spec §5).
|
|
12
|
+
* @param {number} value
|
|
13
|
+
* @returns {{ drive: number, side: number, mfm: boolean, wait: boolean }}
|
|
14
|
+
*/
|
|
15
|
+
export function decodeModel3Select(value) {
|
|
16
|
+
let drive = -1;
|
|
17
|
+
if (value & 0x01) drive = 0;
|
|
18
|
+
else if (value & 0x02) drive = 1;
|
|
19
|
+
else if (value & 0x04) drive = 2;
|
|
20
|
+
else if (value & 0x08) drive = 3;
|
|
21
|
+
return { drive, side: value & 0x10 ? 1 : 0, mfm: (value & 0x80) !== 0, wait: (value & 0x40) !== 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decode a write to the Model I Expansion Interface's drive-select latch at $37E1: "a 4-bit drive
|
|
26
|
+
* select indicator sent to the drive" (Kitsz, The Custom TRS-80 and Other Mysteries, ch. 9,
|
|
27
|
+
* pp. 205-206), bits 0-3 for drives 0-3; the Level II ROM selects drive 0 by writing 1
|
|
28
|
+
* (MDL1LEV2.Z80, BOOT at $069F). The lowest set bit wins, as on the Model III (the hardware would
|
|
29
|
+
* select every drive whose bit is set); none set selects no drive, -1. The stock interface is
|
|
30
|
+
* single-sided and single-density, so bits 4-7 do nothing here (double-density and side-select
|
|
31
|
+
* modifications used them; not modelled).
|
|
32
|
+
* @param {number} value
|
|
33
|
+
* @returns {{ drive: number }}
|
|
34
|
+
*/
|
|
35
|
+
export function decodeModel1Select(value) {
|
|
36
|
+
let drive = -1;
|
|
37
|
+
if (value & 0x01) drive = 0;
|
|
38
|
+
else if (value & 0x02) drive = 1;
|
|
39
|
+
else if (value & 0x04) drive = 2;
|
|
40
|
+
else if (value & 0x08) drive = 3;
|
|
41
|
+
return { drive };
|
|
42
|
+
}
|