@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.
@@ -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
+ }
@@ -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. It also does what the CPU does at the end of any instruction: clears `afterPrefix`
78
- * and counts a pending EI down. It costs what a RET costs, 10 T-states and one refresh-counter step; the
79
- * routine's RAM side effects (its key-rollover buffer at $4036, the repeat counters) are not made.
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
- constructor(matrix, now) {
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(CPU_CLOCK_HZ / cps);
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();
@@ -1,7 +1,10 @@
1
1
  /**
2
- * The Model III video as a text readout: 16 lines over video RAM at $3C00-$3FFF, 64 columns, or
3
- * 32 in 32-column mode (port $EC bit 2), where the hardware shows the even-address bytes
4
- * double-wide. It reads the live video RAM; it draws nothing.
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';
@@ -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
- function checkBaud(baud) {
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(casToPulses(tapeForBaud(bytes, baud), baud));
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(casToPulses(tapeForBaud(bytes, baud), baud));
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
+ }