@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.
@@ -1,41 +1,106 @@
1
1
  /**
2
- * WD1793 Floppy Disk Controller (Model III disk subsystem)
2
+ * WD1793 floppy disk controller (the Model III disk subsystem).
3
3
  *
4
4
  * Ports (handled by IOSystem):
5
5
  * 0xF0 status (R) / command (W) 0xF2 sector register
6
6
  * 0xF1 track register 0xF3 data register
7
- * 0xF4 (W) drive select: bits 0-3 drive, bit 4 side, bit 7 MFM
7
+ * 0xF4 (W) drive select: bits 0-3 drive, bit 4 side, bit 6 WAIT, bit 7 MFM
8
8
  *
9
- * Implements what the boot ROM and LDOS/TRSDOS-class DOSes use: Type I
10
- * RESTORE/SEEK/STEP, Type II READ/WRITE SECTOR with a DRQ byte pump,
11
- * Type III READ ADDRESS, Type IV FORCE INTERRUPT. Command completion is
12
- * delayed in emulated T-states via tick(); INTRQ is exposed for the
13
- * 0xE4 NMI latch. Sector data comes from DiskImage objects attached per
14
- * drive.
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.
15
30
  */
16
31
 
17
- // Status bits (Type I / common)
32
+ import { decodeModel3Select } from "../machine/drive-select.js";
33
+
34
+ // Status bits (data sheet, "Status Register Summary", Table 6)
18
35
  const ST_BUSY = 0x01;
19
36
  const ST_INDEX = 0x02; // Type I: index pulse
20
37
  const ST_DRQ = 0x02; // Type II/III: data request
21
- const ST_TRACK0 = 0x04;
38
+ const ST_TRACK0 = 0x04; // Type I
39
+ const ST_LOST_DATA = 0x04; // Type II/III
22
40
  const ST_CRC_ERROR = 0x08;
23
- const ST_RNF = 0x10; // record not found / seek error
24
- const ST_HEAD_LOADED = 0x20;
25
- const ST_RECORD_TYPE = 0x20; // Type II read: deleted data mark (0xF8)
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
26
45
  const ST_WRITE_PROTECT = 0x40;
27
46
  const ST_NOT_READY = 0x80;
28
47
 
29
- // Index hole: 300 rpm = one pulse per 405,504 T-states, ~6k wide
30
- const INDEX_PERIOD = 405504;
31
- const INDEX_WIDTH = 8000;
48
+ /** The Model III CPU clock (trs80-system.js CPU_CLOCK_HZ), the default `cpuHz`. */
49
+ export const MODEL3_CPU_HZ = 2027520;
32
50
 
33
- const COMPLETION_DELAY = 12000; // T-states for Type I/II command spin-up
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
+ }
34
70
 
35
- // WRITE TRACK capture cap: a DD track is ~6250 bytes; an unthrottled
36
- // DOS can shovel more filler than that in one emulated revolution.
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.
37
93
  const TRACK_WRITE_CAP = 20000;
38
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
+
39
104
  /**
40
105
  * Turn the byte stream a formatting DOS sends during WRITE TRACK into
41
106
  * sectors. MFM control bytes: 0xF5 writes an A1 sync byte, 0xFE starts
@@ -43,15 +108,18 @@ const TRACK_WRITE_CAP = 20000;
43
108
  * normal/deleted data field, 0xF7 writes the CRC. FM (single density)
44
109
  * uses the bare marks after a run of 0x00 sync bytes. The density latch
45
110
  * is only a guess about what the DOS meant, so a parse that finds no
46
- * sectors retries as the other density.
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).
47
115
  *
48
116
  * @returns {{track:number, side:number, sector:number, size:number,
49
117
  * deleted:boolean, data:Uint8Array, mfm:boolean}[]}
50
118
  */
