@fugloy/emu 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * WD1771 floppy disk controller: the Model I Expansion Interface's (emu phase 6, Ruling 5).
3
+ *
4
+ * Memory-mapped by the Expansion Interface (machine/io.js, readExpansion and writeExpansion):
5
+ * $37EC status (R) / command (W) $37EE sector register
6
+ * $37ED track register $37EF data register
7
+ * $37E1 (W) drive select: bits 0-3 drives 0-3 (machine/io.js, decodeModel1Select)
8
+ * The addresses are CONSTANTS.Z80's "Memory Mapped Hardware, Model 1" (kiwisincebirth/TRS-80-ROMS)
9
+ * and the Level II ROM's boot (MDL1LEV2.Z80, BOOT at $069F: drive 0 selected by writing 1 to $37E1,
10
+ * Restore written to $37EC, the sector read through $37EF).
11
+ *
12
+ * The authority is the FD1771-01 data sheet (Western Digital), as reprinted in George Blank,
13
+ * "Pathways Through the ROM" (1980), chapter 14, pp. 93-111 (OCR in
14
+ * docs/reference/web-sources/archive-org/pathways_djvu.txt); comments name its sections in
15
+ * quotes. The 1771 is the WD1793's predecessor, and this class is the phase 5 controller
16
+ * (fdc-wd1793.js) with the 1771's differences:
17
+ *
18
+ * - Single density only: no density latch, every ID recorded in MFM is invisible to it ("The
19
+ * FD1771 is compatible with the IBM 3740 data entry system format"). The stock Expansion
20
+ * Interface drives one side; `select` takes no side.
21
+ * - Read and Write Sector: 1 0 0 m b E 0 0 and 1 0 1 m b E a1 a0 (Table 2). No side compare (the
22
+ * WD1793's C and S flags do not exist). `b` = 1 is the IBM length, 128 x 2^n by the ID's length
23
+ * code; `b` = 0 is 16 x N bytes ("an indicator of all zeroes is interpreted as 256 sixteen byte
24
+ * groups"). A length other than the sector's own leaves the data CRC unmatched, which the
25
+ * command reports as a CRC error.
26
+ * - The data address mark: Read Sector reports its type in status bits 6 and 5 (FB 00, FA 01,
27
+ * F9 10, F8 11: "At the end of the Read operation, the type of Data Address Mark encountered in
28
+ * the data field is recorded in the Status Register (Bits 5 and 6)"); Write Sector writes the
29
+ * mark a1 a0 choose (00 FB, 01 FA, 10 F9, 11 F8: Table 4). Write Track takes FA and F9 as data
30
+ * marks too ("Control Bytes for Initialization").
31
+ * - Times: CLK is 1 MHz (the sheet's "may be 1.0 MHz for mini floppy": 1 MHz / 4 / 2 gives the
32
+ * 125 kbit/s of a single-density 5.25" disk; unverified against the Expansion Interface
33
+ * schematic, spec §5). So the step rates are the 1771-X1, CLK = 1 MHz, TEST = 1 column of
34
+ * Table 1, 12, 12, 20, 40 ms for r1 r0 = 00, 01, 10, 11, and the head settles 20 ms ("IF TEST =
35
+ * 1 AND CLK 1 MHz THERE IS A 20MS DELAY", the Type I and II flow charts). An ID search ends
36
+ * after two revolutions ("within two revolutions of the disk; otherwise, the Record Not Found
37
+ * status bit is set"; a verify likewise): at the third index pulse, the first at which two
38
+ * whole revolutions have passed. The head unloads at the third index pulse after the last
39
+ * command ("remains activated until the third index pulse following the last operation").
40
+ * - Read Address: "The Sector Address of the ID field is written into the Sector Register" (the
41
+ * WD1793 writes the track address).
42
+ * - INTRQ goes to the Expansion Interface's interrupt latch at $37E0, bit 6 (DISK_BIT), not to an
43
+ * NMI: the latch's bit follows INTRQ, so it clears when the status is read or a command is
44
+ * written, as INTRQ does ("The INTRQ signal remains active until reset by reading the Status
45
+ * Register to the processor or by the loading of the Command Register"; Kitsz, The Custom TRS-80
46
+ * and Other Mysteries, p. 137: reading 37EC resets the disk flip-flop).
47
+ * - Master reset runs a Restore ("Note that the RESTORE command is executed when MR goes from an
48
+ * active to an inactive state"); the Model I's reset is taken to pulse MR (unverified, spec §5),
49
+ * which also returns the head to track 0 before the ROM's first status read.
50
+ *
51
+ * Kept from the WD1793 model and unverified for the 1771 (spec §5): the Force Interrupt
52
+ * conditions and DRQ handling, READY true when the selected drive holds a disk (no motor
53
+ * time-out), Lost Data timing, and the data mark window of a DMK (30 bytes in FM; the 1771's sheet
54
+ * says 28).
55
+ */
56
+
57
+ import { FDC1793 } from "./fdc-wd1793.js";
58
+ import { DISK_BIT } from "./interrupt-latch.js";
59
+
60
+ /** Write Sector's a1 a0 (bits 1-0): the data address mark it writes (Table 4). */
61
+ const WRITE_MARKS = [0xfb, 0xfa, 0xf9, 0xf8];
62
+ const ST_RECORD_TYPE = 0x60; // Read Sector: bits 6 and 5
63
+
64
+ export class FDC1771 extends FDC1793 {
65
+ /**
66
+ * @param {{ cpuHz: number, latch?: import('./interrupt-latch.js').InterruptLatch | null }} opts the
67
+ * CPU clock the times count in, and the Expansion Interface's latch INTRQ drives (null: none)
68
+ */
69
+ constructor({ cpuHz, latch = null }) {
70
+ super({ cpuHz });
71
+ this.latch = latch;
72
+ this.mfm = false;
73
+ const ms = (/** @type {number} */ n) => Math.round((n * cpuHz) / 1000);
74
+ this.stepTStates = [12, 12, 20, 40].map(ms);
75
+ this.settleTStates = ms(20);
76
+ this.searchPulses = 3;
77
+ this.headUnloadPulses = 3;
78
+ this.readAddressField = 2;
79
+ this.fmMarks = true;
80
+ }
81
+
82
+ /** INTRQ, mirrored into the Expansion Interface latch's disk bit. */
83
+ get intrq() {
84
+ return this._intrq;
85
+ }
86
+
87
+ set intrq(v) {
88
+ this._intrq = v;
89
+ // The base constructor sets INTRQ before `latch` exists
90
+ if (this.latch) {
91
+ if (v) this.latch.raise(DISK_BIT);
92
+ else this.latch.clear(DISK_BIT);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * The drive-select latch at $37E1, decoded: a drive 0-3 or -1 for none. One side, single
98
+ * density, whatever the caller passes.
99
+ * @param {number} drive
100
+ */
101
+ select(drive) {
102
+ super.select(drive, 0, false);
103
+ }
104
+
105
+ /**
106
+ * Master reset with the machine's: the WD1793 model's reset, then the Restore MR starts (03,
107
+ * as the FD179X loads it; the 1771's sheet names no rate), when a disk is mounted (with none the
108
+ * controller is taken as absent, machine/io.js).
109
+ */
110
+ reset() {
111
+ super.reset();
112
+ if (this.anyDiskMounted()) this.writeCommand(0x03);
113
+ }
114
+
115
+ /**
116
+ * A command written while Busy overrides the one running: "This register should not be loaded
117
+ * when the device is busy unless the execution of the current command is to be overridden.
118
+ * This latter action results in an interrupt" ("Command Register"; the WD1793's sheet allows
119
+ * only Force Interrupt then, and the WD1793 model ignores the write). The running command stops
120
+ * as Force Interrupt stops one (Busy and DRQ reset, a sector not yet written is not written),
121
+ * the new one starts, and INTRQ is raised for the override and left up until a status read
122
+ * (whether the new command's load resets it first is not said; unverified, spec §5). Model I
123
+ * boot sectors rely on it: the dual-boot disks' loader writes its Seek about 190 T-states after
124
+ * the ROM took the boot sector's last byte, while the controller is still reading the CRC.
125
+ * @param {number} value
126
+ */
127
+ writeCommand(value) {
128
+ value &= 0xff;
129
+ if (value >> 4 === 0x0d || !(this.status & 0x01)) {
130
+ super.writeCommand(value);
131
+ return;
132
+ }
133
+ this.pending = null;
134
+ this.buffer = null;
135
+ this.trackWrite = null;
136
+ this.status &= ~(0x01 | (this.commandType === 1 ? 0 : 0x02));
137
+ // With Busy now clear the base loads the command, which resets INTRQ as any command load does
138
+ // (unless Force Interrupt I3 holds it) and may finish at once (not ready: INTRQ raised); then
139
+ // the override's own interrupt is raised over whatever that left. The order matters: raised
140
+ // first, the base's load would clear it.
141
+ super.writeCommand(value);
142
+ this.intrq = true;
143
+ }
144
+
145
+ _startType2(value) {
146
+ super._startType2(value);
147
+ const p = this.pending;
148
+ this.writeDeleted = false;
149
+ if (p === null) return;
150
+ p.compareSide = false;
151
+ p.ibm = (value & 0x08) !== 0;
152
+ if (p.kind === "write") p.dam = WRITE_MARKS[value & 3];
153
+ }
154
+
155
+ /** The data field's length the command's b flag and the ID's length code give. */
156
+ _length(id, p) {
157
+ if (p.ibm) return 128 << (id.sizeCode & 3);
158
+ return 16 * (id.sizeCode === 0 ? 256 : id.sizeCode);
159
+ }
160
+
161
+ _type2Event(p) {
162
+ const searching = p.phase === "search";
163
+ super._type2Event(p);
164
+ if (!searching || this.pending !== p || (p.phase !== "data" && p.phase !== "drq")) return;
165
+ // The ID was found: the 1771's record type and length
166
+ const id = this.currentDisk().trackIds(this._track, this.side)[p.index];
167
+ const len = this._length(id, p);
168
+ if (p.phase === "data") {
169
+ this.status = (this.status & ~ST_RECORD_TYPE) | ((~(id.dam ?? 0xfb) & 3) << 5);
170
+ if (len !== this.buffer.length) {
171
+ const field = new Uint8Array(len);
172
+ field.set(this.buffer.subarray(0, len));
173
+ this.buffer = field;
174
+ p.crcOk = false;
175
+ }
176
+ } else if (len !== this.buffer.length) {
177
+ this.buffer = new Uint8Array(len);
178
+ }
179
+ }
180
+ }