@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.
- package/README.md +124 -8
- package/package.json +1 -1
- package/src/cpu/z80cpu.js +9 -2
- package/src/debug/debugger.js +20 -9
- package/src/debug/label-file.js +10 -4
- package/src/debug/state.js +69 -10
- package/src/devices/cassette.js +5 -1
- package/src/devices/disk-image.js +635 -248
- package/src/devices/fdc-wd1771.js +180 -0
- package/src/devices/fdc-wd1793.js +838 -239
- package/src/devices/interrupt-latch.js +76 -0
- package/src/devices/key-queue.js +22 -5
- package/src/devices/video.js +6 -3
- package/src/index.js +3 -2
- package/src/loaders/load.js +169 -5
- package/src/machine/drive-select.js +42 -0
- package/src/machine/io.js +238 -15
- package/src/machine/machine.js +53 -15
- package/src/machine/memory.js +107 -24
- package/src/machine/models.js +123 -0
- package/src/machine/rom-hashes.js +14 -7
- package/src/machine/trs80-system.js +90 -33
package/src/machine/memory.js
CHANGED
|
@@ -1,26 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TRS-80
|
|
2
|
+
* TRS-80 memory map, by model (src/machine/models.js holds the sources for the Model I facts):
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Model III (non-disk machine):
|
|
5
|
+
* 0x0000-0x37FF 14K ROM (write-protected)
|
|
6
|
+
* 0x3800-0x3BFF keyboard matrix (memory-mapped, read-only;
|
|
7
|
+
* low 8 address bits select rows, A8/A9 undecoded)
|
|
8
|
+
* 0x3C00-0x3FFF 1K video RAM (64 cols x 16 rows)
|
|
9
|
+
* 0x4000-0xFFFF 48K RAM
|
|
10
|
+
*
|
|
11
|
+
* Model I:
|
|
12
|
+
* 0x0000-0x2FFF 12K Level II ROM (write-protected)
|
|
13
|
+
* 0x3000-0x37DF nothing: reads float to 0xFF, writes are lost
|
|
14
|
+
* 0x37E0-0x37EF the Expansion Interface (IOSystem.readExpansion and
|
|
15
|
+
* writeExpansion: the interrupt latch at 0x37E0, the drive
|
|
16
|
+
* select at 0x37E1, the printer at 0x37E8, the WD1771 at
|
|
17
|
+
* 0x37EC-0x37EF); without it, nothing, as 0x3000-0x37DF
|
|
18
|
+
* 0x3800-0x3BFF keyboard matrix, as on the Model III
|
|
19
|
+
* 0x3C00-0x3FFF 1K video RAM, seven bits wide (below)
|
|
20
|
+
* 0x4000-... 4, 16, 32, or 48K RAM; above it reads float to 0xFF
|
|
21
|
+
* and writes are lost
|
|
22
|
+
*
|
|
23
|
+
* The stock Model I's video RAM has no bit 6: "There are seven RAMs. Six
|
|
24
|
+
* are used for ASCII storage, and the seventh is used as a graphic/
|
|
25
|
+
* alphanumeric definition bit. ... Notice the line labeled Bit 6. It is
|
|
26
|
+
* sourced by NOR gate Z30 at pin 13. If Bit 5 ... and Bit 7 ... are low,
|
|
27
|
+
* then Bit 6 will be high" (TRS-80 Micro Computer Technical Reference
|
|
28
|
+
* Handbook, 2nd ed., 1982, p. 34, "Video RAMs"). A byte written there is
|
|
29
|
+
* kept as it reads back and displays: bit 6 = NOT (bit 5 OR bit 7). The
|
|
30
|
+
* lowercase modification (`lowercase: true`) fits the missing RAM, so all
|
|
31
|
+
* eight bits are kept.
|
|
10
32
|
*/
|
|
11
33
|
|
|
12
|
-
|
|
34
|
+
import { MODEL_III } from "./models.js";
|
|
35
|
+
|
|
36
|
+
const KEYBOARD_BASE = 0x3800;
|
|
13
37
|
const KEYBOARD_LIMIT = 0x3c00;
|
|
14
38
|
const RAM_BASE = 0x4000;
|
|
39
|
+
const EXPANSION_BASE = 0x37e0;
|
|
40
|
+
const EXPANSION_LIMIT = 0x37f0;
|
|
15
41
|
|
|
16
42
|
export class MemorySystem {
|
|
17
|
-
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ model?: import('./models.js').Model, ram?: number, lowercase?: boolean }} [opts] the
|
|
45
|
+
* model (the Model III by default), its RAM in K (checked by the Machine against the model's
|
|
46
|
+
* sizes), and on the Model I whether the lowercase modification is fitted (not by default)
|
|
47
|
+
*/
|
|
48
|
+
constructor({ model = MODEL_III, ram = model.defaultRam, lowercase = false } = {}) {
|
|
49
|
+
this.model = model;
|
|
50
|
+
|
|
51
|
+
// The stock Model I's seven-bit video RAM (above); the Model III's and a
|
|
52
|
+
// lowercase-modified Model I's keep all eight bits.
|
|
53
|
+
this.videoBit6Gate = model.id === 1 && !lowercase;
|
|
54
|
+
|
|
55
|
+
// The Model I's Expansion Interface at 0x37E0-0x37EF: the IOSystem, set by
|
|
56
|
+
// TRS80System when the interface is fitted; null, those addresses read 0xFF.
|
|
57
|
+
/** @type {{ readExpansion(a: number): number, peekExpansion(a: number): number, writeExpansion(a: number, v: number): void } | null} */
|
|
58
|
+
this.expansion = null;
|
|
59
|
+
|
|
18
60
|
// ROM storage is 16K so padded 16K images can be loaded verbatim,
|
|
19
|
-
// but only 0x0000
|
|
61
|
+
// but only 0x0000 up to the model's ROM top is ever mapped as ROM.
|
|
20
62
|
this.rom = new Uint8Array(0x4000);
|
|
63
|
+
this.romLimit = model.romTop;
|
|
21
64
|
|
|
22
|
-
//
|
|
23
|
-
this.ram = new Uint8Array(
|
|
65
|
+
// RAM from 0x4000: 48K reaches 0xFFFF
|
|
66
|
+
this.ram = new Uint8Array(ram * 1024);
|
|
67
|
+
this.ramLimit = RAM_BASE + this.ram.length;
|
|
24
68
|
|
|
25
69
|
// 1K video RAM (0x3C00 - 0x3FFF)
|
|
26
70
|
this.videoRam = new Uint8Array(0x400);
|
|
@@ -51,18 +95,19 @@ export class MemorySystem {
|
|
|
51
95
|
}
|
|
52
96
|
|
|
53
97
|
/**
|
|
54
|
-
* Load a ROM image
|
|
55
|
-
* (0x4000) padded image of it
|
|
56
|
-
*
|
|
98
|
+
* Load a ROM image of a size the model takes: the Model III's real 14K
|
|
99
|
+
* (0x3800) ROM or a 16K (0x4000) padded image of it; the Model I's 12K
|
|
100
|
+
* (0x3000) Level II ROM. Anything else is a wrong file — reject it
|
|
101
|
+
* rather than guessing.
|
|
57
102
|
*/
|
|
58
103
|
loadROM(romData) {
|
|
59
104
|
if (!romData || romData.length === 0) {
|
|
60
105
|
throw new Error("ROM data is required");
|
|
61
106
|
}
|
|
62
107
|
|
|
63
|
-
if (
|
|
108
|
+
if (!this.model.romSizes.includes(romData.length)) {
|
|
64
109
|
throw new Error(
|
|
65
|
-
`Invalid ROM size: ${romData.length} bytes.
|
|
110
|
+
`Invalid ROM size: ${romData.length} bytes. ${this.model.romSizeText}`
|
|
66
111
|
);
|
|
67
112
|
}
|
|
68
113
|
|
|
@@ -80,9 +125,16 @@ export class MemorySystem {
|
|
|
80
125
|
readByte(address) {
|
|
81
126
|
address = address & 0xffff;
|
|
82
127
|
|
|
83
|
-
if (address <
|
|
128
|
+
if (address < this.romLimit) {
|
|
84
129
|
return this.rom[address];
|
|
85
130
|
}
|
|
131
|
+
if (address < KEYBOARD_BASE) {
|
|
132
|
+
// The Model I's 0x3000-0x37FF (models.js): the Expansion Interface, or nothing
|
|
133
|
+
if (address >= EXPANSION_BASE && address < EXPANSION_LIMIT && this.expansion !== null) {
|
|
134
|
+
return this.expansion.readExpansion(address);
|
|
135
|
+
}
|
|
136
|
+
return 0xff;
|
|
137
|
+
}
|
|
86
138
|
if (address < KEYBOARD_LIMIT) {
|
|
87
139
|
// Keyboard matrix: reading with no keys down yields 0x00
|
|
88
140
|
return this.keyboard ? this.keyboard.read(address & 0xff) : 0x00;
|
|
@@ -90,26 +142,51 @@ export class MemorySystem {
|
|
|
90
142
|
if (address < RAM_BASE) {
|
|
91
143
|
return this.videoRam[address - KEYBOARD_LIMIT];
|
|
92
144
|
}
|
|
93
|
-
|
|
145
|
+
if (address < this.ramLimit) {
|
|
146
|
+
return this.ram[address - RAM_BASE];
|
|
147
|
+
}
|
|
148
|
+
return 0xff; // above the fitted RAM (models.js)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read a byte as `readByte` does, without a device's read side effect: the
|
|
153
|
+
* Model I's Expansion Interface addresses are peeked (reading the interrupt
|
|
154
|
+
* latch at 0x37E0 does not clear it). For the debugger and `m.mem.read`.
|
|
155
|
+
*/
|
|
156
|
+
peekByte(address) {
|
|
157
|
+
address = address & 0xffff;
|
|
158
|
+
if (address >= EXPANSION_BASE && address < EXPANSION_LIMIT && this.expansion !== null) {
|
|
159
|
+
return this.expansion.peekExpansion(address);
|
|
160
|
+
}
|
|
161
|
+
return this.readByte(address);
|
|
94
162
|
}
|
|
95
163
|
|
|
96
164
|
/**
|
|
97
165
|
* Write a byte to memory
|
|
98
|
-
* ROM
|
|
166
|
+
* ROM, the Model I's unmapped 0x3000-0x37FF, the keyboard region, and
|
|
167
|
+
* addresses above the fitted RAM ignore writes; video RAM and RAM accept them,
|
|
168
|
+
* and the Model I's Expansion Interface takes its own.
|
|
99
169
|
*/
|
|
100
170
|
writeByte(address, value) {
|
|
101
171
|
address = address & 0xffff;
|
|
102
172
|
value = value & 0xff;
|
|
103
173
|
|
|
104
174
|
if (address < KEYBOARD_LIMIT) {
|
|
105
|
-
|
|
175
|
+
// ROM / keyboard matrix: not writable; the Model I's Expansion Interface
|
|
176
|
+
if (this.expansion !== null && address >= EXPANSION_BASE && address < EXPANSION_LIMIT) {
|
|
177
|
+
this.expansion.writeExpansion(address, value);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
106
180
|
}
|
|
107
181
|
if (address < RAM_BASE) {
|
|
182
|
+
if (this.videoBit6Gate) value = (value & 0xbf) | (value & 0xa0 ? 0 : 0x40);
|
|
108
183
|
this.videoRam[address - KEYBOARD_LIMIT] = value;
|
|
109
184
|
this.videoDirty = true;
|
|
110
185
|
return;
|
|
111
186
|
}
|
|
112
|
-
|
|
187
|
+
if (address < this.ramLimit) {
|
|
188
|
+
this.ram[address - RAM_BASE] = value;
|
|
189
|
+
}
|
|
113
190
|
}
|
|
114
191
|
|
|
115
192
|
/**
|
|
@@ -198,10 +275,16 @@ export class MemorySystem {
|
|
|
198
275
|
*/
|
|
199
276
|
getMemoryView() {
|
|
200
277
|
const view = new Uint8Array(0x10000);
|
|
201
|
-
|
|
202
|
-
|
|
278
|
+
// Unmapped addresses read 0xFF; the keyboard region reads as idle (0x00) in the debug view
|
|
279
|
+
view.fill(0xff, this.romLimit, KEYBOARD_BASE);
|
|
280
|
+
view.fill(0xff, this.ramLimit);
|
|
281
|
+
view.set(this.rom.subarray(0, this.romLimit), 0);
|
|
203
282
|
view.set(this.videoRam, KEYBOARD_LIMIT);
|
|
204
283
|
view.set(this.ram, RAM_BASE);
|
|
284
|
+
// The Model I's Expansion Interface, peeked, as m.mem.read shows it
|
|
285
|
+
if (this.expansion !== null) {
|
|
286
|
+
for (let a = EXPANSION_BASE; a < EXPANSION_LIMIT; a++) view[a] = this.expansion.peekExpansion(a);
|
|
287
|
+
}
|
|
205
288
|
return view;
|
|
206
289
|
}
|
|
207
290
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model descriptions: what differs between the TRS-80 models the Machine builds, as data, so the
|
|
3
|
+
* memory map, the I/O ports, the clock, and the ROM checks read one description instead of
|
|
4
|
+
* hard-coding the Model III (spec §5).
|
|
5
|
+
*
|
|
6
|
+
* Model I facts and where they come from (the ROM sources are github.com/kiwisincebirth/TRS-80-ROMS;
|
|
7
|
+
* spec §5 lists what is still unverified against the Model I Technical Reference Manual):
|
|
8
|
+
* - ROM: 12 KB at $0000-$2FFF. The repository's Model I Level II images `test-roms/M1L2_1.2.bin`
|
|
9
|
+
* and `M1L2_1.3.bin` are 12,288 bytes each, and its MODEL1.md describes `MDL1LEV2.Z80` as the
|
|
10
|
+
* source of the "Model I Level 2 12KB ROMS".
|
|
11
|
+
* - Keyboard matrix at $3800, video RAM at $3C00 (400H bytes), RAM from $4000: CONSTANTS.Z80,
|
|
12
|
+
* "Memory Mapped Hardware" and "RAM Communications Area".
|
|
13
|
+
* - $37E0-$37EF belong to the Expansion Interface (drive select $37E1, cassette select $37E4,
|
|
14
|
+
* printer $37E8, the disk controller $37EC-$37EF: CONSTANTS.Z80, "Memory Mapped Hardware,
|
|
15
|
+
* Model 1"). Without the Expansion Interface those addresses read $FF: MDL1LEV2.Z80 at $0069
|
|
16
|
+
* ("$FF if no expansion interface present") and $0696-$069C, where the ROM takes $FF (or $00) at
|
|
17
|
+
* $37EC as "no disk controller" and goes to Level II BASIC. The rest of $3000-$37FF decodes to
|
|
18
|
+
* nothing and is taken to read $FF as well (unverified).
|
|
19
|
+
* - RAM sizes 4, 16, 32, and 48 KB from $4000 (spec §5, unverified against the manual; Ruling 2
|
|
20
|
+
* of the phase 6 plan). Above the fitted RAM a read is taken to float to $FF and a write is
|
|
21
|
+
* lost (unverified); the ROM's MEMORY SIZE? probe at $00C4-$00D4 needs only that a byte there
|
|
22
|
+
* does not hold the complement written to it.
|
|
23
|
+
* - Clock 1,774,080 Hz: spec §5's 1.774 MHz, taken as the 10.6445 MHz crystal divided by 6 (as
|
|
24
|
+
* xtrs runs it; unverified against the manual). The ROM's PAUSE loop at $0060 is commented
|
|
25
|
+
* "BC times 14.65" µs at $06AF, and one pass of it is 26 T-states, which puts the clock at
|
|
26
|
+
* 1.775 MHz.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{ id: 1 | 3, name: string, clockHz: number, timerHz: number, romTop: number,
|
|
31
|
+
* romSizes: readonly number[], romSizeText: string, ram: readonly number[], defaultRam: number,
|
|
32
|
+
* ramNote: string, roms: readonly string[], romTitle: string, stateMachine: string }} Model
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {Model} */
|
|
36
|
+
export const MODEL_I = Object.freeze({
|
|
37
|
+
id: 1,
|
|
38
|
+
name: 'Model I',
|
|
39
|
+
clockHz: 1774080,
|
|
40
|
+
// The Expansion Interface's 40 Hz clock, raised through its latch at $37E0
|
|
41
|
+
// (devices/interrupt-latch.js, which cites it); none without the Expansion Interface.
|
|
42
|
+
timerHz: 40,
|
|
43
|
+
romTop: 0x3000,
|
|
44
|
+
romSizes: Object.freeze([0x3000]),
|
|
45
|
+
romSizeText: "The Model I's Level II ROM is 12KB (0x3000)",
|
|
46
|
+
ram: Object.freeze([4, 16, 32, 48]),
|
|
47
|
+
defaultRam: 48,
|
|
48
|
+
ramNote: '',
|
|
49
|
+
roms: Object.freeze(['model1-level2-1.2', 'model1-level2-1.3']),
|
|
50
|
+
romTitle: 'the Model I Level II ROM',
|
|
51
|
+
stateMachine: 'trs80-model1',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** @type {Model} */
|
|
55
|
+
export const MODEL_III = Object.freeze({
|
|
56
|
+
id: 3,
|
|
57
|
+
name: 'Model III',
|
|
58
|
+
clockHz: 2027520,
|
|
59
|
+
timerHz: 30,
|
|
60
|
+
romTop: 0x3800,
|
|
61
|
+
// The real 14K image, or a 16K padded image of it (only $0000-$37FF is mapped).
|
|
62
|
+
romSizes: Object.freeze([0x3800, 0x4000]),
|
|
63
|
+
romSizeText: 'Expected 14KB (0x3800) or a 16KB (0x4000) padded image',
|
|
64
|
+
ram: Object.freeze([48]),
|
|
65
|
+
defaultRam: 48,
|
|
66
|
+
// Ruling 2 of the phase 6 plan: 16K and 32K are cheap but would run untested programs.
|
|
67
|
+
ramNote: '; 16K and 32K come in a later phase',
|
|
68
|
+
roms: Object.freeze(['model3-rev-c-2ef8', 'model3-rev-c-frehd']),
|
|
69
|
+
romTitle: 'the Model III ROM',
|
|
70
|
+
stateMachine: 'trs80-model3',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** The models built, by number. */
|
|
74
|
+
export const MODELS = Object.freeze({ 1: MODEL_I, 3: MODEL_III });
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The description of model `n`, or a sentence-long error for a model not built.
|
|
78
|
+
* @param {unknown} n
|
|
79
|
+
* @returns {Model}
|
|
80
|
+
*/
|
|
81
|
+
export function modelOf(n) {
|
|
82
|
+
if (n === 1) return MODEL_I;
|
|
83
|
+
if (n === 3) return MODEL_III;
|
|
84
|
+
if (n === 4) throw new Error('There is no Model 4 yet: it needs a Model 4 ROM and boot disk to prove it boots, and comes in its own plan when one is supplied; use model 1 or 3.');
|
|
85
|
+
throw new Error(`model must be 1 (the Model I) or 3 (the Model III), not ${String(n)}.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Why `ram` (in K) is not a size model `desc` is built with, as a sentence, or null when it is.
|
|
90
|
+
* @param {Model} desc
|
|
91
|
+
* @param {unknown} ram
|
|
92
|
+
* @returns {string | null}
|
|
93
|
+
*/
|
|
94
|
+
export function ramProblem(desc, ram) {
|
|
95
|
+
if (desc.ram.includes(/** @type {number} */ (ram))) return null;
|
|
96
|
+
const sizes = desc.ram.length === 1 ? String(desc.ram[0]) : `${desc.ram.slice(0, -1).join(', ')}, or ${desc.ram.at(-1)}`;
|
|
97
|
+
const got = typeof ram === 'string' ? JSON.stringify(ram) : String(ram);
|
|
98
|
+
return `The ${desc.name} takes ram: ${sizes} (K), not ${got}${desc.ramNote}.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The CPU clock of model `n`, in Hz.
|
|
103
|
+
* @param {1 | 3} n
|
|
104
|
+
*/
|
|
105
|
+
export function clockHz(n) {
|
|
106
|
+
return modelOf(n).clockHz;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Why a recognised ROM cannot run on model `n`, as a sentence, or null when it can (or is not
|
|
111
|
+
* recognised: an unknown image is checked by its size alone).
|
|
112
|
+
* @param {string | null} recognized the ROM_HASHES name
|
|
113
|
+
* @param {1 | 3} n
|
|
114
|
+
* @returns {string | null}
|
|
115
|
+
*/
|
|
116
|
+
export function romModelProblem(recognized, n) {
|
|
117
|
+
if (recognized === null) return null;
|
|
118
|
+
const wanted = modelOf(n);
|
|
119
|
+
if (wanted.roms.includes(recognized)) return null;
|
|
120
|
+
const owner = [MODEL_I, MODEL_III].find((d) => d.roms.includes(recognized));
|
|
121
|
+
if (owner === undefined) return null;
|
|
122
|
+
return `This ROM is ${owner.romTitle} (${recognized}); it runs on the ${owner.name}, not the ${wanted.name}: construct the Machine with model: ${owner.id}.`;
|
|
123
|
+
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Known
|
|
3
|
-
*
|
|
4
|
-
* about the images; the images themselves are never committed
|
|
2
|
+
* Known ROM images by SHA-256 (lowercase hex), so the Machine can report which ROM it was given
|
|
3
|
+
* and refuse one given for the other model (spec §5: the core validates by length and a
|
|
4
|
+
* known-hash table). Hashes are facts about the images; the images themselves are never committed
|
|
5
|
+
* or bundled. Each name starts with its model (`model1-`, `model3-`); src/machine/models.js lists
|
|
6
|
+
* which names each model runs.
|
|
5
7
|
*
|
|
6
|
-
* Source for
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Source for all: github.com/kiwisincebirth/TRS-80-ROMS, folder test-roms/.
|
|
9
|
+
* - `M1L2_1.2.bin` and `M1L2_1.3.bin` (12,288 bytes each): the Model I Level II ROM, versions 1.2
|
|
10
|
+
* and 1.3 (the repository's MODEL1.md: "Both 1.3 and 1.2 versions"), hashed 2026-10-18; 1.3 is
|
|
11
|
+
* the image the repository's fugloy.mcp.json names as roms.model1.
|
|
12
|
+
* - `Model3-RevC-2EF8.bin` (14,336 bytes): the stock Model III Rev C ROM and the image named as
|
|
13
|
+
* roms.model3; `Model3-RevC-FreHD.bin` is the same ROM patched to boot a FreHD hard-disk
|
|
14
|
+
* emulator. Hashed 2026-10-17.
|
|
10
15
|
*/
|
|
11
16
|
export const ROM_HASHES = Object.freeze({
|
|
17
|
+
'3cd3824d2ac2743750d2179951ed812794302b885ccb4f98aa2579b352c4b00e': 'model1-level2-1.2',
|
|
18
|
+
'5515d95d7c19058400a2e91c6930bb557035db5ce093bd96fd017f795c346776': 'model1-level2-1.3',
|
|
12
19
|
beeb3741175a8b8706cc384d5b2534ebf32e383662864054e5a0ce4d861a43bb: 'model3-rev-c-2ef8',
|
|
13
20
|
ad6de30c6aece2d6a2243ee90a2959295d5c46b82650b27f46f1ecf8b34b2361: 'model3-rev-c-frehd',
|
|
14
21
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TRS-80 Model III
|
|
2
|
+
* TRS-80 System, Model III or Model I
|
|
3
3
|
*
|
|
4
4
|
* Owns and wires the machine: Z80 CPU, memory map, I/O ports, and the
|
|
5
|
-
* keyboard matrix
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* keyboard matrix, laid out by the model description (models.js). Runs the
|
|
6
|
+
* CPU in T-state-budgeted slices at the model's clock (the Model III's real
|
|
7
|
+
* 2.02752 MHz) and raises the Model III's 30 Hz RTC heartbeat interrupt the
|
|
8
|
+
* ROM's clock/cursor services depend on, or the Model I Expansion
|
|
9
|
+
* Interface's 40 Hz clock, through its latch at $37E0
|
|
10
|
+
* (devices/interrupt-latch.js); a Model I without the Expansion Interface
|
|
11
|
+
* has no clock and takes no interrupts.
|
|
8
12
|
*
|
|
9
13
|
* Rendering is the caller's concern: read `memory.videoRam` (or
|
|
10
14
|
* `screenText()`) and consult `memory.videoDirty`. Keyboard input goes
|
|
@@ -13,17 +17,37 @@
|
|
|
13
17
|
|
|
14
18
|
import { Z80CPU } from "../cpu/z80cpu.js";
|
|
15
19
|
import { MemorySystem } from "./memory.js";
|
|
16
|
-
import { IOSystem, MODE_32COL_BIT } from "./io.js";
|
|
20
|
+
import { IOSystem, MODE_32COL_BIT, M1_MODESEL_BIT } from "./io.js";
|
|
21
|
+
import { TIMER_BIT } from "../devices/interrupt-latch.js";
|
|
17
22
|
import { KeyboardMatrix } from "../devices/keyboard.js";
|
|
23
|
+
import { MODEL_III, modelOf } from "./models.js";
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
export const
|
|
25
|
+
/** The Model III's clock and RTC; a system's own are `clockHz` and `rtcInterval`. */
|
|
26
|
+
export const CPU_CLOCK_HZ = MODEL_III.clockHz;
|
|
27
|
+
export const RTC_HZ = MODEL_III.timerHz;
|
|
21
28
|
export const RTC_INTERVAL_TSTATES = CPU_CLOCK_HZ / RTC_HZ; // 67,584
|
|
22
29
|
|
|
23
30
|
export class TRS80System {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
/**
|
|
32
|
+
* @param {{ romData: Uint8Array, log?: (level: string, text: string) => void,
|
|
33
|
+
* model?: 1 | 3 | import('./models.js').Model, ram?: number, expansion?: boolean, lowercase?: boolean }} opts
|
|
34
|
+
* the model (a number or a description; the Model III by default), its RAM in K (the model's
|
|
35
|
+
* default), and the Model I's Expansion Interface (fitted by default) and lowercase modification
|
|
36
|
+
* (not fitted by default); the Machine checks them
|
|
37
|
+
*/
|
|
38
|
+
constructor({ romData, log, model = MODEL_III, ram, expansion = true, lowercase = false }) {
|
|
39
|
+
const desc = typeof model === "number" ? modelOf(model) : model;
|
|
40
|
+
this.model = desc;
|
|
41
|
+
this.clockHz = desc.clockHz;
|
|
42
|
+
this.rtcInterval = desc.clockHz / desc.timerHz;
|
|
43
|
+
this.expansion = expansion;
|
|
44
|
+
this.lowercase = lowercase;
|
|
45
|
+
this.memory = new MemorySystem({ model: desc, ram: ram ?? desc.defaultRam, lowercase });
|
|
46
|
+
this.io = new IOSystem({ model: desc, expansion });
|
|
47
|
+
// The Model I's Expansion Interface addresses go to the I/O decode
|
|
48
|
+
if (this.io.latch !== null) this.memory.expansion = this.io;
|
|
49
|
+
// The 32-column bit: port $EC bit 2 on the Model III, port $FF bit 3 (MODESEL) on the Model I
|
|
50
|
+
this._mode32Bit = desc.id === 1 ? M1_MODESEL_BIT : MODE_32COL_BIT;
|
|
27
51
|
this.keyboard = new KeyboardMatrix();
|
|
28
52
|
this.cpu = new Z80CPU();
|
|
29
53
|
if (log !== undefined) {
|
|
@@ -47,9 +71,22 @@ export class TRS80System {
|
|
|
47
71
|
// Cassette-out sound transitions are stamped in CPU T-states
|
|
48
72
|
this.io.getCycles = () => this.cpu.cycles;
|
|
49
73
|
|
|
74
|
+
// The Model III WAIT (port F4 bit 6): the controller runs ahead to its
|
|
75
|
+
// next DRQ or INTRQ; the CPU's clock catches up, and stepInstruction
|
|
76
|
+
// does not tick the controller for those T-states again. A port write
|
|
77
|
+
// from outside an instruction (a debugger poke) does not wait.
|
|
78
|
+
this._executing = false;
|
|
79
|
+
this._fdcAhead = 0;
|
|
80
|
+
this.io.onWait = (runToDrq) => {
|
|
81
|
+
if (!this._executing) return;
|
|
82
|
+
const held = runToDrq();
|
|
83
|
+
this.cpu.cycles += held;
|
|
84
|
+
this._fdcAhead += held;
|
|
85
|
+
};
|
|
86
|
+
|
|
50
87
|
this.memory.loadROM(romData);
|
|
51
88
|
|
|
52
|
-
this.nextRtcAt =
|
|
89
|
+
this.nextRtcAt = this.rtcInterval;
|
|
53
90
|
this.reset();
|
|
54
91
|
}
|
|
55
92
|
|
|
@@ -63,7 +100,7 @@ export class TRS80System {
|
|
|
63
100
|
this.io.reset();
|
|
64
101
|
this.keyboard.reset();
|
|
65
102
|
this.memory.clearVideoRAM();
|
|
66
|
-
this.nextRtcAt = this.cpu.cycles +
|
|
103
|
+
this.nextRtcAt = this.cpu.cycles + this.rtcInterval;
|
|
67
104
|
this._nmiWasPending = false;
|
|
68
105
|
}
|
|
69
106
|
|
|
@@ -98,44 +135,64 @@ export class TRS80System {
|
|
|
98
135
|
stepInstruction(beforeExecute) {
|
|
99
136
|
const start = this.cpu.cycles;
|
|
100
137
|
if (this.cpu.cycles >= this.nextRtcAt) {
|
|
101
|
-
this.io.raiseRTC();
|
|
102
|
-
this.
|
|
138
|
+
if (this.model.id === 3) this.io.raiseRTC();
|
|
139
|
+
else if (this.io.latch !== null) this.io.latch.raise(TIMER_BIT);
|
|
140
|
+
this.nextRtcAt += this.rtcInterval;
|
|
103
141
|
}
|
|
104
142
|
if (this.io.pendingInterrupt()) {
|
|
105
143
|
this.cpu.interrupt();
|
|
106
144
|
}
|
|
107
|
-
// FDC INTRQ -> NMI, edge-triggered so one assertion fires one NMI
|
|
145
|
+
// FDC INTRQ -> NMI, edge-triggered so one assertion fires one NMI. The
|
|
146
|
+
// CPU holds an NMI off after a lone DD/FD prefix, as it does a maskable
|
|
147
|
+
// interrupt (Young §3.6); the edge then stays pending for the next
|
|
148
|
+
// boundary.
|
|
108
149
|
const nmiNow = this.io.nmiPending();
|
|
109
150
|
if (nmiNow && !this._nmiWasPending) {
|
|
110
|
-
this.cpu.nmi();
|
|
151
|
+
if (this.cpu.nmi()) this._nmiWasPending = true;
|
|
152
|
+
} else {
|
|
153
|
+
this._nmiWasPending = nmiNow;
|
|
111
154
|
}
|
|
112
|
-
|
|
155
|
+
|
|
156
|
+
// The disk controller ticks by every T-state the boundary takes, an interrupt or NMI
|
|
157
|
+
// response included, so its clock (the disk's angle) keeps step with the CPU's: the response
|
|
158
|
+
// first, so an OUT that sets the WAIT finds the controller at the start of that OUT; then the
|
|
159
|
+
// instruction, less the T-states the WAIT already ran it ahead for (`_fdcAhead`).
|
|
160
|
+
const before = this.cpu.cycles;
|
|
161
|
+
if (before !== start) this.io.fdc.tick(before - start);
|
|
113
162
|
|
|
114
163
|
// beforeExecute returning true has executed the boundary's instruction itself (the
|
|
115
164
|
// Machine's ROM keyboard-scan hook stands in for a RET), so the CPU does not run one.
|
|
116
|
-
// Returning 'stop' runs nothing
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
165
|
+
// Returning 'stop' runs nothing; the return value is then the response's T-states.
|
|
166
|
+
// try/finally: a callback or an instruction that throws must not leave `_executing` set, or
|
|
167
|
+
// a later debugger port poke would wait as if an instruction were running.
|
|
168
|
+
this._executing = true;
|
|
169
|
+
try {
|
|
170
|
+
if (beforeExecute !== undefined) {
|
|
171
|
+
const r = beforeExecute();
|
|
172
|
+
if (r === 'stop') return before - start;
|
|
173
|
+
if (r !== true) this.cpu.executeInstruction();
|
|
174
|
+
} else {
|
|
175
|
+
this.cpu.executeInstruction();
|
|
123
176
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
} catch (e) {
|
|
178
|
+
this._fdcAhead = 0;
|
|
179
|
+
throw e;
|
|
180
|
+
} finally {
|
|
181
|
+
this._executing = false;
|
|
127
182
|
}
|
|
128
|
-
this.io.fdc.tick(this.cpu.cycles - before);
|
|
183
|
+
this.io.fdc.tick(this.cpu.cycles - before - this._fdcAhead);
|
|
184
|
+
this._fdcAhead = 0;
|
|
129
185
|
return this.cpu.cycles - start;
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
/**
|
|
133
|
-
* True when the machine is in 32-character mode (
|
|
134
|
-
*
|
|
135
|
-
*
|
|
189
|
+
* True when the machine is in 32-character mode (on the Model III the
|
|
190
|
+
* mode register's bit 2, on the Model I port $FF's MODESEL, bit 3; set by
|
|
191
|
+
* the ROM for CHR$(23), cleared by CLS). The ROM lays 32-column text at
|
|
192
|
+
* even video addresses; hardware shows them double-wide.
|
|
136
193
|
*/
|
|
137
194
|
get columns32() {
|
|
138
|
-
return (this.io.modeRegister &
|
|
195
|
+
return (this.io.modeRegister & this._mode32Bit) !== 0;
|
|
139
196
|
}
|
|
140
197
|
|
|
141
198
|
/** Mount a .dsk image (DiskImage) in drive 0-3. */
|
|
@@ -176,7 +233,7 @@ export class TRS80System {
|
|
|
176
233
|
|
|
177
234
|
/** Convenience for tests: run N emulated seconds. */
|
|
178
235
|
runSeconds(seconds) {
|
|
179
|
-
return this.runTStates(Math.round(seconds *
|
|
236
|
+
return this.runTStates(Math.round(seconds * this.clockHz));
|
|
180
237
|
}
|
|
181
238
|
|
|
182
239
|
/**
|