51
- export function parseWriteTrackStream(bytes, mfmGuess = true) {
52
- const first = mfmGuess ? scanMfm(bytes) : scanFm(bytes);
119
+ export function parseWriteTrackStream(bytes, mfmGuess = true, fmMarks = false) {
120
+ const first = mfmGuess ? scanMfm(bytes) : scanFm(bytes, fmMarks);
53
121
  if (first.length) return first;
54
- return mfmGuess ? scanFm(bytes) : scanMfm(bytes);
122
+ return mfmGuess ? scanFm(bytes, fmMarks) : scanMfm(bytes);
55
123
  }
56
124
 
57
125
  function scanMfm(bytes) {
@@ -90,7 +158,7 @@ function scanMfm(bytes) {
90
158
  return sectors;
91
159
  }
92
160
 
93
- function scanFm(bytes) {
161
+ function scanFm(bytes, fmMarks = false) {
94
162
  const sectors = [];
95
163
  let i = 0;
96
164
  let id = null;
@@ -114,10 +182,11 @@ function scanFm(bytes) {
114
182
  i += 5;
115
183
  continue;
116
184
  }
117
- if ((b === 0xfb || b === 0xf8) && id) {
185
+ if ((b === 0xfb || b === 0xf8 || (fmMarks && (b === 0xfa || b === 0xf9))) && id) {
118
186
  sectors.push({
119
187
  ...id,
120
188
  deleted: b === 0xf8,
189
+ ...(fmMarks ? { dam: b } : {}),
121
190
  data: bytes.slice(i + 1, i + 1 + id.size),
122
191
  mfm: false,
123
192
  });
@@ -133,11 +202,72 @@ function scanFm(bytes) {
133
202
  return sectors;
134
203
  }
135
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
+
136
255
  export class FDC1793 {
137
- constructor() {
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
+
138
267
  this.drives = [null, null, null, null]; // DiskImage or null
139
- this.selected = 0;
268
+ this.selected = 0; // -1 when a port F4 write selected no drive
140
269
  this.side = 0;
270
+ this.mfm = true; // port 0xF4 bit 7 density latch (Model III default DD)
141
271
 
142
272
  this.trackReg = 0;
143
273
  this.sectorReg = 0;
@@ -147,26 +277,40 @@ export class FDC1793 {
147
277
  this.status = 0;
148
278
  this.intrq = false;
149
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
150
285
 
151
- // In-flight operation
152
- this.pending = null; // { kind, delay }
153
- this.buffer = null; // Uint8Array being pumped
154
- this.bufferPos = 0;
155
- this.writing = false;
156
- this.writeDeleted = false; // WRITE SECTOR bit a0: write a 0xF8 DAM
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
157
291
  this.trackWrite = null; // WRITE TRACK capture (array of bytes)
158
- this.mfm = true; // port 0xF4 bit 7 density latch (Model III default DD)
159
- this._readAddrIndex = 0; // READ ADDRESS round-robins the track's IDs
160
292
 
161
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;
162
302
  }
163
303
 
164
304
  attachDrive(n, image) {
305
+ const was = this._ready();
165
306
  this.drives[n & 3] = image;
307
+ this._readyChanged(was);
166
308
  }
167
309
 
168
310
  ejectDrive(n) {
311
+ const was = this._ready();
169
312
  this.drives[n & 3] = null;
313
+ this._readyChanged(was);
170
314
  }
171
315
 
172
316
  anyDiskMounted() {
@@ -174,245 +318,724 @@ export class FDC1793 {
174
318
  }
175
319
 
176
320
  currentDisk() {
177
- return this.drives[this.selected];
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];
178
348
  }
179
349
 
180
- /** Port 0xF4 write */
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
+ */
181
372
  selectDrive(value) {
182
- if (value & 0x01) this.selected = 0;
183
- else if (value & 0x02) this.selected = 1;
184
- else if (value & 0x04) this.selected = 2;
185
- else if (value & 0x08) this.selected = 3;
186
- this.side = value & 0x10 ? 1 : 0;
187
- this.mfm = (value & 0x80) !== 0;
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;
188
398
  }
189
399
 
190
- /** Advance emulated time; completes in-flight commands. */
400
+ /** Advance emulated time by `cycles` T-states, running the command's events as they fall due. */
191
401
  tick(cycles) {
192
- this.indexClock = (this.indexClock + cycles) % INDEX_PERIOD;
193
- if (this.pending) {
194
- this.pending.delay -= cycles;
195
- if (this.pending.delay <= 0) {
196
- const kind = this.pending.kind;
197
- this.pending = null;
198
- this._complete(kind);
402
+ const p = this.pending;
403
+ if (p !== null) {
404
+ if (p.delay <= cycles) {
405
+ this._runEvents(cycles);
406
+ return;
199
407
  }
408
+ p.delay -= cycles;
200
409
  }
410
+ this.indexClock += cycles;
411
+ if (this.indexClock >= this.indexPeriod) this._advance(0);
201
412
  }
202
413
 
203
- _complete(kind) {
204
- const disk = this.currentDisk();
205
- switch (kind) {
206
- case "type1":
207
- this.status = this._type1Status();
208
- this._finish();
209
- break;
210
-
211
- case "read-sector": {
212
- const read = disk
213
- ? disk.readSectorEx(this.physicalTrack[this.selected], this.side, this.sectorReg)
214
- : null;
215
- if (!read) {
216
- this.status = ST_RNF;
217
- this._finish();
218
- } else {
219
- this.buffer = read.data;
220
- this.bufferPos = 0;
221
- this.writing = false;
222
- // The 179x knows the DAM before the first data byte, and DOSes
223
- // check bit 5 after completion — _finish only drops BUSY|DRQ,
224
- // so the record type survives until the next command.
225
- this.status = ST_BUSY | ST_DRQ | (read.deleted ? ST_RECORD_TYPE : 0);
226
- }
227
- break;
228
- }
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
+ }
229
425
 
230
- case "write-sector": {
231
- if (!disk) {
232
- this.status = ST_RNF;
233
- this._finish();
234
- } else if (disk.writeProtected) {
235
- this.status = ST_WRITE_PROTECT;
236
- this._finish();
237
- } else {
238
- // Probe the sector exists before accepting data
239
- const probe = disk.readSector(this.physicalTrack[this.selected], this.side, this.sectorReg);
240
- if (!probe) {
241
- this.status = ST_RNF;
242
- this._finish();
243
- } else {
244
- this.buffer = new Uint8Array(probe.length);
245
- this.bufferPos = 0;
246
- this.writing = true;
247
- this.status = ST_BUSY | ST_DRQ;
248
- }
249
- }
250
- break;
251
- }
426
+ _advance(d) {
427
+ this.indexClock += d;
428
+ while (this.indexClock >= this.indexPeriod) {
429
+ this.indexClock -= this.indexPeriod;
430
+ this._indexPulse();
431
+ }
432
+ }
252
433
 
253
- case "read-address": {
254
- // 6 ID bytes: track, side, sector, size code, CRC1, CRC2 — from
255
- // the real IDs on the track, round-robin like passing IDAMs
256
- // (FORMAT verify passes read these to check the seek landed).
257
- const track = this.physicalTrack[this.selected];
258
- const ids = disk ? disk.sectorsOn(track, this.side) : [];
259
- if (ids.length === 0) {
260
- this.status = ST_RNF;
261
- this._finish();
262
- break;
263
- }
264
- const id = ids[this._readAddrIndex++ % ids.length];
265
- this.buffer = new Uint8Array([track, this.side, id.sector, id.sizeCode, 0, 0]);
266
- // The 1793 copies the ID's track address into the sector register
267
- this.sectorReg = track;
268
- this.bufferPos = 0;
269
- this.writing = false;
270
- this.status = ST_BUSY | ST_DRQ;
271
- break;
272
- }
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
+ }
273
441
 
274
- case "write-track": {
275
- const stream = new Uint8Array(this.trackWrite || []);
276
- this.trackWrite = null;
277
- const sectors = parseWriteTrackStream(stream, this.mfm);
278
- if (disk && sectors.length) {
279
- disk.formatTrack(this.physicalTrack[this.selected], this.side, sectors);
280
- }
281
- this.status = 0;
282
- this._finish();
283
- break;
284
- }
442
+ _byteTime() {
443
+ return this.mfm ? this.byteTStatesMfm : this.byteTStatesFm;
444
+ }
285
445
 
286
- default:
287
- this._finish();
288
- }
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);
289
466
  }
290
467
 
291
468
  _finish() {
292
- // Completion drops BUSY and DRQ together (type I status is
293
- // recomputed live on the next read, so losing bit 1 there is fine)
294
- this.status &= ~(ST_BUSY | ST_DRQ);
469
+ this.status &= ~ST_BUSY;
295
470
  this.intrq = true;
471
+ this.pending = null;
296
472
  this.buffer = null;
297
- this.writing = false;
298
473
  this.trackWrite = null;
474
+ this.idlePulses = 0;
299
475
  }
300
476
 
301
477
  _type1Status() {
302
478
  const disk = this.currentDisk();
303
- let s = ST_HEAD_LOADED;
479
+ let s = this.status & (ST_BUSY | ST_SEEK_ERROR | ST_CRC_ERROR);
304
480
  if (!disk) s |= ST_NOT_READY;
305
- if (this.physicalTrack[this.selected] === 0) s |= ST_TRACK0;
306
481
  if (disk && disk.writeProtected) s |= ST_WRITE_PROTECT;
307
- if (this.indexClock < INDEX_WIDTH) s |= ST_INDEX;
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;
308
485
  return s;
309
486
  }
310
487
 
311
- /** Port 0xF0 read */
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
+ */
312
494
  readStatus() {
313
- this.intrq = false; // reading status clears INTRQ
314
- if (this.commandType === 1) {
315
- // Recompute live bits (index pulse, track 0)
316
- if (!(this.status & ST_BUSY)) {
317
- this.status = this._type1Status();
318
- }
319
- return this.status;
320
- }
321
- return this.status;
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);
322
503
  }
323
504
 
324
505
  /** Port 0xF0 write */
325
506
  writeCommand(value) {
326
- this.intrq = false;
507
+ value &= 0xff;
327
508
  const high = value >> 4;
328
-
329
- // Type IV: force interrupt
330
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) {
331
533
  this.pending = null;
332
534
  this.buffer = null;
333
535
  this.trackWrite = null;
334
- this.status &= ~ST_BUSY;
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 {
335
543
  this.commandType = 1;
336
- this.status = this._type1Status();
337
- if (value & 0x08) this.intrq = true;
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);
338
629
  return;
339
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
+ }
340
650
 
341
- if (this.status & ST_BUSY && this.pending) {
342
- return; // busy: ignore new commands (except force interrupt above)
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;
343
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
+ }
344
668
 
345
- if (high <= 0x07) {
346
- // Type I: restore/seek/step family
347
- this.commandType = 1;
348
- const drive = this.selected;
349
- if (high === 0x00) {
350
- this.physicalTrack[drive] = 0;
351
- this.trackReg = 0;
352
- } else if (high === 0x01) {
353
- this.physicalTrack[drive] = this.dataReg;
354
- this.trackReg = this.dataReg;
355
- } else {
356
- // STEP/STEP-IN/STEP-OUT with optional track update (bit 4)
357
- const dir = high >= 0x06 ? -1 : high >= 0x04 ? 1 : this._lastStepDir || 1;
358
- if (high >= 0x04) this._lastStepDir = dir;
359
- this.physicalTrack[drive] = Math.max(0, this.physicalTrack[drive] + dir);
360
- if (value & 0x10) this.trackReg = this.physicalTrack[drive];
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;
361
681
  }
362
- this.status = ST_BUSY;
363
- this.pending = { kind: "type1", delay: COMPLETION_DELAY };
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
364
685
  return;
365
686
  }
687
+ p.phase = "verified";
688
+ p.result = crc | ST_SEEK_ERROR;
689
+ p.delay = this._searchWindow();
690
+ }
366
691
 
367
- if (high === 0x08 || high === 0x09) {
368
- // READ SECTOR (multi-sector treated as single)
369
- this.commandType = 2;
370
- this.status = ST_BUSY;
371
- this.pending = { kind: "read-sector", delay: COMPLETION_DELAY };
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();
372
706
  return;
373
707
  }
374
- if (high === 0x0a || high === 0x0b) {
375
- // WRITE SECTOR (bit 0 selects the DAM: 1 = deleted/0xF8)
376
- this.commandType = 2;
377
- this.writeDeleted = (value & 0x01) !== 0;
378
- this.status = ST_BUSY;
379
- this.pending = { kind: "write-sector", delay: COMPLETION_DELAY };
708
+ if (write && this.currentDisk().writeProtected) {
709
+ this.status = ST_WRITE_PROTECT;
710
+ this._finish();
380
711
  return;
381
712
  }
382
- if (high === 0x0c) {
383
- // READ ADDRESS
384
- this.commandType = 3;
385
- this.status = ST_BUSY;
386
- this.pending = { kind: "read-address", delay: COMPLETION_DELAY };
387
- return;
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();
388
855
  }
389
- if (high === 0x0e) {
390
- // READ TRACK: rarely used; accept and complete with no data
391
- this.commandType = 3;
392
- this.status = ST_BUSY;
393
- this.pending = { kind: "noop", delay: COMPLETION_DELAY };
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();
394
862
  return;
395
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
396
871
 
397
- // WRITE TRACK (0xF): a formatting DOS streams a whole track image
398
- // through the data register; the pass ends at the next index pulse
399
- // (one revolution), when the stream is parsed into sectors and the
400
- // track is rebuilt on the mounted image.
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) {
401
878
  this.commandType = 3;
402
- const disk = this.currentDisk();
403
- if (!disk) {
404
- this.status = ST_NOT_READY;
405
- this.intrq = true;
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();
406
884
  return;
407
885
  }
408
- if (disk.writeProtected) {
886
+ if (kind === "format" && this.currentDisk().writeProtected) {
409
887
  this.status = ST_WRITE_PROTECT;
410
- this.intrq = true;
888
+ this._finish();
411
889
  return;
412
890
  }
413
- this.trackWrite = [];
414
- this.status = ST_BUSY | ST_DRQ;
415
- this.pending = { kind: "write-track", delay: INDEX_PERIOD };
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
416
1039
  }
417
1040
 
418
1041
  readTrackReg() {
@@ -428,39 +1051,15 @@ export class FDC1793 {
428
1051
  this.sectorReg = v & 0xff;
429
1052
  }
430
1053
 
431
- /** Port 0xF3 read: pump read buffer */
1054
+ /** Port 0xF3 read: the data register; reading it resets DRQ. */
432
1055
  readData() {
433
- if (this.buffer && !this.writing) {
434
- this.dataReg = this.buffer[this.bufferPos++];
435
- if (this.bufferPos >= this.buffer.length) {
436
- this._finish();
437
- }
438
- }
1056
+ if (this.commandType !== 1) this.status &= ~ST_DRQ;
439
1057
  return this.dataReg;
440
1058
  }
441
1059
 
442
- /** Port 0xF3 write: pump write buffer */
1060
+ /** Port 0xF3 write: the data register; loading it resets DRQ on a write command. */
443
1061
  writeData(v) {
444
1062
  this.dataReg = v & 0xff;
445
- if (this.trackWrite) {
446
- if (this.trackWrite.length < TRACK_WRITE_CAP) {
447
- this.trackWrite.push(this.dataReg);
448
- }
449
- return;
450
- }
451
- if (this.buffer && this.writing) {
452
- this.buffer[this.bufferPos++] = this.dataReg;
453
- if (this.bufferPos >= this.buffer.length) {
454
- const disk = this.currentDisk();
455
- disk.writeSector(
456
- this.physicalTrack[this.selected],
457
- this.side,
458
- this.sectorReg,
459
- this.buffer,
460
- { deleted: this.writeDeleted }
461
- );
462
- this._finish();
463
- }
464
- }
1063
+ if (this.commandType !== 1) this.status &= ~ST_DRQ;
465
1064
  }
466
1065
  }