@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.
@@ -0,0 +1,1065 @@
1
+ /**
2
+ * WD1793 floppy disk controller (the Model III disk subsystem).
3
+ *
4
+ * Ports (handled by IOSystem):
5
+ * 0xF0 status (R) / command (W) 0xF2 sector register
6
+ * 0xF1 track register 0xF3 data register
7
+ * 0xF4 (W) drive select: bits 0-3 drive, bit 4 side, bit 6 WAIT, bit 7 MFM
8
+ *
9
+ * The authority is the Western Digital FD179X data sheet; the copy read for this model is the
10
+ * FD179X-01 sheet of October 1979 (bitsavers, components/westernDigital), whose command, flag,
11
+ * and status descriptions the -02 part shares. Comments name its sections in quotes. All time is
12
+ * in CPU T-states, so the controller is as deterministic as the CPU: a command is a chain of
13
+ * timed events (`pending`), advanced by tick(). The CPU clock is a constructor option (`cpuHz`,
14
+ * the Model III's 2.02752 MHz by default); every time below is derived from it, and the exported
15
+ * constants are the Model III's values.
16
+ *
17
+ * The drive-select latch is the machine's: the FDC takes a decoded `select(drive, side, mfm)`,
18
+ * and each model's I/O decodes its own port (the Model III's port F4 in machine/io.js).
19
+ *
20
+ * The disk turns at 300 rpm, one index pulse per 405,504 T-states at 2.02752 MHz (`indexClock`
21
+ * is the angle). IDs pass at the positions DiskImage.trackIds gives, and data bytes arrive one
22
+ * byte time apart: 32 microseconds in MFM and 64 in FM at the 5.25" data rates (250 and 125
23
+ * kbit/s), 65 and 130 T-states at 2.02752 MHz. A byte the CPU has not taken when the next arrives is lost (status bit 2), as on the
24
+ * chip; the Model III's WAIT (port F4 bit 6) lets the ROM and DOSes keep up with INI loops.
25
+ *
26
+ * Model III wiring assumed and not verified against the Technical Reference Manual (spec §5):
27
+ * the controller runs from a 1 MHz clock (the data sheet's "Floppy Disk Interface": 1 MHz for
28
+ * mini-floppies in both densities), so the 1 MHz column of the step rates and 30 ms settling
29
+ * apply; READY is true when the selected drive holds a disk; HLT is always true.
30
+ */
31
+
32
+ import { decodeModel3Select } from "../machine/drive-select.js";
33
+
34
+ // Status bits (data sheet, "Status Register Summary", Table 6)
35
+ const ST_BUSY = 0x01;
36
+ const ST_INDEX = 0x02; // Type I: index pulse
37
+ const ST_DRQ = 0x02; // Type II/III: data request
38
+ const ST_TRACK0 = 0x04; // Type I
39
+ const ST_LOST_DATA = 0x04; // Type II/III
40
+ const ST_CRC_ERROR = 0x08;
41
+ const ST_SEEK_ERROR = 0x10; // Type I
42
+ const ST_RNF = 0x10; // Type II/III: record not found
43
+ const ST_HEAD_LOADED = 0x20; // Type I
44
+ const ST_RECORD_TYPE = 0x20; // Read Sector: deleted data mark
45
+ const ST_WRITE_PROTECT = 0x40;
46
+ const ST_NOT_READY = 0x80;
47
+
48
+ /** The Model III CPU clock (trs80-system.js CPU_CLOCK_HZ), the default `cpuHz`. */
49
+ export const MODEL3_CPU_HZ = 2027520;
50
+
51
+ /**
52
+ * The controller's times in T-states of a CPU clocked at `cpuHz`: one revolution at 300 rpm
53
+ * (0.2 s), the index pulse's width (5.25" drives: about 4 ms; 8,000 T-states at 2.02752 MHz),
54
+ * "Stepping Rates" (Table 1) in the CLK = 1 MHz column for r1 r0 = 00, 01, 10, 11, "Head
55
+ * Positioning"'s 15 ms of settling doubled to 30 ms with a 1 MHz clock, and one byte at 250
56
+ * kbit/s (MFM) and 125 kbit/s (FM): 32 and 64 microseconds.
57
+ * @param {number} cpuHz
58
+ */
59
+ export function fdcTimes(cpuHz) {
60
+ const ms = (/** @type {number} */ n) => Math.round((n * cpuHz) / 1000);
61
+ return {
62
+ indexPeriod: Math.round(cpuHz / 5),
63
+ indexWidth: Math.round((8000 * cpuHz) / MODEL3_CPU_HZ),
64
+ step: [6, 12, 20, 30].map(ms),
65
+ settle: ms(30),
66
+ byteMfm: Math.round((32 * cpuHz) / 1e6),
67
+ byteFm: Math.round((64 * cpuHz) / 1e6),
68
+ };
69
+ }
70
+
71
+ const MODEL3 = fdcTimes(MODEL3_CPU_HZ);
72
+ /** One revolution at 300 rpm on the Model III. */
73
+ export const INDEX_PERIOD = MODEL3.indexPeriod;
74
+ /** "Stepping Rates" (Table 1), the CLK = 1 MHz column, on the Model III. */
75
+ export const STEP_TSTATES = MODEL3.step;
76
+ /** "Head Positioning": 30 ms of settling with a 1 MHz clock, on the Model III. */
77
+ export const SETTLE_TSTATES = MODEL3.settle;
78
+ /** One byte at 250 kbit/s (MFM) and 125 kbit/s (FM), on the Model III. */
79
+ export const BYTE_TSTATES_MFM = MODEL3.byteMfm;
80
+ export const BYTE_TSTATES_FM = MODEL3.byteFm;
81
+
82
+ /**
83
+ * How long an ID search runs before Record Not Found or Seek Error: the data sheet says "within
84
+ * 5 revolutions" (Type I verify, "Head Positioning") and "5 index pulses" (side compare) in some
85
+ * places and "four revolutions" in others ("Type I Commands", "Type II Commands"); the search ends
86
+ * at the fifth index pulse, which satisfies both readings.
87
+ */
88
+ const SEARCH_INDEX_PULSES = 5;
89
+ /** "Type I Commands": idle (not busy) for 15 revolutions, the head is disengaged. */
90
+ const HEAD_UNLOAD_PULSES = 15;
91
+
92
+ // WRITE TRACK capture cap, a guard: one revolution of MFM is 6,238 byte times here.
93
+ const TRACK_WRITE_CAP = 20000;
94
+
95
+ /** CRC-CCITT preset FFFF, as the 179X writes it after ID and data fields. */
96
+ function crc16(bytes, crc = 0xffff) {
97
+ for (const b of bytes) {
98
+ crc ^= b << 8;
99
+ for (let k = 0; k < 8; k++) crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
100
+ }
101
+ return crc;
102
+ }
103
+
104
+ /**
105
+ * Turn the byte stream a formatting DOS sends during WRITE TRACK into
106
+ * sectors. MFM control bytes: 0xF5 writes an A1 sync byte, 0xFE starts
107
+ * an ID field (track, side, sector, size code), 0xFB/0xF8 start a
108
+ * normal/deleted data field, 0xF7 writes the CRC. FM (single density)
109
+ * uses the bare marks after a run of 0x00 sync bytes. The density latch
110
+ * is only a guess about what the DOS meant, so a parse that finds no
111
+ * sectors retries as the other density. With `fmMarks` (the WD1771) the FM
112
+ * data marks FA and F9 start a data field too, and each sector carries
113
+ * its mark as `dam` (FD1771-01 data sheet, "Control Bytes for
114
+ * Initialization": F8, F9, FA, FB are data address marks).
115
+ *
116
+ * @returns {{track:number, side:number, sector:number, size:number,
117
+ * deleted:boolean, data:Uint8Array, mfm:boolean}[]}
118
+ */
119
+ export function parseWriteTrackStream(bytes, mfmGuess = true, fmMarks = false) {
120
+ const first = mfmGuess ? scanMfm(bytes) : scanFm(bytes, fmMarks);
121
+ if (first.length) return first;
122
+ return mfmGuess ? scanFm(bytes, fmMarks) : scanMfm(bytes);
123
+ }
124
+
125
+ function scanMfm(bytes) {
126
+ const sectors = [];
127
+ let i = 0;
128
+ let id = null;
129
+ while (i < bytes.length) {
130
+ if (bytes[i] !== 0xf5) {
131
+ i++;
132
+ continue;
133
+ }
134
+ let j = i;
135
+ while (j < bytes.length && bytes[j] === 0xf5) j++;
136
+ const mark = bytes[j];
137
+ if (mark === 0xfe && j + 4 < bytes.length) {
138
+ id = {
139
+ track: bytes[j + 1],
140
+ side: bytes[j + 2],
141
+ sector: bytes[j + 3],
142
+ size: 128 << (bytes[j + 4] & 3),
143
+ };
144
+ i = j + 5;
145
+ } else if ((mark === 0xfb || mark === 0xf8) && id) {
146
+ sectors.push({
147
+ ...id,
148
+ deleted: mark === 0xf8,
149
+ data: bytes.slice(j + 1, j + 1 + id.size),
150
+ mfm: true,
151
+ });
152
+ i = j + 1 + id.size;
153
+ id = null;
154
+ } else {
155
+ i = j + 1;
156
+ }
157
+ }
158
+ return sectors;
159
+ }
160
+
161
+ function scanFm(bytes, fmMarks = false) {
162
+ const sectors = [];
163
+ let i = 0;
164
+ let id = null;
165
+ let zeros = 0;
166
+ while (i < bytes.length) {
167
+ const b = bytes[i];
168
+ if (b === 0x00) {
169
+ zeros++;
170
+ i++;
171
+ continue;
172
+ }
173
+ if (zeros >= 2) {
174
+ if (b === 0xfe && i + 4 < bytes.length) {
175
+ id = {
176
+ track: bytes[i + 1],
177
+ side: bytes[i + 2],
178
+ sector: bytes[i + 3],
179
+ size: 128 << (bytes[i + 4] & 3),
180
+ };
181
+ zeros = 0;
182
+ i += 5;
183
+ continue;
184
+ }
185
+ if ((b === 0xfb || b === 0xf8 || (fmMarks && (b === 0xfa || b === 0xf9))) && id) {
186
+ sectors.push({
187
+ ...id,
188
+ deleted: b === 0xf8,
189
+ ...(fmMarks ? { dam: b } : {}),
190
+ data: bytes.slice(i + 1, i + 1 + id.size),
191
+ mfm: false,
192
+ });
193
+ zeros = 0;
194
+ i += 1 + id.size;
195
+ id = null;
196
+ continue;
197
+ }
198
+ }
199
+ zeros = 0;
200
+ i++;
201
+ }
202
+ return sectors;
203
+ }
204
+
205
+
206
+ /**
207
+ * A synthesized raw track for READ TRACK on a container that keeps no raw bytes (JV1, JV3): the
208
+ * data sheet's IBM System 34 layout in MFM ("IBM System 34 Format") or IBM 3740 in FM ("IBM 3740
209
+ * Format"), sector data from the image, CRCs computed, padded with gap bytes to `length`. When the
210
+ * IBM gaps do not fit a revolution (18 sectors of 256 bytes in MFM do not), the minimum gaps of
211
+ * "Non-IBM Formats" are used, without the optional index mark.
212
+ */
213
+ function synthesizeTrack(ids, dataOf, mfm, length) {
214
+ const ibm = layTrack(ids, dataOf, mfm, true);
215
+ const laid = ibm.length <= length ? ibm : layTrack(ids, dataOf, mfm, false);
216
+ const out = new Uint8Array(length).fill(mfm ? 0x4e : 0xff);
217
+ out.set(laid.slice(0, length));
218
+ return out;
219
+ }
220
+
221
+ function layTrack(ids, dataOf, mfm, ibm) {
222
+ const out = [];
223
+ const run = (n, v) => {
224
+ for (let k = 0; k < n; k++) out.push(v);
225
+ };
226
+ const sync = mfm ? [0xa1, 0xa1, 0xa1] : [];
227
+ const gapByte = mfm ? 0x4e : 0xff;
228
+ if (!ibm) run(mfm ? 32 : 16, gapByte); // Gap I
229
+ else if (mfm) {
230
+ run(80, 0x4e);
231
+ run(12, 0x00);
232
+ out.push(0xc2, 0xc2, 0xc2, 0xfc);
233
+ run(50, 0x4e);
234
+ } else {
235
+ run(40, 0xff);
236
+ run(6, 0x00);
237
+ out.push(0xfc);
238
+ run(26, 0xff);
239
+ }
240
+ for (const id of ids) {
241
+ run(mfm ? 12 : 6, 0x00);
242
+ const idField = [0xfe, id.track, id.side, id.sector, id.sizeCode];
243
+ const idCrc = crc16([...sync, ...idField]);
244
+ out.push(...sync, ...idField, idCrc >> 8, idCrc & 0xff);
245
+ run(mfm ? 22 : 11, gapByte);
246
+ run(mfm ? 12 : 6, 0x00);
247
+ const data = [id.dam ?? (id.deleted ? 0xf8 : 0xfb), ...(dataOf(id) ?? new Uint8Array(id.size))];
248
+ const dataCrc = crc16([...sync, ...data]);
249
+ out.push(...sync, ...data, dataCrc >> 8, dataCrc & 0xff);
250
+ run(ibm ? (mfm ? 54 : 27) : mfm ? 24 : 10, gapByte); // Gap III
251
+ }
252
+ return out;
253
+ }
254
+
255
+ export class FDC1793 {
256
+ /** @param {{ cpuHz?: number }} [opts] the CPU clock the controller's times count in */
257
+ constructor({ cpuHz = MODEL3_CPU_HZ } = {}) {
258
+ this.cpuHz = cpuHz;
259
+ const t = fdcTimes(cpuHz);
260
+ this.indexPeriod = t.indexPeriod;
261
+ this.indexWidth = t.indexWidth;
262
+ this.stepTStates = t.step;
263
+ this.settleTStates = t.settle;
264
+ this.byteTStatesMfm = t.byteMfm;
265
+ this.byteTStatesFm = t.byteFm;
266
+
267
+ this.drives = [null, null, null, null]; // DiskImage or null
268
+ this.selected = 0; // -1 when a port F4 write selected no drive
269
+ this.side = 0;
270
+ this.mfm = true; // port 0xF4 bit 7 density latch (Model III default DD)
271
+
272
+ this.trackReg = 0;
273
+ this.sectorReg = 0;
274
+ this.dataReg = 0;
275
+ this.physicalTrack = [0, 0, 0, 0];
276
+
277
+ this.status = 0;
278
+ this.intrq = false;
279
+ this.commandType = 1;
280
+ this.command = 0; // the last command byte loaded
281
+ this.stepDir = 1; // +1 toward track 76 (in), -1 toward track 0 (out)
282
+ this.headLoaded = false; // HLD (HLT is taken as always true)
283
+ this.idlePulses = 0; // index pulses while idle with the head loaded
284
+ this.intrqCond = 0; // Force Interrupt condition bits I3-I0
285
+
286
+ // The command in flight: a plain object (it is serialized as it is) naming the next event,
287
+ // the T-states until it, and what the command has to remember.
288
+ this.pending = null;
289
+ this.buffer = null; // sector, ID, or track bytes being read or written
290
+ this.writeDeleted = false; // WRITE SECTOR bit a0: write an F8 data mark
291
+ this.trackWrite = null; // WRITE TRACK capture (array of bytes)
292
+
293
+ this.indexClock = 0;
294
+
295
+ // What differs on the WD1771 (fdc-wd1771.js), at the WD1793's values: index pulses to an ID
296
+ // search's end and to the head's unloading, the sector register's source in Read Address (0,
297
+ // the ID's track), and whether Write Track takes the FM marks FA and F9.
298
+ this.searchPulses = SEARCH_INDEX_PULSES;
299
+ this.headUnloadPulses = HEAD_UNLOAD_PULSES;
300
+ this.readAddressField = 0;
301
+ this.fmMarks = false;
302
+ }
303
+
304
+ attachDrive(n, image) {
305
+ const was = this._ready();
306
+ this.drives[n & 3] = image;
307
+ this._readyChanged(was);
308
+ }
309
+
310
+ ejectDrive(n) {
311
+ const was = this._ready();
312
+ this.drives[n & 3] = null;
313
+ this._readyChanged(was);
314
+ }
315
+
316
+ anyDiskMounted() {
317
+ return this.drives.some((d) => d !== null);
318
+ }
319
+
320
+ currentDisk() {
321
+ return this.selected < 0 ? null : this.drives[this.selected];
322
+ }
323
+
324
+ /** Controller reset with the machine: no command, no interrupt; heads and registers stay. */
325
+ reset() {
326
+ this.intrq = false;
327
+ this.intrqCond = 0;
328
+ this.pending = null;
329
+ this.buffer = null;
330
+ this.trackWrite = null;
331
+ this.status = 0;
332
+ this.commandType = 1;
333
+ }
334
+
335
+ _ready() {
336
+ return this.currentDisk() !== null;
337
+ }
338
+
339
+ /** "Force Interrupt": I0 fires on a not-ready-to-ready transition, I1 on ready-to-not-ready. */
340
+ _readyChanged(was) {
341
+ const now = this._ready();
342
+ if (!was && now && this.intrqCond & 0x01) this.intrq = true;
343
+ if (was && !now && this.intrqCond & 0x02) this.intrq = true;
344
+ }
345
+
346
+ get _track() {
347
+ return this.selected < 0 ? 0 : this.physicalTrack[this.selected];
348
+ }
349
+
350
+ /**
351
+ * The drive-select latch, decoded: `drive` 0-3, or -1 for none; `side` 0 or 1; `mfm` the density
352
+ * (true double, false single). Each model's I/O decodes its own select port into this (the
353
+ * Model III's port F4: machine/drive-select.js, decodeModel3Select, applied in machine/io.js).
354
+ * @param {number} drive
355
+ * @param {number} side
356
+ * @param {boolean} mfm
357
+ */
358
+ select(drive, side, mfm) {
359
+ const was = this._ready();
360
+ this.selected = drive >= 0 && drive <= 3 ? drive : -1;
361
+ this.side = side ? 1 : 0;
362
+ this.mfm = !!mfm;
363
+ this._readyChanged(was);
364
+ }
365
+
366
+ /**
367
+ * A Model III port F4 byte (bits 0-3 drive, bit 4 side, bit 7 MFM), for a bare controller driven
368
+ * without the machine's I/O (tests, probes). The machine decodes port F4 in machine/io.js and
369
+ * calls select(); the decoding is that module's, and this applies it.
370
+ * @param {number} value
371
+ */
372
+ selectDrive(value) {
373
+ const d = decodeModel3Select(value);
374
+ this.select(d.drive, d.side, d.mfm);
375
+ }
376
+
377
+ /**
378
+ * The Model III WAIT (port F4 bit 6): the CPU is held until DRQ or INTRQ rises. Runs the
379
+ * controller forward to that moment and returns the T-states it took, which the caller adds to
380
+ * the CPU's clock; 0 when DRQ or INTRQ is already up or no command is running (nothing would
381
+ * release the line; real hardware would hang, and the model does not).
382
+ *
383
+ * The controller is ticked after each instruction, so when the OUT that sets WAIT runs, its clock
384
+ * still stands at the start of that OUT: the wait runs ahead from there, and the OUT's own
385
+ * T-states (11 for `OUT (n),A`) are ticked after the DRQ. A DRQ can thus release the CPU up to
386
+ * 11 T-states before the OUT would have ended, well inside the 65 T-state MFM byte window, and
387
+ * the ROM's `OUT`/`INI` loop takes every byte either way.
388
+ */
389
+ waitForDrq() {
390
+ let total = 0;
391
+ const cap = (this.searchPulses + 2) * this.indexPeriod;
392
+ while (this.pending !== null && !(this.status & ST_DRQ && this.commandType !== 1) && !this.intrq && total < cap) {
393
+ const d = Math.max(0, this.pending.delay);
394
+ this.tick(d);
395
+ total += d;
396
+ }
397
+ return total;
398
+ }
399
+
400
+ /** Advance emulated time by `cycles` T-states, running the command's events as they fall due. */
401
+ tick(cycles) {
402
+ const p = this.pending;
403
+ if (p !== null) {
404
+ if (p.delay <= cycles) {
405
+ this._runEvents(cycles);
406
+ return;
407
+ }
408
+ p.delay -= cycles;
409
+ }
410
+ this.indexClock += cycles;
411
+ if (this.indexClock >= this.indexPeriod) this._advance(0);
412
+ }
413
+
414
+ _runEvents(cycles) {
415
+ while (this.pending !== null && this.pending.delay <= cycles) {
416
+ const d = Math.max(0, this.pending.delay);
417
+ this._advance(d);
418
+ cycles -= d;
419
+ this.pending.delay = 0;
420
+ this._event();
421
+ }
422
+ if (this.pending !== null) this.pending.delay -= cycles;
423
+ this._advance(cycles);
424
+ }
425
+
426
+ _advance(d) {
427
+ this.indexClock += d;
428
+ while (this.indexClock >= this.indexPeriod) {
429
+ this.indexClock -= this.indexPeriod;
430
+ this._indexPulse();
431
+ }
432
+ }
433
+
434
+ _indexPulse() {
435
+ if (!this._ready()) return; // no disk turning, no index pulses
436
+ if (!(this.status & ST_BUSY) && this.headLoaded && ++this.idlePulses >= this.headUnloadPulses) {
437
+ this.headLoaded = false;
438
+ }
439
+ if (this.intrqCond & 0x04) this.intrq = true; // Force Interrupt I2: every index pulse
440
+ }
441
+
442
+ _byteTime() {
443
+ return this.mfm ? this.byteTStatesMfm : this.byteTStatesFm;
444
+ }
445
+
446
+ /** T-states until the disk turns to `pos` (a fraction of a revolution after the index pulse). */
447
+ _until(pos) {
448
+ const d = Math.round(pos * this.indexPeriod) - this.indexClock;
449
+ return d < 0 ? d + this.indexPeriod : d;
450
+ }
451
+
452
+ /** T-states until the fifth index pulse from now: where an ID search gives up. */
453
+ _searchWindow() {
454
+ return this.indexPeriod - this.indexClock + (this.searchPulses - 1) * this.indexPeriod;
455
+ }
456
+
457
+ /** The IDs the head can read now, in the order they pass: this track side, this density. */
458
+ _passingIds() {
459
+ const disk = this.currentDisk();
460
+ if (!disk) return [];
461
+ return disk
462
+ .trackIds(this._track, this.side)
463
+ .filter((id) => id.mfm === null || id.mfm === this.mfm)
464
+ .map((id) => ({ id, at: this._until(id.pos) }))
465
+ .sort((a, b) => a.at - b.at);
466
+ }
467
+
468
+ _finish() {
469
+ this.status &= ~ST_BUSY;
470
+ this.intrq = true;
471
+ this.pending = null;
472
+ this.buffer = null;
473
+ this.trackWrite = null;
474
+ this.idlePulses = 0;
475
+ }
476
+
477
+ _type1Status() {
478
+ const disk = this.currentDisk();
479
+ let s = this.status & (ST_BUSY | ST_SEEK_ERROR | ST_CRC_ERROR);
480
+ if (!disk) s |= ST_NOT_READY;
481
+ if (disk && disk.writeProtected) s |= ST_WRITE_PROTECT;
482
+ if (this.headLoaded) s |= ST_HEAD_LOADED; // HLD and HLT, HLT taken as true
483
+ if (this.selected >= 0 && this._track === 0) s |= ST_TRACK0;
484
+ if (disk && this.indexClock < this.indexWidth) s |= ST_INDEX;
485
+ return s;
486
+ }
487
+
488
+ /**
489
+ * Port 0xF0 read. Reading status resets INTRQ ("General Disk Read Operations"), except an
490
+ * immediate interrupt (Force Interrupt I3), which only a later D0 lets clear. Type I status is
491
+ * live: Not Ready, Write Protect, Head Loaded, Track 00, and Index copy the drive's lines
492
+ * ("Status for Type I Commands"); Type II and III report Not Ready live too.
493
+ */
494
+ readStatus() {
495
+ if (!(this.intrqCond & 0x08)) this.intrq = false;
496
+ return this.peekStatus();
497
+ }
498
+
499
+ /** The status register as a read returns it, leaving INTRQ alone (for the debugger). */
500
+ peekStatus() {
501
+ if (this.commandType === 1) return this._type1Status();
502
+ return (this.status & ~ST_NOT_READY) | (this._ready() ? 0 : ST_NOT_READY);
503
+ }
504
+
505
+ /** Port 0xF0 write */
506
+ writeCommand(value) {
507
+ value &= 0xff;
508
+ const high = value >> 4;
509
+ if (high === 0x0d) {
510
+ this._forceInterrupt(value);
511
+ return;
512
+ }
513
+ // "Command Description": a command other than Force Interrupt is loaded only when not busy.
514
+ if (this.status & ST_BUSY) return;
515
+ // Loading the command register resets INTRQ, unless an immediate interrupt holds it.
516
+ if (!(this.intrqCond & 0x08)) this.intrq = false;
517
+ this.intrqCond &= 0x08; // conditions I0-I2 end with a new command (unverified)
518
+ this.command = value;
519
+ this.idlePulses = 0;
520
+ if (high <= 0x07) this._startType1(value);
521
+ else if (high <= 0x0b) this._startType2(value);
522
+ else this._startType3(value);
523
+ }
524
+
525
+ /**
526
+ * "Force Interrupt": a running command stops, Busy resets, and the other status bits stay; with
527
+ * none running, Busy resets and the status reflects Type I. I3 interrupts at once and holds
528
+ * INTRQ ("requires reset": only a D0 lets a later status read or command load clear it); I2
529
+ * interrupts at every index pulse; I0 and I1 on ready transitions; none of them: no interrupt.
530
+ */
531
+ _forceInterrupt(value) {
532
+ if (this.status & ST_BUSY) {
533
+ this.pending = null;
534
+ this.buffer = null;
535
+ this.trackWrite = null;
536
+ // DRQ is cleared with Busy. The sheet ("Force Interrupt") says "the Busy status bit is
537
+ // reset, and the rest of the status bits are unchanged", which read literally leaves DRQ
538
+ // set; the model drops it because the stopped command will move no more bytes, so a DRQ
539
+ // left up would request one nobody can take. Unverified: spec §5.
540
+ this.status &= ~(ST_BUSY | (this.commandType === 1 ? 0 : ST_DRQ));
541
+ this.idlePulses = 0;
542
+ } else {
543
+ this.commandType = 1;
544
+ this.status = 0;
545
+ }
546
+ if (!(this.intrqCond & 0x08)) this.intrq = false; // a command load resets INTRQ
547
+ this.intrqCond = value & 0x0f;
548
+ if (this.intrqCond & 0x08) this.intrq = true;
549
+ }
550
+
551
+ // ---------------------------------------------------------------- Type I
552
+
553
+ /**
554
+ * Restore, Seek, Step, Step-In, Step-Out ("Type I Commands"). h loads the head at the start
555
+ * (h = 0 unloads it); one step pulse per r1 r0 period, the head moving one track each; a step
556
+ * outward with Track 00 active issues no pulse and ends with the track register 0 (the flow
557
+ * chart, `_atTrack0Outward`); V verifies after the last step: the head loads, 30 ms
558
+ * settle, then the first ID with a good CRC must carry the track register's number.
559
+ */
560
+ _startType1(value) {
561
+ this.commandType = 1;
562
+ this.status = ST_BUSY; // Seek Error and CRC Error "reset to 0 when updated"
563
+ this.headLoaded = (value & 0x08) !== 0;
564
+ const high = value >> 4;
565
+ const op = high === 0 ? "restore" : high === 1 ? "seek" : "step";
566
+ if (high >= 0x04) this.stepDir = high >= 0x06 ? -1 : 1;
567
+ if (op === "restore") this.stepDir = -1;
568
+ this.pending = {
569
+ kind: "type1",
570
+ phase: "step",
571
+ delay: 0,
572
+ op,
573
+ // Restore issues at most 255 pulses; Step one; Seek until TR = DR.
574
+ left: op === "restore" ? 255 : op === "step" ? 1 : 0,
575
+ update: op === "step" ? (value & 0x10) !== 0 : true,
576
+ verify: (value & 0x04) !== 0,
577
+ rate: this.stepTStates[value & 3],
578
+ };
579
+ }
580
+
581
+ _type1Event(p) {
582
+ if (p.phase === "step") {
583
+ if (p.op === "restore") {
584
+ // "Restore": TR00 is sampled; when active the track register is loaded with zeroes.
585
+ if (this._track === 0 && this.selected >= 0) {
586
+ this.trackReg = 0;
587
+ this._afterSteps(p);
588
+ return;
589
+ }
590
+ if (p.left === 0) {
591
+ // "If the TR00 input does not go active low after 255 stepping pulses ... sets the
592
+ // Seek error status bit."
593
+ this.status |= ST_SEEK_ERROR;
594
+ this._finish();
595
+ return;
596
+ }
597
+ p.left--;
598
+ this._stepPulse(-1);
599
+ p.delay = p.rate;
600
+ return;
601
+ }
602
+ if (p.op === "seek") {
603
+ // "Seek": the track register is updated and step pulses issued until TR = DR.
604
+ if (this.trackReg === this.dataReg) {
605
+ this._afterSteps(p);
606
+ return;
607
+ }
608
+ this.stepDir = this.dataReg > this.trackReg ? 1 : -1;
609
+ this.trackReg = (this.trackReg + this.stepDir) & 0xff;
610
+ if (this._atTrack0Outward(p)) return;
611
+ this._stepPulse(this.stepDir);
612
+ p.delay = p.rate;
613
+ return;
614
+ }
615
+ // Step, Step-In, Step-Out: one pulse; with u = 1 the track register follows it.
616
+ if (p.left === 0) {
617
+ this._afterSteps(p);
618
+ return;
619
+ }
620
+ p.left = 0;
621
+ if (p.update) this.trackReg = (this.trackReg + this.stepDir) & 0xff;
622
+ if (this._atTrack0Outward(p)) return;
623
+ this._stepPulse(this.stepDir);
624
+ p.delay = p.rate;
625
+ return;
626
+ }
627
+ if (p.phase === "settle") {
628
+ this._verify(p);
629
+ return;
630
+ }
631
+ if (p.phase === "verified") {
632
+ this.status |= p.result;
633
+ this._finish();
634
+ }
635
+ }
636
+
637
+ /**
638
+ * The "Type I Command Flow" chart (page 9 of the FD179X-01 sheet): after the track register is
639
+ * updated, "Is head at track 0 and direction = 0?" (TR00 active, stepping out) leads to "0 to
640
+ * TR" and on to the verify branch, with no step pulse and no r1 r0 delay. So a Step-Out or an
641
+ * outward Seek with the head on track 0 ends at once with the track register 0, whatever u is.
642
+ * Needs a selected drive: with none, TR00 is not active.
643
+ */
644
+ _atTrack0Outward(p) {
645
+ if (this.stepDir !== -1 || this.selected < 0 || this._track !== 0) return false;
646
+ this.trackReg = 0;
647
+ this._afterSteps(p);
648
+ return true;
649
+ }
650
+
651
+ _stepPulse(dir) {
652
+ if (this.selected < 0) return;
653
+ const t = this.physicalTrack[this.selected] + dir;
654
+ this.physicalTrack[this.selected] = Math.min(255, Math.max(0, t));
655
+ }
656
+
657
+ _afterSteps(p) {
658
+ if (!p.verify) {
659
+ this._finish();
660
+ return;
661
+ }
662
+ // "h = 0 and V = 1: HLD is set near the end of the command, an internal 15 ms [30 ms at
663
+ // 1 MHz] occurs"; with h = 1 the same delay follows the last step.
664
+ this.headLoaded = true;
665
+ p.phase = "settle";
666
+ p.delay = this.settleTStates;
667
+ }
668
+
669
+ /**
670
+ * Type I verification ("Type I Commands"): IDs are read as they pass. A good CRC decides: the
671
+ * track matches the track register (done) or it does not (Seek Error). A bad CRC sets CRC Error
672
+ * and the next ID is read. With no good ID before the search window ends: Seek Error.
673
+ */
674
+ _verify(p) {
675
+ const B = this._byteTime();
676
+ let crc = 0;
677
+ for (const { id, at } of this._passingIds()) {
678
+ if (!id.idCrcOk) {
679
+ crc = ST_CRC_ERROR;
680
+ continue;
681
+ }
682
+ p.phase = "verified";
683
+ p.result = crc | (id.track === this.trackReg ? 0 : ST_SEEK_ERROR);
684
+ p.delay = at + 7 * B; // the ID field: mark, four bytes, two CRC bytes
685
+ return;
686
+ }
687
+ p.phase = "verified";
688
+ p.result = crc | ST_SEEK_ERROR;
689
+ p.delay = this._searchWindow();
690
+ }
691
+
692
+ // ---------------------------------------------------------------- Type II
693
+
694
+ /**
695
+ * Read Sector, Write Sector ("Type II Commands"): 1 0 0 m S E C 0 and 1 0 1 m S E C a0. Ready is
696
+ * sampled first (not ready: not executed, interrupt); a write to a protected disk is "immediately
697
+ * terminated, an interrupt is generated and the Write Protect status bit is set" ("Disk Write
698
+ * Operation"); the head loads, E adds the 30 ms settle, and the ID search begins.
699
+ */
700
+ _startType2(value) {
701
+ this.commandType = 2;
702
+ this.status = 0;
703
+ const write = (value & 0x20) !== 0;
704
+ if (!this._ready()) {
705
+ this._finish();
706
+ return;
707
+ }
708
+ if (write && this.currentDisk().writeProtected) {
709
+ this.status = ST_WRITE_PROTECT;
710
+ this._finish();
711
+ return;
712
+ }
713
+ this.status = ST_BUSY;
714
+ this.headLoaded = true;
715
+ this.writeDeleted = write && (value & 0x01) !== 0;
716
+ this.pending = {
717
+ kind: write ? "write" : "read",
718
+ phase: "search",
719
+ delay: value & 0x04 ? this.settleTStates : 0,
720
+ multiple: (value & 0x10) !== 0,
721
+ compareSide: (value & 0x02) !== 0,
722
+ sideFlag: (value >> 3) & 1,
723
+ sector: 0, // the ID's sector number once found
724
+ k: 0, // bytes transferred
725
+ };
726
+ }
727
+
728
+ /**
729
+ * The Type II ID search: the next ID with the track register's track, the sector register's
730
+ * sector, the S flag's side when C = 1, and a good CRC. Matching IDs with bad CRCs set CRC Error
731
+ * on the way; none found by the fifth index pulse: Record Not Found.
732
+ */
733
+ _search(p) {
734
+ let crc = 0;
735
+ for (const { id, at } of this._passingIds()) {
736
+ if (id.track !== this.trackReg || id.sector !== this.sectorReg) continue;
737
+ if (p.compareSide && (id.side & 1) !== p.sideFlag) continue;
738
+ if (!id.idCrcOk) {
739
+ crc = ST_CRC_ERROR;
740
+ continue;
741
+ }
742
+ // "Read Sector": no data mark within 30 (FM) or 43 (MFM) bytes of the ID's CRC, and "the ID
743
+ // field is searched for and verified again". Write Sector writes its own mark and needs none.
744
+ if (id.noData && p.kind === "read") continue;
745
+ this.status |= crc;
746
+ return { id, at };
747
+ }
748
+ this.status |= crc;
749
+ return null;
750
+ }
751
+
752
+ _type2Event(p) {
753
+ const B = this._byteTime();
754
+ switch (p.phase) {
755
+ case "search": {
756
+ const found = this._search(p);
757
+ if (!found) {
758
+ p.phase = "rnf";
759
+ p.delay = this._searchWindow();
760
+ return;
761
+ }
762
+ const { id, at } = found;
763
+ p.sector = id.sector;
764
+ p.index = id.index; // which ID, when two on the track share the sector number
765
+ p.size = id.size;
766
+ p.k = 0;
767
+ if (p.kind === "read") {
768
+ // "Read Sector": the data mark's type goes to status bit 5; the first data byte is in
769
+ // the DR one byte time after it has passed the head.
770
+ const read = this.currentDisk().readSectorEx(this._track, this.side, id.sector, id.index);
771
+ this.buffer = read ? read.data : new Uint8Array(id.size);
772
+ p.crcOk = id.dataCrcOk;
773
+ this.status = (this.status & ~ST_RECORD_TYPE) | (id.deleted ? ST_RECORD_TYPE : 0);
774
+ p.phase = "data";
775
+ p.delay = at + (id.dataGap + 1) * B;
776
+ } else {
777
+ // "Write Sector": DRQ once the ID field has passed.
778
+ this.buffer = new Uint8Array(id.size);
779
+ p.phase = "drq";
780
+ p.delay = at + 7 * B;
781
+ }
782
+ return;
783
+ }
784
+ case "rnf":
785
+ this.status |= ST_RNF;
786
+ this._finish();
787
+ return;
788
+ case "data": {
789
+ // A byte reaches the DR; if the computer has not read the last one it is lost.
790
+ if (this.status & ST_DRQ) this.status |= ST_LOST_DATA;
791
+ this.dataReg = this.buffer[p.k++];
792
+ this.status |= ST_DRQ;
793
+ if (p.k < this.buffer.length) p.delay = B;
794
+ else {
795
+ p.phase = "crc";
796
+ p.delay = 2 * B;
797
+ }
798
+ return;
799
+ }
800
+ case "crc":
801
+ // A bad data CRC ends the command, "even if it is a multiple record command".
802
+ if (!p.crcOk) {
803
+ this.status |= ST_CRC_ERROR;
804
+ this._finish();
805
+ return;
806
+ }
807
+ this._nextRecord(p);
808
+ return;
809
+ case "drq":
810
+ this.status |= ST_DRQ;
811
+ p.phase = "gate";
812
+ p.delay = (this.mfm ? 22 : 11) * B;
813
+ return;
814
+ case "gate":
815
+ // "counts off 11 bytes in single density and 22 bytes in double density ... If DRQ has
816
+ // not been serviced, the command is terminated and the Lost Data status bit is set."
817
+ if (this.status & ST_DRQ) {
818
+ this.status = (this.status & ~ST_DRQ) | ST_LOST_DATA;
819
+ this._finish();
820
+ return;
821
+ }
822
+ // Then 6 (FM) or 12 (MFM) zeros, the A1s in MFM, and the data mark go out first.
823
+ p.phase = "wbyte";
824
+ p.delay = (this.mfm ? 12 + 3 + 1 : 6 + 1) * B;
825
+ return;
826
+ case "wbyte": {
827
+ // The DR goes to the shift register; "If the DRQ is not serviced in time ... a byte of
828
+ // zeros is written on the disk", Lost Data set, the command goes on.
829
+ let byte = this.dataReg;
830
+ if (p.k > 0 && this.status & ST_DRQ) {
831
+ byte = 0;
832
+ this.status |= ST_LOST_DATA;
833
+ }
834
+ this.buffer[p.k++] = byte;
835
+ if (p.k < this.buffer.length) {
836
+ this.status |= ST_DRQ;
837
+ p.delay = B;
838
+ } else {
839
+ p.phase = "wcrc";
840
+ p.delay = 3 * B; // two CRC bytes and one byte of ones
841
+ }
842
+ return;
843
+ }
844
+ case "wcrc":
845
+ // `p.dam` is the WD1771's a1 a0 mark (fdc-wd1771.js); the WD1793 writes FB or F8 by a0
846
+ this.currentDisk()?.writeSector(this._track, this.side, p.sector, this.buffer, {
847
+ deleted: this.writeDeleted,
848
+ dam: p.dam,
849
+ index: p.index,
850
+ });
851
+ this._nextRecord(p);
852
+ return;
853
+ default:
854
+ this._finish();
855
+ }
856
+ }
857
+
858
+ /** "m = 1": the sector register counts up and the next record follows, until one is not found. */
859
+ _nextRecord(p) {
860
+ if (!p.multiple) {
861
+ this._finish();
862
+ return;
863
+ }
864
+ this.sectorReg = (this.sectorReg + 1) & 0xff;
865
+ this.buffer = null;
866
+ p.phase = "search";
867
+ p.delay = 0;
868
+ }
869
+
870
+ // ---------------------------------------------------------------- Type III
871
+
872
+ /**
873
+ * Read Address (1 1 0 0 0 E 0 0), Read Track (1 1 1 0 0 E 0 0), Write Track (1 1 1 1 0 E 0 0)
874
+ * ("Type III Commands"). Ready is sampled as for Type II; Write Track on a protected disk stops
875
+ * at once with Write Protect.
876
+ */
877
+ _startType3(value) {
878
+ this.commandType = 3;
879
+ this.status = 0;
880
+ const high = value >> 4;
881
+ const kind = high === 0x0c ? "address" : high === 0x0e ? "track" : "format";
882
+ if (!this._ready()) {
883
+ this._finish();
884
+ return;
885
+ }
886
+ if (kind === "format" && this.currentDisk().writeProtected) {
887
+ this.status = ST_WRITE_PROTECT;
888
+ this._finish();
889
+ return;
890
+ }
891
+ this.status = ST_BUSY;
892
+ this.headLoaded = true;
893
+ this.pending = { kind, phase: "start", delay: value & 0x04 ? this.settleTStates : 0, k: 0 };
894
+ // "Write Track": "The Data Request is activated immediately upon receiving the command".
895
+ if (kind === "format") this.status |= ST_DRQ;
896
+ }
897
+
898
+ _type3Event(p) {
899
+ const B = this._byteTime();
900
+ if (p.kind === "address") {
901
+ if (p.phase === "start") {
902
+ // "Read Address": the next ID field's six bytes, a DRQ for each.
903
+ const next = this._passingIds()[0];
904
+ if (!next) {
905
+ p.phase = "rnf";
906
+ p.delay = this._searchWindow();
907
+ return;
908
+ }
909
+ const { id, at } = next;
910
+ const crc = crc16([...(this.mfm ? [0xa1, 0xa1, 0xa1] : []), 0xfe, id.track, id.side, id.sector, id.sizeCode]);
911
+ // A DMK ID with a bad CRC shows its own CRC bytes; the model keeps them good-looking but
912
+ // flags the error, which is what the status reports.
913
+ this.buffer = Uint8Array.from([id.track, id.side, id.sector, id.sizeCode, crc >> 8, crc & 0xff]);
914
+ p.crcOk = id.idCrcOk;
915
+ // No longer read (the sector register takes buffer[readAddressField]); kept so a Read
916
+ // Address in flight serializes as it did before emu phase 6
917
+ p.idTrack = id.track;
918
+ p.phase = "data";
919
+ p.delay = at + 2 * B;
920
+ return;
921
+ }
922
+ if (p.phase === "data") {
923
+ if (this.status & ST_DRQ) this.status |= ST_LOST_DATA;
924
+ this.dataReg = this.buffer[p.k++];
925
+ this.status |= ST_DRQ;
926
+ if (p.k < this.buffer.length) {
927
+ p.delay = B;
928
+ return;
929
+ }
930
+ // "The Track Address of the ID field is written into the sector register" (the WD1771
931
+ // writes the sector address: `readAddressField`); the CRC is checked.
932
+ this.sectorReg = this.buffer[this.readAddressField];
933
+ if (!p.crcOk) this.status |= ST_CRC_ERROR;
934
+ this._finish();
935
+ return;
936
+ }
937
+ this.status |= ST_RNF;
938
+ this._finish();
939
+ return;
940
+ }
941
+
942
+ const perRevolution = Math.floor(this.indexPeriod / B);
943
+ if (p.phase === "start") {
944
+ // Both start "with the leading edge of the first encountered index pulse".
945
+ p.phase = "index";
946
+ p.delay = this.indexPeriod - this.indexClock;
947
+ return;
948
+ }
949
+ if (p.kind === "track") {
950
+ if (p.phase === "index") {
951
+ // The drive went empty or was deselected while the command waited for the index pulse
952
+ // (an OUT to the select port, or m.fdc.unmount). With no disk turning there is no index
953
+ // pulse and nothing to read; the chip would wait for one that never comes, and the model
954
+ // ends the command instead, as Type II and III end when Ready is false at the start: Busy
955
+ // reset, an interrupt, and Not Ready in the status, which reports it live (spec §5,
956
+ // unverified).
957
+ if (!this._ready()) {
958
+ this._finish();
959
+ return;
960
+ }
961
+ this.buffer = this._rawTrack(perRevolution);
962
+ p.k = 0;
963
+ p.phase = "data";
964
+ p.delay = B;
965
+ return;
966
+ }
967
+ // "Read Track": every byte, gaps included, no CRC check, until the next index pulse.
968
+ if (this.status & ST_DRQ) this.status |= ST_LOST_DATA;
969
+ this.dataReg = this.buffer[p.k++];
970
+ this.status |= ST_DRQ;
971
+ if (p.k < this.buffer.length) p.delay = B;
972
+ else this._finish();
973
+ return;
974
+ }
975
+ // "Write Track"
976
+ if (p.phase === "index") {
977
+ // No disk turning: no index pulse to start at; ended as Read Track's is (above).
978
+ if (!this._ready()) {
979
+ this.status &= ~ST_DRQ;
980
+ this._finish();
981
+ return;
982
+ }
983
+ // "If the DR has not been loaded by the time the index pulse is encountered the operation
984
+ // is terminated making the device Not Busy, the Lost Data Status Bit is set".
985
+ if (this.status & ST_DRQ) {
986
+ this.status = (this.status & ~ST_DRQ) | ST_LOST_DATA;
987
+ this._finish();
988
+ return;
989
+ }
990
+ this.trackWrite = [];
991
+ p.phase = "write";
992
+ p.delay = 0;
993
+ return;
994
+ }
995
+ if (p.k < perRevolution) {
996
+ // "If a byte is not present in the DR when needed, a byte of zeros is substituted."
997
+ let byte = this.dataReg;
998
+ if (p.k > 0 && this.status & ST_DRQ) {
999
+ byte = 0;
1000
+ this.status |= ST_LOST_DATA;
1001
+ }
1002
+ if (this.trackWrite.length < TRACK_WRITE_CAP) this.trackWrite.push(byte);
1003
+ p.k++;
1004
+ this.status |= ST_DRQ;
1005
+ p.delay = B;
1006
+ return;
1007
+ }
1008
+ // The next index pulse: the track is written. The byte stream becomes sectors on the image.
1009
+ const sectors = parseWriteTrackStream(new Uint8Array(this.trackWrite), this.mfm, this.fmMarks);
1010
+ const disk = this.currentDisk();
1011
+ if (disk && sectors.length) disk.formatTrack(this._track, this.side, sectors);
1012
+ this.status &= ~ST_DRQ;
1013
+ this._finish();
1014
+ }
1015
+
1016
+ /** The bytes READ TRACK delivers in one revolution at the latched density. */
1017
+ _rawTrack(length) {
1018
+ const disk = this.currentDisk();
1019
+ if (!disk) return new Uint8Array(length).fill(this.mfm ? 0x4e : 0xff); // callers check Ready first
1020
+ const raw = disk.rawTrack(this._track, this.side);
1021
+ if (raw) {
1022
+ const step = !this.mfm && raw.doubled ? 2 : 1;
1023
+ const out = new Uint8Array(length).fill(this.mfm ? 0x4e : 0xff);
1024
+ for (let i = 0, j = 0; i < length && j < raw.bytes.length; i++, j += step) out[i] = raw.bytes[j];
1025
+ return out;
1026
+ }
1027
+ const ids = this._passingIds()
1028
+ .map((x) => x.id)
1029
+ .sort((a, b) => a.pos - b.pos);
1030
+ return synthesizeTrack(ids, (id) => disk.readSectorEx(this._track, this.side, id.sector, id.index)?.data, this.mfm, length);
1031
+ }
1032
+
1033
+ _event() {
1034
+ const p = this.pending;
1035
+ if (p.kind === "type1") this._type1Event(p);
1036
+ else if (p.kind === "read" || p.kind === "write") this._type2Event(p);
1037
+ else if (p.kind === "address" || p.kind === "track" || p.kind === "format") this._type3Event(p);
1038
+ else this._finish(); // a command from a save state older than this model
1039
+ }
1040
+
1041
+ readTrackReg() {
1042
+ return this.trackReg;
1043
+ }
1044
+ writeTrackReg(v) {
1045
+ this.trackReg = v & 0xff;
1046
+ }
1047
+ readSectorReg() {
1048
+ return this.sectorReg;
1049
+ }
1050
+ writeSectorReg(v) {
1051
+ this.sectorReg = v & 0xff;
1052
+ }
1053
+
1054
+ /** Port 0xF3 read: the data register; reading it resets DRQ. */
1055
+ readData() {
1056
+ if (this.commandType !== 1) this.status &= ~ST_DRQ;
1057
+ return this.dataReg;
1058
+ }
1059
+
1060
+ /** Port 0xF3 write: the data register; loading it resets DRQ on a write command. */
1061
+ writeData(v) {
1062
+ this.dataReg = v & 0xff;
1063
+ if (this.commandType !== 1) this.status &= ~ST_DRQ;
1064
+ }
1065
+ }