@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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Machine's .CMD and .CAS loaders and its cassette deck controls. Each loader returns a
|
|
3
|
+
* manifest: where the bytes land and why (spec §6).
|
|
4
|
+
*
|
|
5
|
+
* A .CAS goes in one of two ways. Via 'rom' (the default) the tape is put in the cassette deck as
|
|
6
|
+
* pulses at the chosen baud, and the ROM's own loader reads it when the caller types CLOAD or
|
|
7
|
+
* SYSTEM; nothing is written until then, so every manifest block carries `predicted: true`. Via
|
|
8
|
+
* 'fast' the blocks are written at once (fastLoadBasic, fastLoadSystem).
|
|
9
|
+
*/
|
|
10
|
+
import { parseCmd, fastLoadCmd } from './cmd-format.js';
|
|
11
|
+
import { parseCas, fastLoadBasic, fastLoadSystem } from './cas-format.js';
|
|
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';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ addr: number, len: number, why: string, predicted?: true }} ManifestBlock
|
|
18
|
+
* `predicted` marks a block that has not landed yet (via 'rom', before the ROM reads the tape)
|
|
19
|
+
* @typedef {{ format: 'cmd', name: string, entry: number, blocks: ManifestBlock[] }} CmdManifest
|
|
20
|
+
* @typedef {{ format: 'cas', kind: 'basic' | 'system', name: string, entry?: number, blocks: ManifestBlock[],
|
|
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)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const TXTTAB_PTR = 0x40a4;
|
|
30
|
+
|
|
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
|
+
}
|
|
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)}`);
|
|
43
|
+
}
|
|
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
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The tape as the deck plays it at `baud`: the file's own leader and sync (00s or 55s, then A5
|
|
61
|
+
* or 7F, any of which may be missing) replaced by 255 leader bytes and the sync byte of that
|
|
62
|
+
* rate, 00s and A5 at 500 baud, 55s and 7F at 1500, which is what the ROM hunts for.
|
|
63
|
+
* @param {Uint8Array} bytes
|
|
64
|
+
* @param {500 | 1500} baud
|
|
65
|
+
*/
|
|
66
|
+
export function tapeForBaud(bytes, baud) {
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < bytes.length && (bytes[i] === 0x00 || bytes[i] === 0x55)) i++;
|
|
69
|
+
// Leader bytes not followed by a sync are content (a bare SYSTEM tape starts with its 55).
|
|
70
|
+
const body = bytes[i] === 0xa5 || bytes[i] === 0x7f ? i + 1 : 0;
|
|
71
|
+
const out = new Uint8Array(256 + bytes.length - body);
|
|
72
|
+
out.fill(baud === 500 ? 0x00 : 0x55, 0, 255);
|
|
73
|
+
out[255] = baud === 500 ? 0xa5 : 0x7f;
|
|
74
|
+
out.set(bytes.subarray(body), 256);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The cassette deck: insert a .CAS as a pulse tape at a baud, rewind it, read the motor relay.
|
|
80
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
81
|
+
*/
|
|
82
|
+
export function cassetteDeck(machine) {
|
|
83
|
+
const deck = machine.system.io.cassette;
|
|
84
|
+
return {
|
|
85
|
+
/**
|
|
86
|
+
* @param {Uint8Array} bytes a .CAS image
|
|
87
|
+
* @param {{ baud?: 500 | 1500 }} [opts]
|
|
88
|
+
*/
|
|
89
|
+
insert(bytes, { baud = 500 } = {}) {
|
|
90
|
+
checkBaud(baud, machine);
|
|
91
|
+
deck.insertPulseTape(pulsesFor(machine, bytes, baud));
|
|
92
|
+
},
|
|
93
|
+
/** Back to the start of the tape. */
|
|
94
|
+
rewind() {
|
|
95
|
+
deck.rewindPulseTape();
|
|
96
|
+
},
|
|
97
|
+
/** True while the ROM holds the motor relay on. */
|
|
98
|
+
get motor() {
|
|
99
|
+
return deck.motorOn;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load a .CMD: write its 01 blocks and leave the machine ready to run at the transfer address.
|
|
106
|
+
* Text fed or typed for the previous program is dropped (the keyboard queue is cleared, queued
|
|
107
|
+
* keys released), so the new program's first keyboard read does not get stale input.
|
|
108
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
109
|
+
* @param {Uint8Array} bytes
|
|
110
|
+
* @returns {CmdManifest}
|
|
111
|
+
*/
|
|
112
|
+
export function loadCmd(machine, bytes) {
|
|
113
|
+
const parsed = parseCmd(bytes);
|
|
114
|
+
fastLoadCmd(machine.system, parsed);
|
|
115
|
+
machine.keyboard.clear();
|
|
116
|
+
return {
|
|
117
|
+
format: 'cmd',
|
|
118
|
+
name: parsed.name,
|
|
119
|
+
entry: parsed.entry,
|
|
120
|
+
blocks: parsed.blocks.map((b) => ({ addr: b.addr, len: b.data.length, why: 'record 01' })),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load a .CAS, through the ROM's loader (the tape goes in the deck; the caller types CLOAD, or
|
|
126
|
+
* SYSTEM and the name then `/`) or fast (written now; a SYSTEM tape also jumps to its entry).
|
|
127
|
+
* `baud` applies only via 'rom'; via 'fast' it is ignored and the manifest's `baud` is null.
|
|
128
|
+
* Via 'fast' the keyboard queue is cleared as `load.cmd` does; via 'rom' it is left alone, since
|
|
129
|
+
* the caller types CLOAD or SYSTEM next.
|
|
130
|
+
* @param {import('../machine/machine.js').Machine} machine
|
|
131
|
+
* @param {Uint8Array} bytes
|
|
132
|
+
* @param {{ baud?: 500 | 1500, via?: 'rom' | 'fast' }} [opts]
|
|
133
|
+
* @returns {CasManifest}
|
|
134
|
+
*/
|
|
135
|
+
export function loadCas(machine, bytes, { baud = 500, via = 'rom' } = {}) {
|
|
136
|
+
if (via !== 'rom' && via !== 'fast') throw new Error(`load.cas: via must be 'rom' or 'fast', not ${String(via)}`);
|
|
137
|
+
if (via === 'rom') checkBaud(baud, machine);
|
|
138
|
+
const parsed = parseCas(bytes);
|
|
139
|
+
const system = machine.system;
|
|
140
|
+
|
|
141
|
+
/** @type {ManifestBlock[]} */
|
|
142
|
+
let blocks;
|
|
143
|
+
if (parsed.kind === 'system') {
|
|
144
|
+
blocks = parsed.blocks.map((b) => ({ addr: b.addr, len: b.data.length, why: 'block 3C' }));
|
|
145
|
+
} else {
|
|
146
|
+
// CLOAD and the fast path both put the program at TXTTAB: per line a pointer, a line number, the body, and 00, then 00 00.
|
|
147
|
+
const start = system.memory.readWord(TXTTAB_PTR);
|
|
148
|
+
const len = parsed.lines.reduce((n, l) => n + 5 + l.tokens.length, 2);
|
|
149
|
+
blocks = [{ addr: start, len, why: via === 'rom' ? 'BASIC program (predicted from TXTTAB; CLOAD not yet run)' : 'BASIC program' }];
|
|
150
|
+
}
|
|
151
|
+
// Via 'rom' nothing has landed until the ROM reads the tape: every block is where it will go.
|
|
152
|
+
if (via === 'rom') blocks = blocks.map((b) => ({ ...b, predicted: /** @type {true} */ (true) }));
|
|
153
|
+
|
|
154
|
+
if (via === 'rom') {
|
|
155
|
+
system.io.cassette.insertPulseTape(pulsesFor(machine, bytes, baud));
|
|
156
|
+
} else if (parsed.kind === 'system') {
|
|
157
|
+
fastLoadSystem(system, parsed);
|
|
158
|
+
} else {
|
|
159
|
+
fastLoadBasic(system, parsed);
|
|
160
|
+
}
|
|
161
|
+
if (via === 'fast') machine.keyboard.clear();
|
|
162
|
+
|
|
163
|
+
const head = { format: /** @type {'cas'} */ ('cas'), kind: parsed.kind, name: parsed.name };
|
|
164
|
+
const tail = { blocks, baud: via === 'rom' ? baud : null, via };
|
|
165
|
+
return parsed.kind === 'system' ? { ...head, entry: parsed.entry, ...tail } : { ...head, ...tail };
|
|
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
|
+
}
|