@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRS-80 Model III Cassette Interface
|
|
3
|
+
*
|
|
4
|
+
* Two personalities share this deck:
|
|
5
|
+
*
|
|
6
|
+
* - The byte-level tape (tapeData/readByte/simulate*) that the fast
|
|
7
|
+
* loaders and legacy phase consoles use.
|
|
8
|
+
*
|
|
9
|
+
* - The pulse-level deck for the REAL port-0xFF path: a pulse tape
|
|
10
|
+
* (transition times from cassette-tape.js) plays against the CPU
|
|
11
|
+
* clock while the ROM holds the motor relay on, setting the input
|
|
12
|
+
* latch on every transition; any port-0xFF write clears the latch
|
|
13
|
+
* (the Model III's latch circuit). Output transitions written
|
|
14
|
+
* while the motor runs are recorded, and recordedCas() decodes
|
|
15
|
+
* them back into a .cas byte stream — a real CSAVE capture.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { pulsesToBytes } from "./cassette-tape.js";
|
|
19
|
+
|
|
20
|
+
export class CassetteSystem {
|
|
21
|
+
constructor() {
|
|
22
|
+
// Where load and save notes go; a no-op unless the host sets one.
|
|
23
|
+
this.log = () => {};
|
|
24
|
+
this.motorOn = false;
|
|
25
|
+
this.playing = false;
|
|
26
|
+
this.recording = false;
|
|
27
|
+
|
|
28
|
+
this.tapeData = null;
|
|
29
|
+
this.tapePosition = 0;
|
|
30
|
+
this.tapeLength = 0;
|
|
31
|
+
|
|
32
|
+
// Pulse deck: playback
|
|
33
|
+
this.pulseTape = null; // transition times (tape-relative T-states)
|
|
34
|
+
this.pulsePos = 0;
|
|
35
|
+
this.playClock = 0; // tape time elapsed while the motor has been on
|
|
36
|
+
this.lastCycle = 0;
|
|
37
|
+
this.inputLevel = false; // the square wave's current polarity (bit 0)
|
|
38
|
+
this.pulseLatch = false; // rising-edge latch (bit 7, OUT-0xFF resets)
|
|
39
|
+
this.risePending = false; // edges since last consume, by direction
|
|
40
|
+
this.fallPending = false;
|
|
41
|
+
|
|
42
|
+
// Pulse deck: recording (port-0xFF output transitions, cycle-stamped)
|
|
43
|
+
this.recordLog = [];
|
|
44
|
+
this.RECORD_LIMIT = 400000;
|
|
45
|
+
this.restoredRecording = null; // decoded bytes carried in by a save state
|
|
46
|
+
// The pulse codec (cassette-tape.js) counts Model III T-states; a machine
|
|
47
|
+
// with another clock (the Model I) sets this to the Model III's clock over
|
|
48
|
+
// its own, so a recording decodes at the times it took in real time.
|
|
49
|
+
this.timeScale = 1;
|
|
50
|
+
|
|
51
|
+
this.BUFFER_ADDRESS = 0x4300;
|
|
52
|
+
this.BUFFER_SIZE = 256;
|
|
53
|
+
|
|
54
|
+
this.onLoadComplete = null;
|
|
55
|
+
this.onSaveComplete = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Advance the playback clock to `cycles`, playing due transitions:
|
|
59
|
+
* each one flips the input level and marks an edge event. */
|
|
60
|
+
_advance(cycles) {
|
|
61
|
+
if (this.motorOn && cycles > this.lastCycle) {
|
|
62
|
+
this.playClock += cycles - this.lastCycle;
|
|
63
|
+
}
|
|
64
|
+
this.lastCycle = cycles;
|
|
65
|
+
if (this.pulseTape && this.motorOn) {
|
|
66
|
+
while (
|
|
67
|
+
this.pulsePos < this.pulseTape.length &&
|
|
68
|
+
this.pulseTape[this.pulsePos] <= this.playClock
|
|
69
|
+
) {
|
|
70
|
+
this.pulsePos++;
|
|
71
|
+
this.inputLevel = !this.inputLevel;
|
|
72
|
+
if (this.inputLevel) {
|
|
73
|
+
this.risePending = true;
|
|
74
|
+
this.pulseLatch = true;
|
|
75
|
+
} else {
|
|
76
|
+
this.fallPending = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Mount a pulse tape (casToPulses output), rewound. */
|
|
83
|
+
insertPulseTape(times) {
|
|
84
|
+
this.pulseTape = times;
|
|
85
|
+
this.pulsePos = 0;
|
|
86
|
+
this.playClock = 0;
|
|
87
|
+
this.inputLevel = false;
|
|
88
|
+
this.pulseLatch = false;
|
|
89
|
+
this.risePending = false;
|
|
90
|
+
this.fallPending = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
rewindPulseTape() {
|
|
94
|
+
this.pulsePos = 0;
|
|
95
|
+
this.playClock = 0;
|
|
96
|
+
this.inputLevel = false;
|
|
97
|
+
this.pulseLatch = false;
|
|
98
|
+
this.risePending = false;
|
|
99
|
+
this.fallPending = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The tape signal's current level (port 0xFF bit 0). */
|
|
103
|
+
tapeLevel(cycles) {
|
|
104
|
+
this._advance(cycles);
|
|
105
|
+
return this.inputLevel;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Any OUT (0xFF) resets the pulse latch (the 500-baud reader's ack). */
|
|
109
|
+
clearPulseLatch(cycles) {
|
|
110
|
+
this._advance(cycles);
|
|
111
|
+
this.pulseLatch = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Edge events since the last consume, by direction. */
|
|
115
|
+
consumeEdges(cycles) {
|
|
116
|
+
this._advance(cycles);
|
|
117
|
+
const edges = { rise: this.risePending, fall: this.fallPending };
|
|
118
|
+
this.risePending = false;
|
|
119
|
+
this.fallPending = false;
|
|
120
|
+
return edges;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Log an output level change while the motor runs (CSAVE's tape). */
|
|
124
|
+
recordTransition(cycles, level) {
|
|
125
|
+
if (!this.motorOn) return;
|
|
126
|
+
if (this.recordLog.length < this.RECORD_LIMIT) {
|
|
127
|
+
this.recordLog.push({ t: cycles, level });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decode whatever CSAVE recorded into .cas bytes.
|
|
133
|
+
* @returns {{bytes: Uint8Array, baud: 500|1500|null}}
|
|
134
|
+
*/
|
|
135
|
+
recordedCas() {
|
|
136
|
+
if (this.recordLog.length) {
|
|
137
|
+
return pulsesToBytes(this.recordLog.map((r) => r.t * this.timeScale));
|
|
138
|
+
}
|
|
139
|
+
if (this.restoredRecording) {
|
|
140
|
+
return { bytes: this.restoredRecording, baud: null };
|
|
141
|
+
}
|
|
142
|
+
return { bytes: new Uint8Array(0), baud: null };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Playback position 0..1, or null when no pulse tape is mounted. */
|
|
146
|
+
progress() {
|
|
147
|
+
return this.pulseTape?.length ? this.pulsePos / this.pulseTape.length : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
loadTape(programData) {
|
|
151
|
+
if (!programData || programData.length === 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.tapeData =
|
|
156
|
+
programData instanceof Uint8Array
|
|
157
|
+
? programData
|
|
158
|
+
: new Uint8Array(programData);
|
|
159
|
+
|
|
160
|
+
this.tapePosition = 0;
|
|
161
|
+
this.tapeLength = this.tapeData.length;
|
|
162
|
+
|
|
163
|
+
this.log("info", `Cassette loaded: ${this.tapeLength} bytes`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
simulateCLoad(memory, targetAddress = 0x4200) {
|
|
168
|
+
if (!this.tapeData) {
|
|
169
|
+
this.log("error", "No tape loaded");
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < this.tapeLength; i++) {
|
|
174
|
+
memory.writeByte(targetAddress + i, this.tapeData[i]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.log("info",
|
|
178
|
+
`CLOAD: ${this.tapeLength} bytes at 0x${targetAddress.toString(16)}`
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (this.onLoadComplete) {
|
|
182
|
+
this.onLoadComplete(targetAddress, this.tapeLength);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return targetAddress;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
simulateCSave(memory, startAddress, length) {
|
|
189
|
+
this.tapeData = new Uint8Array(length);
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < length; i++) {
|
|
192
|
+
this.tapeData[i] = memory.readByte(startAddress + i);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.tapePosition = 0;
|
|
196
|
+
this.tapeLength = length;
|
|
197
|
+
|
|
198
|
+
this.log("info", `CSAVE: ${length} bytes from 0x${startAddress.toString(16)}`);
|
|
199
|
+
|
|
200
|
+
if (this.onSaveComplete) {
|
|
201
|
+
this.onSaveComplete(this.tapeData);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.tapeData;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getStatus() {
|
|
208
|
+
let status = 0x00;
|
|
209
|
+
|
|
210
|
+
if (this.motorOn) status |= 0x01;
|
|
211
|
+
if (this.playing) status |= 0x02;
|
|
212
|
+
if (this.recording) status |= 0x04;
|
|
213
|
+
if (this.tapeData && this.tapePosition < this.tapeLength) {
|
|
214
|
+
status |= 0x08;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return status;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
control(value, cycles = 0) {
|
|
221
|
+
this._advance(cycles); // settle the old motor state's clock first
|
|
222
|
+
this.motorOn = (value & 0x01) !== 0;
|
|
223
|
+
|
|
224
|
+
if (this.motorOn) {
|
|
225
|
+
this.playing = (value & 0x02) !== 0;
|
|
226
|
+
this.recording = (value & 0x04) !== 0;
|
|
227
|
+
} else {
|
|
228
|
+
this.playing = false;
|
|
229
|
+
this.recording = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
readByte() {
|
|
234
|
+
if (!this.tapeData || this.tapePosition >= this.tapeLength) {
|
|
235
|
+
return 0x00;
|
|
236
|
+
}
|
|
237
|
+
return this.tapeData[this.tapePosition++];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
rewind() {
|
|
241
|
+
this.tapePosition = 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
eject() {
|
|
245
|
+
this.tapeData = null;
|
|
246
|
+
this.tapePosition = 0;
|
|
247
|
+
this.tapeLength = 0;
|
|
248
|
+
this.pulseTape = null;
|
|
249
|
+
this.pulsePos = 0;
|
|
250
|
+
this.playClock = 0;
|
|
251
|
+
this.inputLevel = false;
|
|
252
|
+
this.pulseLatch = false;
|
|
253
|
+
this.risePending = false;
|
|
254
|
+
this.fallPending = false;
|
|
255
|
+
this.motorOn = false;
|
|
256
|
+
this.playing = false;
|
|
257
|
+
this.recording = false;
|
|
258
|
+
// The recording is the recorder's own cassette — eject leaves it
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|