jsbeeb 1.13.1 → 1.15.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 +97 -2
- package/package.json +23 -16
- package/public/roms/tube/65C102Tube.rom +0 -0
- package/src/6502.js +74 -17
- package/src/acia.js +29 -11
- package/src/bem-snapshot.js +154 -15
- package/src/config.js +114 -83
- package/src/disc.js +47 -5
- package/src/dom-utils.js +16 -0
- package/src/fake6502.js +7 -2
- package/src/gamepads.js +11 -4
- package/src/keyboard.js +3 -2
- package/src/machine-session.js +11 -2
- package/src/main.js +117 -100
- package/src/models.js +57 -5
- package/src/mouse-coordinates.js +16 -0
- package/src/serial.js +1 -0
- package/src/snapshot-helpers.js +17 -3
- package/src/snapshot.js +19 -1
- package/src/soundchip.js +23 -5
- package/src/teletext_adaptor.js +40 -14
- package/src/tube.js +33 -1
- package/src/url-params.js +23 -19
- package/src/utils.js +10 -5
- package/src/utils_atom.js +9 -5
- package/src/web/audio-handler.js +4 -1
- package/tests/test-machine.js +7 -5
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ different peripherals.
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
13
|
- [Keyboard Mappings](#keyboard-mappings)
|
|
14
|
+
- [Remapping Keys](#remapping-keys)
|
|
14
15
|
- [Emulator Shortcuts](#emulator-shortcuts)
|
|
15
16
|
- [Save State and Rewind](#save-state-and-rewind)
|
|
16
17
|
- [Getting Set Up to Run Locally](#getting-set-up-to-run-locally)
|
|
@@ -36,6 +37,65 @@ The BBC had a somewhat different-looking keyboard to a modern PC, and so it's us
|
|
|
36
37
|
To play right now, visit [https://bbc.xania.org/](https://bbc.xania.org/). To load the default disc image (Elite in this
|
|
37
38
|
case), press shift-F12 (which is shift-Break on the BBC).
|
|
38
39
|
|
|
40
|
+
### Remapping Keys
|
|
41
|
+
|
|
42
|
+
Plenty of games use keys that are awkward on a modern keyboard: `COPY` (which is `End`, or `fn`+`→` on a Mac), or
|
|
43
|
+
`CAPS LOCK` (which on a Mac toggles rather than acting as a key you hold down). Any host key can be made to press any
|
|
44
|
+
BBC key by adding a `KEY.` parameter to the URL:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
KEY.<host key>=<BBC key>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add one for each key you want to change. For example, Superior Software's Space Invaders fires with `COPY`; this makes
|
|
51
|
+
`Enter` fire instead:
|
|
52
|
+
|
|
53
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY)
|
|
54
|
+
|
|
55
|
+
Superior's Frogger uses `A`/`Z`/`DELETE`/`COPY` to move; this puts it on the arrow keys:
|
|
56
|
+
|
|
57
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FFrogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY)
|
|
58
|
+
|
|
59
|
+
And Superior's Hunchback steers with `CAPS LOCK` and `CTRL`, which the arrow keys can stand in for:
|
|
60
|
+
|
|
61
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FHunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL)
|
|
62
|
+
|
|
63
|
+
The **host key** names are jsbeeb's names for the keys on your own keyboard. Most are what you'd expect, but note:
|
|
64
|
+
|
|
65
|
+
- `ENTER` (the BBC's `RETURN` key is called `ENTER` on the host side)
|
|
66
|
+
- `K0` to `K9` for the number keys, `NUMPAD0` to `NUMPAD9` for the keypad
|
|
67
|
+
- `SHIFT_LEFT` / `SHIFT_RIGHT`, `CTRL_LEFT` / `CTRL_RIGHT`, `ALT_LEFT` / `ALT_RIGHT` to distinguish the two of each
|
|
68
|
+
- `BACK_QUOTE`, `APOSTROPHE`, `SEMICOLON`, `MINUS`, `EQUALS`, `HASH`, `BACKSLASH`, `LEFT_SQUARE_BRACKET`,
|
|
69
|
+
`RIGHT_SQUARE_BRACKET` for punctuation
|
|
70
|
+
|
|
71
|
+
The **BBC key** names are:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
RETURN COPY DELETE ESCAPE TAB SPACE SHIFT SHIFTLOCK CAPSLOCK CTRL
|
|
75
|
+
LEFT RIGHT UP DOWN
|
|
76
|
+
A-Z, K0-K9 (the number keys), F0-F9 (the red function keys)
|
|
77
|
+
SEMICOLON_PLUS MINUS COMMA PERIOD SLASH AT COLON_STAR HAT_TILDE
|
|
78
|
+
UNDERSCORE_POUND PIPE_BACKSLASH LEFT_SQUARE_BRACKET RIGHT_SQUARE_BRACKET
|
|
79
|
+
|
|
80
|
+
(and, on the Master's numeric keypad only)
|
|
81
|
+
NUMPAD0-NUMPAD9 NUMPADPLUS NUMPADMINUS NUMPADSLASH NUMPADASTERISK NUMPADCOMMA
|
|
82
|
+
NUMPADHASH NUMPADENTER NUMPAD_DELETE NUMPAD_DECIMAL_POINT
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Some things to know:
|
|
86
|
+
|
|
87
|
+
- Names are case-insensitive, and a remapped key ignores the `SHIFT` state, so `KEY.ENTER=COPY` presses `COPY` whether
|
|
88
|
+
or not shift is held.
|
|
89
|
+
- Remapping replaces what that host key normally does; in the Space Invaders example above, `Enter` no longer presses
|
|
90
|
+
`RETURN`.
|
|
91
|
+
- The remapping is applied on top of whichever keyboard layout is selected, and survives changing layout or model.
|
|
92
|
+
- If a name isn't recognised the mapping is skipped, and the emulator says so on startup, naming the parameter that
|
|
93
|
+
was at fault.
|
|
94
|
+
- On the Atom, use the Atom's own key names (`LOCK`, `UP_DOWN`, `LEFT_RIGHT` and so on) rather than the BBC's.
|
|
95
|
+
|
|
96
|
+
The definitive lists are `keyCodes` (host) and `BBC` (BBC micro) in [`src/utils.js`](src/utils.js), and `ATOM` in
|
|
97
|
+
[`src/utils_atom.js`](src/utils_atom.js).
|
|
98
|
+
|
|
39
99
|
### Emulator Shortcuts
|
|
40
100
|
|
|
41
101
|
| Shortcut | Action |
|
|
@@ -63,6 +123,35 @@ jsbeeb supports both USB/Bluetooth gamepads and mouse-based analogue joystick em
|
|
|
63
123
|
- X-axis: Left = 65535, Right = 0
|
|
64
124
|
- Y-axis: Up = 65535, Down = 0
|
|
65
125
|
|
|
126
|
+
A gamepad presses BBC keys, and which key each control presses can be changed from the URL in the same way as
|
|
127
|
+
[remapping the keyboard](#remapping-keys):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
GP.<gamepad control>=<BBC key>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
By default the D-pad presses the "Snapper" keys (`Z`, `X`, `:`, `/`), the `A` button presses `RETURN` and `Start`
|
|
134
|
+
presses `SPACE`. To play Superior's Space Invaders on a pad, where `COPY` fires:
|
|
135
|
+
|
|
136
|
+
[`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY)
|
|
137
|
+
|
|
138
|
+
The gamepad control names are:
|
|
139
|
+
|
|
140
|
+
- `FIRE` — every button at once, which is usually what you want for a one-button game
|
|
141
|
+
- `UP` `DOWN` `LEFT` `RIGHT` — both analogue sticks at once, plus one face button each (`UP` is also `A`, `DOWN` is
|
|
142
|
+
`X`, `LEFT` is `Y`, `RIGHT` is `B`)
|
|
143
|
+
- `UP1` `DOWN1` `LEFT1` `RIGHT1` — the left stick only; `UP2` `DOWN2` … — the right stick only; `UP3` `DOWN3` … — the
|
|
144
|
+
face buttons only
|
|
145
|
+
- `A` `B` `X` `Y` `START` `BACK` `LB` `RB` `LT` `RT` — individual buttons, by their Xbox 360 names
|
|
146
|
+
- `FIRE1` `FIRE2` — clicking the left and right sticks
|
|
147
|
+
|
|
148
|
+
The BBC key names are the same as for the keyboard, and digits may be written either way round: `GP.A=1` and `GP.A=K1`
|
|
149
|
+
both press `1`. Unlike `KEY.`, gamepad mappings are BBC-only: there's no Atom equivalent. The D-pad's default mapping
|
|
150
|
+
can't currently be changed.
|
|
151
|
+
|
|
152
|
+
The older `LEFT=`, `RIGHT=`, `UP=`, `DOWN=` and `FIRE=` parameters (no `GP.` prefix) still work and mean the same
|
|
153
|
+
thing.
|
|
154
|
+
|
|
66
155
|
## Getting Set Up to Run Locally
|
|
67
156
|
|
|
68
157
|
### Prerequisites
|
|
@@ -157,6 +246,8 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
157
246
|
- `disc1=sth:ZZZ` - loads disc ZZZ from the Stairway to Hell archive
|
|
158
247
|
- `tape=XXX` - loads tape XXX (from the `tapes/` directory)
|
|
159
248
|
- `tape=sth:ZZZ` - loads tape ZZZ from the Stairway to Hell archive
|
|
249
|
+
- `KEY.X=Y` - makes host key `X` press BBC key `Y`, e.g. `KEY.ENTER=COPY`. See
|
|
250
|
+
[Remapping Keys](#remapping-keys).
|
|
160
251
|
- `patch=P` - applies a memory patch `P`. See below.
|
|
161
252
|
- `loadBasic=X` - loads 'X' (a resource on the webserver) as text, tokenises it and puts it in `PAGE` as if you'd typed
|
|
162
253
|
it in to the emulator
|
|
@@ -167,8 +258,12 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
167
258
|
- `autotype` - types whatever you put after. e.g. `&autotype=PRINT"Matt is cool"%0a` (return is URI escaped to `%0a`)
|
|
168
259
|
- `embed` - Remove the margins around the screen, hide most navigation entries and make the page background
|
|
169
260
|
transparent (intended for use when running within an iframe in a third-party site).
|
|
170
|
-
- `cpuMultiplier=X` speeds up the CPU by a factor of `X
|
|
171
|
-
|
|
261
|
+
- `cpuMultiplier=X` speeds up the CPU by a factor of `X` relative to the peripherals: video, sound and the VIAs keep
|
|
262
|
+
running at their real-world rates. May be fractional or below one to slow the CPU down. NB disc loads become
|
|
263
|
+
unreliable with a too-slow CPU, and running too fast might cause the browser to hang.
|
|
264
|
+
- `tubeCpuMultiplier=X` overclocks the second processor by a factor of `X`, which may be fractional. `1`, the default,
|
|
265
|
+
runs it at the real part's own clock: 3MHz for the 6502 second processor a BBC B takes, 4MHz for the 65C102 Turbo
|
|
266
|
+
board a Master takes. Below about 2.2MHz the MOS's unhandshaken tube transfers lose data.
|
|
172
267
|
- `sbLeft` / `sbRight` / `sbBottom` - a URL to place left of, right of, or below the cub monitor. The left and right
|
|
173
268
|
should be around 648 high and the bottom image should be around 896 wide. Left and right wider than 300 will run into
|
|
174
269
|
problems on smaller screens; bottom taller than 100 or so similarly.
|
package/package.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
11
|
-
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
10
|
+
"version": "1.15.0",
|
|
11
|
+
"//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24.15.0"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./src/app/app.js",
|
|
@@ -27,33 +27,37 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@popperjs/core": "^2.11.8",
|
|
30
|
-
"argparse": "^
|
|
30
|
+
"argparse": "^3.0.0",
|
|
31
31
|
"bootstrap": "^5.3.8",
|
|
32
32
|
"bootswatch": "^5.3.8",
|
|
33
|
-
"sharp": "^0.
|
|
33
|
+
"sharp": "^0.35.3",
|
|
34
34
|
"smoothie": "^1.36.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
|
38
38
|
"@vitest/coverage-v8": "^4.0.18",
|
|
39
|
-
"eslint": "^10.
|
|
39
|
+
"eslint": "^10.7.0",
|
|
40
40
|
"eslint-config-prettier": "^10.1.8",
|
|
41
|
-
"eslint-plugin-prettier": "^5.5.
|
|
42
|
-
"globals": "^17.
|
|
41
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
42
|
+
"globals": "^17.7.0",
|
|
43
43
|
"husky": "^9.1.7",
|
|
44
|
-
"jsdom": "^
|
|
45
|
-
"lint-staged": "^
|
|
46
|
-
"npm-run-all2": "^
|
|
47
|
-
"pixelmatch": "^7.
|
|
48
|
-
"prettier": "^3.
|
|
49
|
-
"vite": "^8.
|
|
44
|
+
"jsdom": "^30.0.1",
|
|
45
|
+
"lint-staged": "^17.1.1",
|
|
46
|
+
"npm-run-all2": "^9.0.2",
|
|
47
|
+
"pixelmatch": "^7.2.0",
|
|
48
|
+
"prettier": "^3.9.6",
|
|
49
|
+
"vite": "^8.1.5",
|
|
50
50
|
"vitest": "^4.0.10"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"electron": "^
|
|
54
|
-
"electron-builder": "^26.
|
|
53
|
+
"electron": "^43.2.0",
|
|
54
|
+
"electron-builder": "^26.15.3",
|
|
55
55
|
"electron-store": "^11.0.2"
|
|
56
56
|
},
|
|
57
|
+
"//overrides": "brace-expansion forced to the patched version for GHSA-mh99-v99m-4gvg; the electron-builder dependency chain still pins vulnerable versions. Remove once `npm audit` is clean without it.",
|
|
58
|
+
"overrides": {
|
|
59
|
+
"brace-expansion": "^5.0.8"
|
|
60
|
+
},
|
|
57
61
|
"license": "GPL-3.0-or-later",
|
|
58
62
|
"build": {
|
|
59
63
|
"appId": "org.godbolt.bbc",
|
|
@@ -108,5 +112,8 @@
|
|
|
108
112
|
"prettier --write"
|
|
109
113
|
],
|
|
110
114
|
"*.{md,json,yml,yaml,css,html}": "prettier --write"
|
|
115
|
+
},
|
|
116
|
+
"allowScripts": {
|
|
117
|
+
"electron-winstaller@5.4.0": true
|
|
111
118
|
}
|
|
112
119
|
}
|
|
Binary file
|
package/src/6502.js
CHANGED
|
@@ -407,11 +407,14 @@ class Base6502 {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
class Tube6502 extends Base6502 {
|
|
410
|
-
constructor(model, cpu) {
|
|
410
|
+
constructor(model, cpu, { cpuMultiplier = 1 } = {}) {
|
|
411
411
|
super(model, { cycleAccurate: false });
|
|
412
412
|
|
|
413
|
+
if (!(cpuMultiplier > 0)) throw new Error(`Tube CPU multiplier must be positive, got ${cpuMultiplier}`);
|
|
414
|
+
|
|
413
415
|
this.cycles = 0;
|
|
414
|
-
this.
|
|
416
|
+
this.cyclesPerHostCycle = model.clockMhz / cpu.model.clockMhz;
|
|
417
|
+
this.cpuMultiplier = cpuMultiplier;
|
|
415
418
|
this.romPaged = true;
|
|
416
419
|
this.memory = new Uint8Array(65536);
|
|
417
420
|
this.rom = new Uint8Array(4096);
|
|
@@ -424,6 +427,10 @@ class Tube6502 extends Base6502 {
|
|
|
424
427
|
this.pc = this.readmem(0xfffc) | (this.readmem(0xfffd) << 8);
|
|
425
428
|
this.p.i = true;
|
|
426
429
|
this.s = (this.s - 3) & 0xff; // Simulate 3 dummy pushes during reset
|
|
430
|
+
// A latched NMI would otherwise survive the reset and be taken on the first instruction
|
|
431
|
+
// after it, defeating the empty R3 FIFO the ULA seeds for exactly that reason.
|
|
432
|
+
this._nmiLevel = this._nmiEdge = false;
|
|
433
|
+
this.takeInt = false;
|
|
427
434
|
this.tube.reset(hard);
|
|
428
435
|
}
|
|
429
436
|
|
|
@@ -472,7 +479,7 @@ class Tube6502 extends Base6502 {
|
|
|
472
479
|
}
|
|
473
480
|
|
|
474
481
|
execute(cycles) {
|
|
475
|
-
this.cycles +=
|
|
482
|
+
this.cycles += cycles * this.cyclesPerHostCycle * this.cpuMultiplier;
|
|
476
483
|
if (this.cycles < 3) return;
|
|
477
484
|
while (this.cycles > 0) {
|
|
478
485
|
const opcode = this.readmem(this.pc);
|
|
@@ -482,6 +489,46 @@ class Tube6502 extends Base6502 {
|
|
|
482
489
|
}
|
|
483
490
|
}
|
|
484
491
|
|
|
492
|
+
snapshotState({ includeRoms = false } = {}) {
|
|
493
|
+
return {
|
|
494
|
+
a: this.a,
|
|
495
|
+
x: this.x,
|
|
496
|
+
y: this.y,
|
|
497
|
+
s: this.s,
|
|
498
|
+
pc: this.pc,
|
|
499
|
+
p: this.p.asByte(),
|
|
500
|
+
nmiLevel: this._nmiLevel,
|
|
501
|
+
nmiEdge: this._nmiEdge,
|
|
502
|
+
takeInt: this.takeInt,
|
|
503
|
+
// The parasite has no scheduler of its own, so this is not relative to any epoch.
|
|
504
|
+
cycles: this.cycles,
|
|
505
|
+
romPaged: this.romPaged,
|
|
506
|
+
memory: this.memory.slice(),
|
|
507
|
+
rom: includeRoms ? this.rom.slice() : undefined,
|
|
508
|
+
ula: this.tube.snapshotState(),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
restoreState(state) {
|
|
513
|
+
this.a = state.a;
|
|
514
|
+
this.x = state.x;
|
|
515
|
+
this.y = state.y;
|
|
516
|
+
this.s = state.s;
|
|
517
|
+
this.pc = state.pc;
|
|
518
|
+
this.p.setFromByte(state.p);
|
|
519
|
+
this._nmiLevel = state.nmiLevel;
|
|
520
|
+
this._nmiEdge = state.nmiEdge;
|
|
521
|
+
this.takeInt = state.takeInt;
|
|
522
|
+
this.cycles = state.cycles;
|
|
523
|
+
this.romPaged = state.romPaged;
|
|
524
|
+
this.memory.set(state.memory);
|
|
525
|
+
if (state.rom) this.rom.set(state.rom);
|
|
526
|
+
|
|
527
|
+
// After the registers, so the edge-triggered NMI moves from its saved level to the one
|
|
528
|
+
// the ULA implies: no edge if they agree, and the edge it is owed if they do not.
|
|
529
|
+
this.tube.restoreState(state.ula);
|
|
530
|
+
}
|
|
531
|
+
|
|
485
532
|
async loadOs() {
|
|
486
533
|
console.log("Loading tube rom from roms/" + this.model.os);
|
|
487
534
|
const tubeRom = this.rom;
|
|
@@ -620,10 +667,13 @@ export class Cpu6502 extends Base6502 {
|
|
|
620
667
|
this.cpuMultiplier = this.config.cpuMultiplier;
|
|
621
668
|
this.videoCyclesBatch = this.config.videoCyclesBatch | 0;
|
|
622
669
|
this.peripheralCyclesPerSecond = 2 * 1000 * 1000;
|
|
623
|
-
this.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
670
|
+
this.hasTube = !!this.config.tube;
|
|
671
|
+
this.hasMusic5000 = !!this.config.hasMusic5000;
|
|
672
|
+
this.hasTeletextAdaptor = !!this.config.hasTeletextAdaptor;
|
|
673
|
+
this.teletextAdaptor = this.hasTeletextAdaptor ? new TeletextAdaptor(this) : null;
|
|
674
|
+
this.tube = this.hasTube
|
|
675
|
+
? new Tube6502(this.config.tube, this, { cpuMultiplier: this.config.tubeCpuMultiplier })
|
|
676
|
+
: new FakeTube();
|
|
627
677
|
this.music5000PageSel = 0;
|
|
628
678
|
this.econet = econet;
|
|
629
679
|
|
|
@@ -648,7 +698,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
648
698
|
getGamepads: this.config.getGamepads,
|
|
649
699
|
});
|
|
650
700
|
this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
|
|
651
|
-
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.
|
|
701
|
+
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.relayNoise);
|
|
652
702
|
this.serial = new Serial(this.acia);
|
|
653
703
|
this.adconverter = new Adc(this.sysvia, this.scheduler);
|
|
654
704
|
this.soundChip.setScheduler(this.scheduler);
|
|
@@ -799,7 +849,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
799
849
|
|
|
800
850
|
switch (addr & ~0x0003) {
|
|
801
851
|
case 0xfc10:
|
|
802
|
-
if (this.
|
|
852
|
+
if (this.hasTeletextAdaptor) return this.teletextAdaptor.read(addr - 0xfc10);
|
|
803
853
|
break;
|
|
804
854
|
case 0xfc20:
|
|
805
855
|
case 0xfc24:
|
|
@@ -822,7 +872,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
822
872
|
// IDE
|
|
823
873
|
break;
|
|
824
874
|
case 0xfcfc:
|
|
825
|
-
if (addr === 0xfcff && this.
|
|
875
|
+
if (addr === 0xfcff && this.hasMusic5000) return this.music5000PageSel;
|
|
826
876
|
break;
|
|
827
877
|
case 0xfe00:
|
|
828
878
|
case 0xfe04:
|
|
@@ -909,7 +959,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
909
959
|
return this.tube.read(addr);
|
|
910
960
|
}
|
|
911
961
|
|
|
912
|
-
if (this.
|
|
962
|
+
if (this.hasMusic5000) {
|
|
913
963
|
if ((this.music5000PageSel & 0xf0) === 0x30 && (addr & 0xff00) === 0xfd00) {
|
|
914
964
|
return this.music5000.read(this.music5000PageSel, addr);
|
|
915
965
|
}
|
|
@@ -966,14 +1016,14 @@ export class Cpu6502 extends Base6502 {
|
|
|
966
1016
|
addr &= 0xffff;
|
|
967
1017
|
b |= 0;
|
|
968
1018
|
|
|
969
|
-
if (this.
|
|
1019
|
+
if (this.hasMusic5000 && (addr & 0xff00) === 0xfd00 && (this.music5000PageSel & 0xf0) === 0x30) {
|
|
970
1020
|
this.music5000.write(this.music5000PageSel, addr, b);
|
|
971
1021
|
return;
|
|
972
1022
|
}
|
|
973
1023
|
|
|
974
1024
|
switch (addr & ~0x0003) {
|
|
975
1025
|
case 0xfc10:
|
|
976
|
-
if (this.
|
|
1026
|
+
if (this.hasTeletextAdaptor) return this.teletextAdaptor.write(addr - 0xfc10, b);
|
|
977
1027
|
break;
|
|
978
1028
|
case 0xfc20:
|
|
979
1029
|
case 0xfc24:
|
|
@@ -996,7 +1046,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
996
1046
|
// IDE
|
|
997
1047
|
break;
|
|
998
1048
|
case 0xfcfc:
|
|
999
|
-
if (addr === 0xfcff && this.
|
|
1049
|
+
if (addr === 0xfcff && this.hasMusic5000) {
|
|
1000
1050
|
this.music5000PageSel = b;
|
|
1001
1051
|
}
|
|
1002
1052
|
break;
|
|
@@ -1183,6 +1233,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1183
1233
|
acia: this.acia.snapshotState(),
|
|
1184
1234
|
adc: this.adconverter.snapshotState(),
|
|
1185
1235
|
fdc: this.fdc.snapshotState(),
|
|
1236
|
+
tube: this.hasTube ? this.tube.snapshotState({ includeRoms }) : undefined,
|
|
1186
1237
|
};
|
|
1187
1238
|
}
|
|
1188
1239
|
|
|
@@ -1243,6 +1294,13 @@ export class Cpu6502 extends Base6502 {
|
|
|
1243
1294
|
if (state.fdc) {
|
|
1244
1295
|
this.fdc.restoreState(state.fdc);
|
|
1245
1296
|
}
|
|
1297
|
+
|
|
1298
|
+
// Tube state (v3+). Rather than leave the parasite running on state the host knows
|
|
1299
|
+
// nothing about, reset it; restoreSnapshot() rejects such a pairing before reaching here.
|
|
1300
|
+
if (this.hasTube) {
|
|
1301
|
+
if (state.tube) this.tube.restoreState(state.tube);
|
|
1302
|
+
else this.tube.reset(true);
|
|
1303
|
+
}
|
|
1246
1304
|
}
|
|
1247
1305
|
|
|
1248
1306
|
reset(hard) {
|
|
@@ -1304,7 +1362,6 @@ export class Cpu6502 extends Base6502 {
|
|
|
1304
1362
|
this.adconverter.reset();
|
|
1305
1363
|
|
|
1306
1364
|
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1307
|
-
if (this.model.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
|
|
1308
1365
|
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1309
1366
|
}
|
|
1310
1367
|
|
|
@@ -1364,7 +1421,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1364
1421
|
// Builds common code between polltimeSlow and polltimeFast
|
|
1365
1422
|
buildPolltime() {
|
|
1366
1423
|
const nop = (_cycles) => {};
|
|
1367
|
-
const tubeStuff = (cycles) =>
|
|
1424
|
+
const tubeStuff = this.hasTube ? (cycles) => this.tube.execute(cycles) : nop;
|
|
1368
1425
|
const teletextStuff = this.teletextAdaptor ? (cycles) => this.teletextAdaptor.polltime(cycles) : nop;
|
|
1369
1426
|
const musicStuff = this.music5000 ? (cycles) => this.music5000.polltime(cycles) : nop;
|
|
1370
1427
|
const econetStuff = this.econet
|
|
@@ -1506,7 +1563,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1506
1563
|
if (this.model.os.length) {
|
|
1507
1564
|
await this.loadOs.apply(this, this.model.os);
|
|
1508
1565
|
}
|
|
1509
|
-
if (this.
|
|
1566
|
+
if (this.hasTube) {
|
|
1510
1567
|
await this.tube.loadOs();
|
|
1511
1568
|
}
|
|
1512
1569
|
this.reset(true);
|
package/src/acia.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// http://www.classiccmp.org/dunfield/r/6850.pdf
|
|
6
6
|
|
|
7
7
|
export class Acia {
|
|
8
|
-
constructor(cpu, toneGen, scheduler,
|
|
8
|
+
constructor(cpu, toneGen, scheduler, relayNoise) {
|
|
9
9
|
this.cpu = cpu;
|
|
10
10
|
this.toneGen = toneGen;
|
|
11
|
-
this.rs423Handler =
|
|
11
|
+
this.rs423Handler = null;
|
|
12
12
|
this.relayNoise = relayNoise;
|
|
13
13
|
|
|
14
14
|
this.sr = 0x00;
|
|
@@ -22,17 +22,27 @@ export class Acia {
|
|
|
22
22
|
this.hadDcdHigh = false;
|
|
23
23
|
this.serialReceiveRate = 0;
|
|
24
24
|
this.serialReceiveCyclesPerByte = 0;
|
|
25
|
+
this.serialTransmitRate = 0;
|
|
26
|
+
this.serialTransmitCyclesPerByte = 0;
|
|
25
27
|
|
|
26
28
|
this.setSerialReceive(19200);
|
|
29
|
+
this.setSerialTransmit(19200);
|
|
27
30
|
this.txCompleteTask = scheduler.newTask(() => {
|
|
28
31
|
this.sr |= 0x02; // set the TDRE
|
|
32
|
+
this.updateIrq();
|
|
29
33
|
});
|
|
30
34
|
this.runTapeTask = scheduler.newTask(() => this.runTape());
|
|
31
35
|
this.runRs423Task = scheduler.newTask(() => this.runRs423());
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// MC6850: the transmit interrupt is selected by CR6:CR5 = 01 and follows
|
|
39
|
+
// TDRE, which a high CTS inhibits.
|
|
40
|
+
txIrq() {
|
|
41
|
+
return (this.cr & 0x60) === 0x20 && (this.sr & 0x0a) === 0x02;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
updateIrq() {
|
|
35
|
-
if (this.sr & this.cr & 0x80) {
|
|
45
|
+
if (this.sr & this.cr & 0x80 || this.txIrq()) {
|
|
36
46
|
this.cpu.interrupt |= 0x04;
|
|
37
47
|
} else {
|
|
38
48
|
this.cpu.interrupt &= ~0x04;
|
|
@@ -77,6 +87,7 @@ export class Acia {
|
|
|
77
87
|
return this.dr;
|
|
78
88
|
} else {
|
|
79
89
|
let result = (this.sr & 0x7f) | (this.sr & this.cr & 0x80);
|
|
90
|
+
if (this.txIrq()) result |= 0x80;
|
|
80
91
|
// MC6850: "A low CTS indicates that there is a Clear-to-Send
|
|
81
92
|
// from the modem. In the high state, the Transmit Data Register
|
|
82
93
|
// Empty bit is inhibited".
|
|
@@ -100,10 +111,7 @@ export class Acia {
|
|
|
100
111
|
write(addr, val) {
|
|
101
112
|
if (addr & 1) {
|
|
102
113
|
this.sr &= ~0x02;
|
|
103
|
-
|
|
104
|
-
// That could be straight away if not already tx-ing, but as we don't really tx,
|
|
105
|
-
// be conservative here.
|
|
106
|
-
this.txCompleteTask.reschedule(2000);
|
|
114
|
+
this.txCompleteTask.reschedule(this.serialTransmitCyclesPerByte);
|
|
107
115
|
this.updateIrq();
|
|
108
116
|
if (this.rs423Selected && this.rs423Handler) this.rs423Handler.onTransmit(val);
|
|
109
117
|
} else {
|
|
@@ -114,6 +122,8 @@ export class Acia {
|
|
|
114
122
|
} else {
|
|
115
123
|
this.cr = val;
|
|
116
124
|
this.setSerialReceive(this.serialReceiveRate);
|
|
125
|
+
this.setSerialTransmit(this.serialTransmitRate);
|
|
126
|
+
this.updateIrq();
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
}
|
|
@@ -212,7 +222,7 @@ export class Acia {
|
|
|
212
222
|
tapeDcdLineLevel: this.tapeDcdLineLevel,
|
|
213
223
|
hadDcdHigh: this.hadDcdHigh,
|
|
214
224
|
serialReceiveRate: this.serialReceiveRate,
|
|
215
|
-
|
|
225
|
+
serialTransmitRate: this.serialTransmitRate,
|
|
216
226
|
txCompleteTaskOffset: this.txCompleteTask.scheduled()
|
|
217
227
|
? this.txCompleteTask.expireEpoch - scheduler.epoch
|
|
218
228
|
: null,
|
|
@@ -230,8 +240,10 @@ export class Acia {
|
|
|
230
240
|
this.tapeCarrierCount = state.tapeCarrierCount;
|
|
231
241
|
this.tapeDcdLineLevel = state.tapeDcdLineLevel;
|
|
232
242
|
this.hadDcdHigh = state.hadDcdHigh;
|
|
233
|
-
|
|
234
|
-
this.
|
|
243
|
+
// The byte times are derived from the rates and the restored word format, not saved.
|
|
244
|
+
this.setSerialReceive(state.serialReceiveRate);
|
|
245
|
+
// Snapshots predating transmit timing have no saved rate.
|
|
246
|
+
this.setSerialTransmit(state.serialTransmitRate ?? 19200);
|
|
235
247
|
this.updateIrq();
|
|
236
248
|
|
|
237
249
|
this.txCompleteTask.cancel();
|
|
@@ -294,7 +306,8 @@ export class Acia {
|
|
|
294
306
|
parityBits = 1;
|
|
295
307
|
break;
|
|
296
308
|
}
|
|
297
|
-
|
|
309
|
+
const startBits = 1;
|
|
310
|
+
return startBits + wordLength + stopBits + parityBits;
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
rts() {
|
|
@@ -307,6 +320,11 @@ export class Acia {
|
|
|
307
320
|
this.serialReceiveCyclesPerByte = this.secondsToCycles(this.numBitsPerByte() / rate);
|
|
308
321
|
}
|
|
309
322
|
|
|
323
|
+
setSerialTransmit(rate) {
|
|
324
|
+
this.serialTransmitRate = rate;
|
|
325
|
+
this.serialTransmitCyclesPerByte = this.secondsToCycles(this.numBitsPerByte() / rate);
|
|
326
|
+
}
|
|
327
|
+
|
|
310
328
|
runTape() {
|
|
311
329
|
if (this.tape) this.runTapeTask.reschedule(this.tape.poll(this));
|
|
312
330
|
}
|