@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,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
|
+
}
|