jsbeeb 1.12.0 → 1.13.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 +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://bbc.xania.org/)
|
|
6
6
|
|
|
7
|
-
A BBC Micro emulator written in JavaScript and running in modern browsers. Emulates a 32K BBC B
|
|
8
|
-
|
|
7
|
+
A BBC Micro and other 8-bit Acorn emulator written in JavaScript and running in modern browsers. Emulates a 32K BBC B
|
|
8
|
+
(with sideways RAM), a 128K BBC Master, and an Acorn Atom (with AtoMMC2 SD card interface), along with a number of
|
|
9
|
+
different peripherals.
|
|
9
10
|
|
|
10
11
|
## Table of Contents
|
|
11
12
|
|
|
@@ -179,6 +180,14 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
179
180
|
- (mostly internal use) `logFdcCommands`, `logFdcStateChanges` - turn on logging in the disc controller.
|
|
180
181
|
- `audioDebug` - show audio queue stats chart.
|
|
181
182
|
|
|
183
|
+
### Atom-specific parameters
|
|
184
|
+
|
|
185
|
+
- `model=Atom` - select the Acorn Atom (MMC) model. Other Atom variants: `Atom-Tape`, `Atom-Tape-FP`, `Atom-DOS`.
|
|
186
|
+
- `mmc=XXX` - load an MMC/SD card image (ZIP) for the Atom.
|
|
187
|
+
|
|
188
|
+
Atom models can also be selected automatically by hostname: any hostname starting with `atom` (e.g. `atom.xania.org`)
|
|
189
|
+
defaults to the Atom model.
|
|
190
|
+
|
|
182
191
|
## Patches
|
|
183
192
|
|
|
184
193
|
Patches can be applied by making a `patch=P` URL parameter. `P` is a sequence of semicolon-separated patches of the form
|
|
@@ -258,6 +267,11 @@ Cheers to [Ed Spittles](https://github.com/BigEd) for testing various interrupt
|
|
|
258
267
|
|
|
259
268
|
Thanks to Chris Jordan for his thorough testing, bug reports, ideas and help.
|
|
260
269
|
|
|
270
|
+
Huge thanks to [Andrew Hague](https://github.com/CommanderCoder) (CommanderCoder) for the Acorn Atom emulation
|
|
271
|
+
support. Andrew developed the original Atom implementation including the MC6847 video chip, 8255 PPIA, AtoMMC2 SD card
|
|
272
|
+
interface, Atom keyboard mapping, tape support, and speaker output. His work in [PR #505](https://github.com/mattgodbolt/jsbeeb/pull/505)
|
|
273
|
+
was incrementally merged and refined into the codebase.
|
|
274
|
+
|
|
261
275
|
A lot of the early development used the amazing [Visual 6502](http://visual6502.org/) as reference for intra-instruction
|
|
262
276
|
timings. Amazing stuff.
|
|
263
277
|
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.13.0",
|
|
11
11
|
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=22.12.0"
|
|
@@ -36,21 +36,21 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
|
38
38
|
"@vitest/coverage-v8": "^4.0.18",
|
|
39
|
-
"eslint": "^10.
|
|
39
|
+
"eslint": "^10.2.0",
|
|
40
40
|
"eslint-config-prettier": "^10.1.8",
|
|
41
41
|
"eslint-plugin-prettier": "^5.5.5",
|
|
42
|
-
"globals": "^17.
|
|
42
|
+
"globals": "^17.5.0",
|
|
43
43
|
"husky": "^9.1.7",
|
|
44
|
-
"jsdom": "^29.0.
|
|
45
|
-
"lint-staged": "^16.
|
|
44
|
+
"jsdom": "^29.0.2",
|
|
45
|
+
"lint-staged": "^16.4.0",
|
|
46
46
|
"npm-run-all2": "^8.0.4",
|
|
47
47
|
"pixelmatch": "^7.1.0",
|
|
48
|
-
"prettier": "^3.8.
|
|
49
|
-
"vite": "^8.0.
|
|
48
|
+
"prettier": "^3.8.2",
|
|
49
|
+
"vite": "^8.0.8",
|
|
50
50
|
"vitest": "^4.0.10"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"electron": "^41.
|
|
53
|
+
"electron": "^41.2.0",
|
|
54
54
|
"electron-builder": "^26.8.1",
|
|
55
55
|
"electron-store": "^11.0.2"
|
|
56
56
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/6502.js
CHANGED
|
@@ -10,6 +10,8 @@ import { TouchScreen } from "./touchscreen.js";
|
|
|
10
10
|
import { TeletextAdaptor } from "./teletext_adaptor.js";
|
|
11
11
|
import { Filestore } from "./filestore.js";
|
|
12
12
|
import { FakeRelayNoise } from "./relaynoise.js";
|
|
13
|
+
import { AtomPPIA } from "./ppia.js";
|
|
14
|
+
import { AtomMMC2 } from "./mmc.js";
|
|
13
15
|
|
|
14
16
|
const signExtend = utils.signExtend;
|
|
15
17
|
|
|
@@ -1245,53 +1247,69 @@ export class Cpu6502 extends Base6502 {
|
|
|
1245
1247
|
|
|
1246
1248
|
reset(hard) {
|
|
1247
1249
|
if (hard) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
this.memStatOffsetByIFetchBank = this.model.isMaster ? (1 << 0xc) | (1 << 0xd) : 0x0000;
|
|
1251
|
-
if (!this.model.isTest) {
|
|
1252
|
-
for (let i = 0; i < 128; ++i) this.memStat[i] = this.memStat[256 + i] = 1;
|
|
1253
|
-
for (let i = 128; i < 256; ++i) this.memStat[i] = this.memStat[256 + i] = 2;
|
|
1254
|
-
for (let i = 0; i < 128; ++i) this.memLook[i] = this.memLook[256 + i] = 0;
|
|
1255
|
-
for (let i = 128; i < 192; ++i) this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0x8000;
|
|
1256
|
-
for (let i = 192; i < 256; ++i) this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xc000;
|
|
1257
|
-
|
|
1258
|
-
for (let i = 0xfc; i < 0xff; ++i) this.memStat[i] = this.memStat[256 + i] = 0;
|
|
1259
|
-
} else {
|
|
1260
|
-
// Test sets everything as RAM.
|
|
1261
|
-
for (let i = 0; i < 256; ++i) {
|
|
1262
|
-
this.memStat[i] = this.memStat[256 + i] = 1;
|
|
1263
|
-
this.memLook[i] = this.memLook[256 + i] = 0;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
// DRAM content is not guaranteed to contain any particular
|
|
1267
|
-
// value on start up, so we choose values that help avoid
|
|
1268
|
-
// bugs in various games.
|
|
1269
|
-
for (let i = 0; i < this.romOffset; ++i) {
|
|
1270
|
-
if (i < 0x100) {
|
|
1271
|
-
// For Clogger.
|
|
1272
|
-
this.ramRomOs[i] = 0x00;
|
|
1273
|
-
} else {
|
|
1274
|
-
// For Eagle Empire.
|
|
1275
|
-
this.ramRomOs[i] = 0xff;
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
this.videoDisplayPage = 0;
|
|
1279
|
-
if (this.config.printerPort) this.uservia.ca2changecallback = this.config.printerPort.outputStrobe;
|
|
1280
|
-
|
|
1281
|
-
this.sysvia.reset();
|
|
1282
|
-
this.uservia.reset();
|
|
1283
|
-
this.acia.reset();
|
|
1284
|
-
this.serial.reset();
|
|
1285
|
-
this.ddNoise.spinDown();
|
|
1286
|
-
this.fdc.powerOnReset();
|
|
1287
|
-
this.adconverter.reset();
|
|
1288
|
-
|
|
1289
|
-
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1290
|
-
if (this.model.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
|
|
1291
|
-
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1250
|
+
this.setupMemoryMap();
|
|
1251
|
+
this.resetPeripherals(hard);
|
|
1292
1252
|
} else {
|
|
1293
1253
|
this.fdc.reset();
|
|
1294
1254
|
}
|
|
1255
|
+
this.resetCpuState(hard);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Override in subclasses for different memory maps.
|
|
1259
|
+
setupMemoryMap() {
|
|
1260
|
+
// On the Master, opcodes executing from 0xc000 - 0xdfff can optionally have their memory accesses
|
|
1261
|
+
// redirected to shadow RAM.
|
|
1262
|
+
this.memStatOffsetByIFetchBank = this.model.isMaster ? (1 << 0xc) | (1 << 0xd) : 0x0000;
|
|
1263
|
+
if (!this.model.isTest) {
|
|
1264
|
+
for (let i = 0; i < 128; ++i) this.memStat[i] = this.memStat[256 + i] = 1;
|
|
1265
|
+
for (let i = 128; i < 256; ++i) this.memStat[i] = this.memStat[256 + i] = 2;
|
|
1266
|
+
for (let i = 0; i < 128; ++i) this.memLook[i] = this.memLook[256 + i] = 0;
|
|
1267
|
+
for (let i = 128; i < 192; ++i) this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0x8000;
|
|
1268
|
+
for (let i = 192; i < 256; ++i) this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xc000;
|
|
1269
|
+
|
|
1270
|
+
for (let i = 0xfc; i < 0xff; ++i) this.memStat[i] = this.memStat[256 + i] = 0;
|
|
1271
|
+
} else {
|
|
1272
|
+
// Test sets everything as RAM.
|
|
1273
|
+
for (let i = 0; i < 256; ++i) {
|
|
1274
|
+
this.memStat[i] = this.memStat[256 + i] = 1;
|
|
1275
|
+
this.memLook[i] = this.memLook[256 + i] = 0;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// DRAM content is not guaranteed to contain any particular
|
|
1279
|
+
// value on start up, so we choose values that help avoid
|
|
1280
|
+
// bugs in various games.
|
|
1281
|
+
for (let i = 0; i < this.romOffset; ++i) {
|
|
1282
|
+
if (i < 0x100) {
|
|
1283
|
+
// For Clogger.
|
|
1284
|
+
this.ramRomOs[i] = 0x00;
|
|
1285
|
+
} else {
|
|
1286
|
+
// For Eagle Empire.
|
|
1287
|
+
this.ramRomOs[i] = 0xff;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
this.videoDisplayPage = 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Override in subclasses for different peripheral sets.
|
|
1294
|
+
// Only called on hard reset.
|
|
1295
|
+
resetPeripherals() {
|
|
1296
|
+
if (this.config.printerPort) this.uservia.ca2changecallback = this.config.printerPort.outputStrobe;
|
|
1297
|
+
|
|
1298
|
+
this.sysvia.reset();
|
|
1299
|
+
this.uservia.reset();
|
|
1300
|
+
this.acia.reset();
|
|
1301
|
+
this.serial.reset();
|
|
1302
|
+
this.ddNoise.spinDown();
|
|
1303
|
+
this.fdc.powerOnReset();
|
|
1304
|
+
this.adconverter.reset();
|
|
1305
|
+
|
|
1306
|
+
this.touchScreen = new TouchScreen(this.scheduler);
|
|
1307
|
+
if (this.model.hasTeletextAdaptor) this.teletextAdaptor = new TeletextAdaptor(this);
|
|
1308
|
+
if (this.econet) this.filestore = new Filestore(this, this.econet);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Universal CPU state reset. Shared by all machine types.
|
|
1312
|
+
resetCpuState(hard) {
|
|
1295
1313
|
this.tube.reset(hard);
|
|
1296
1314
|
if (hard) {
|
|
1297
1315
|
this.targetCycles = 0;
|
|
@@ -1495,3 +1513,285 @@ export class Cpu6502 extends Base6502 {
|
|
|
1495
1513
|
this.debugger.setCpu(this);
|
|
1496
1514
|
}
|
|
1497
1515
|
}
|
|
1516
|
+
|
|
1517
|
+
// Acorn Atom memory map:
|
|
1518
|
+
// 0x0000-0x7FFF RAM (32KB, or 40KB with extensions up to 0x9FFF)
|
|
1519
|
+
// 0x0A00-0x0AFF FDC (8271) -- hole in RAM region
|
|
1520
|
+
// 0x8000-0x9FFF Video RAM (shared with MC6847 VDG)
|
|
1521
|
+
// 0xA000-0xAFFF Banked ROM/RAM (Branquart, 16 x 4KB banks via latch at 0xBFFF)
|
|
1522
|
+
// 0xB000-0xB003 PPIA (8255)
|
|
1523
|
+
// 0xB400-0xB40F AtomMMC
|
|
1524
|
+
// 0xB800-0xB80F VIA (6522)
|
|
1525
|
+
// 0xBFFF Bank select latch (write-only)
|
|
1526
|
+
// 0xC000-0xEFFF ROM (BASIC, FP, DOS in 4KB blocks)
|
|
1527
|
+
// 0xF000-0xFFFF OS kernel ROM (4KB)
|
|
1528
|
+
//
|
|
1529
|
+
// Branquart banking: bits 0-3 of the latch at 0xBFFF select which 4KB
|
|
1530
|
+
// bank is visible at 0xA000-0xAFFF. Banks can contain ROM or RAM.
|
|
1531
|
+
// The latch is write-only; bit 6 is a lock bit.
|
|
1532
|
+
|
|
1533
|
+
// Storage for Branquart banks starts after the BBC ROM area.
|
|
1534
|
+
const BranquartOffset = 128 * 1024 + 17 * 16 * 16384;
|
|
1535
|
+
const BranquartBankSize = 0x1000; // 4KB per bank
|
|
1536
|
+
const NumBranquartBanks = 16;
|
|
1537
|
+
const AtomRomBlockSize = 0x1000; // 4KB
|
|
1538
|
+
|
|
1539
|
+
export class AtomCpu6502 extends Cpu6502 {
|
|
1540
|
+
constructor(model, options) {
|
|
1541
|
+
super(model, options);
|
|
1542
|
+
|
|
1543
|
+
// Atom peripherals
|
|
1544
|
+
this.atomppia = new AtomPPIA(this, this.config.keyLayout, this.scheduler);
|
|
1545
|
+
this.atommc = new AtomMMC2(this);
|
|
1546
|
+
|
|
1547
|
+
// Branquart bank selection
|
|
1548
|
+
this.branquartLatch = 0;
|
|
1549
|
+
// Track which banks are ROM (false) vs RAM (true) for write protection.
|
|
1550
|
+
this.branquartRam = new Array(NumBranquartBanks).fill(true);
|
|
1551
|
+
|
|
1552
|
+
// Expand ramRomOs to include Branquart bank storage.
|
|
1553
|
+
const expandedSize = BranquartOffset + NumBranquartBanks * BranquartBankSize;
|
|
1554
|
+
if (this.ramRomOs.length < expandedSize) {
|
|
1555
|
+
const expanded = new Uint8Array(expandedSize);
|
|
1556
|
+
expanded.set(this.ramRomOs);
|
|
1557
|
+
this.ramRomOs = expanded;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Atom runs at 1 MHz
|
|
1561
|
+
this.peripheralCyclesPerSecond = 1 * 1000 * 1000;
|
|
1562
|
+
// reset() and debugger.setCpu() are called by initialise() after loadOs().
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Select which Branquart bank is visible at 0xA000-0xAFFF by
|
|
1566
|
+
// updating the memLook table. This keeps reads on the fast path.
|
|
1567
|
+
selectBranquartBank(bank) {
|
|
1568
|
+
bank &= 0x0f;
|
|
1569
|
+
const offset = BranquartOffset + bank * BranquartBankSize - 0xa000;
|
|
1570
|
+
for (let i = 0xa0; i < 0xb0; ++i) {
|
|
1571
|
+
this.memLook[i] = this.memLook[256 + i] = offset;
|
|
1572
|
+
}
|
|
1573
|
+
// ROM banks are read-only, RAM banks are writable
|
|
1574
|
+
const stat = this.branquartRam[bank] ? 1 : 2;
|
|
1575
|
+
for (let i = 0xa0; i < 0xb0; ++i) {
|
|
1576
|
+
this.memStat[i] = this.memStat[256 + i] = stat;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
setupMemoryMap() {
|
|
1581
|
+
this.memStatOffsetByIFetchBank = 0; // no shadow RAM on Atom
|
|
1582
|
+
|
|
1583
|
+
// 0x0000-0x9FFF: RAM (including video RAM at 0x8000)
|
|
1584
|
+
for (let i = 0; i < 0xa0; ++i) {
|
|
1585
|
+
this.memStat[i] = this.memStat[256 + i] = 1;
|
|
1586
|
+
this.memLook[i] = this.memLook[256 + i] = 0;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// 0xA000-0xAFFF: Branquart banked region (set up via selectBranquartBank)
|
|
1590
|
+
this.branquartLatch = 0;
|
|
1591
|
+
this.selectBranquartBank(0);
|
|
1592
|
+
|
|
1593
|
+
// 0xB000-0xBFFF: Device I/O (PPIA, MMC, VIA, bank latch)
|
|
1594
|
+
for (let i = 0xb0; i < 0xc0; ++i) {
|
|
1595
|
+
this.memStat[i] = this.memStat[256 + i] = 0;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// 0xC000-0xEFFF: ROM (4KB blocks loaded into romOffset area)
|
|
1599
|
+
for (let i = 0xc0; i < 0xf0; ++i) {
|
|
1600
|
+
this.memStat[i] = this.memStat[256 + i] = 2;
|
|
1601
|
+
this.memLook[i] = this.memLook[256 + i] = this.romOffset - 0xa000;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// 0xF000-0xFFFF: OS kernel ROM
|
|
1605
|
+
for (let i = 0xf0; i < 0x100; ++i) {
|
|
1606
|
+
this.memStat[i] = this.memStat[256 + i] = 2;
|
|
1607
|
+
this.memLook[i] = this.memLook[256 + i] = this.osOffset - 0xf000;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// FDC at 0x0A00 (device hole in RAM, if model uses FDC)
|
|
1611
|
+
if (this.model.Fdc) {
|
|
1612
|
+
this.memStat[0x0a] = this.memStat[256 + 0x0a] = 0;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Randomise video RAM and seed bytes
|
|
1616
|
+
for (let i = 8; i < 13; ++i) this.ramRomOs[i] = (256 * Math.random()) | 0;
|
|
1617
|
+
for (let i = 0x8000; i < 0x9000; ++i) this.ramRomOs[i] = (256 * Math.random()) | 0;
|
|
1618
|
+
|
|
1619
|
+
this.videoDisplayPage = 0;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
resetPeripherals() {
|
|
1623
|
+
super.resetPeripherals();
|
|
1624
|
+
this.atomppia.reset();
|
|
1625
|
+
this.atommc.reset(true);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
readDevice(addr) {
|
|
1629
|
+
addr &= 0xffff;
|
|
1630
|
+
switch (addr & ~0x0003) {
|
|
1631
|
+
case 0x0a00:
|
|
1632
|
+
case 0x0a04:
|
|
1633
|
+
return this.fdc.read(addr);
|
|
1634
|
+
case 0xb000:
|
|
1635
|
+
case 0xb004:
|
|
1636
|
+
return this.atomppia.read(addr);
|
|
1637
|
+
case 0xb008:
|
|
1638
|
+
case 0xb00c:
|
|
1639
|
+
return 0x00;
|
|
1640
|
+
case 0xb400:
|
|
1641
|
+
case 0xb404:
|
|
1642
|
+
case 0xb408:
|
|
1643
|
+
case 0xb40c:
|
|
1644
|
+
return this.atommc.read(addr);
|
|
1645
|
+
case 0xb800:
|
|
1646
|
+
case 0xb804:
|
|
1647
|
+
case 0xb808:
|
|
1648
|
+
case 0xb80c:
|
|
1649
|
+
return this.uservia.read(addr);
|
|
1650
|
+
}
|
|
1651
|
+
return addr >>> 8; // open bus
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
writeDevice(addr, b) {
|
|
1655
|
+
addr &= 0xffff;
|
|
1656
|
+
b |= 0;
|
|
1657
|
+
switch (addr & ~0x0003) {
|
|
1658
|
+
case 0x0a00:
|
|
1659
|
+
case 0x0a04:
|
|
1660
|
+
return this.fdc.write(addr, b);
|
|
1661
|
+
case 0xb000:
|
|
1662
|
+
case 0xb004:
|
|
1663
|
+
case 0xb008:
|
|
1664
|
+
case 0xb00c:
|
|
1665
|
+
return this.atomppia.write(addr, b);
|
|
1666
|
+
case 0xb400:
|
|
1667
|
+
case 0xb404:
|
|
1668
|
+
case 0xb408:
|
|
1669
|
+
case 0xb40c:
|
|
1670
|
+
return this.atommc.write(addr, b);
|
|
1671
|
+
case 0xb800:
|
|
1672
|
+
case 0xb804:
|
|
1673
|
+
case 0xb808:
|
|
1674
|
+
case 0xb80c:
|
|
1675
|
+
return this.uservia.write(addr, b);
|
|
1676
|
+
case 0xbffc:
|
|
1677
|
+
if (addr === 0xbfff) {
|
|
1678
|
+
this.branquartLatch = b & 0xff;
|
|
1679
|
+
this.selectBranquartBank(b & 0x0f);
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
writemem(addr, b) {
|
|
1686
|
+
addr &= 0xffff;
|
|
1687
|
+
b |= 0;
|
|
1688
|
+
if (this._debugWrite) this._debugWrite(addr, b);
|
|
1689
|
+
const statOffset = this.memStatOffset + (addr >>> 8);
|
|
1690
|
+
const stat = this.memStat[statOffset];
|
|
1691
|
+
if (stat === 1) {
|
|
1692
|
+
// Notify VDG of CPU address bus activity for snow effect
|
|
1693
|
+
if (this.video.video6847) {
|
|
1694
|
+
this.video.video6847.cpuAddrAccess(addr);
|
|
1695
|
+
}
|
|
1696
|
+
const offset = this.memLook[statOffset];
|
|
1697
|
+
this.ramRomOs[offset + addr] = b;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (stat === 2) return; // ROM: write ignored
|
|
1701
|
+
// Device page
|
|
1702
|
+
this.writeDevice(addr, b);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Atom ROMs are 4KB, not 16KB like BBC.
|
|
1706
|
+
// OS layout: osOffset holds 4KB kernel at 0xF000.
|
|
1707
|
+
// Extra ROMs (BASIC, FP, DOS) are 4KB blocks at romOffset+0..romOffset+0x5000
|
|
1708
|
+
// mapped at 0xC000, 0xD000, 0xE000 etc.
|
|
1709
|
+
async loadOs(os) {
|
|
1710
|
+
const extraRoms = Array.prototype.slice.call(arguments, 1).concat(this.config.extraRoms);
|
|
1711
|
+
const bankRoms = this.model.banks || [];
|
|
1712
|
+
os = "roms/" + os;
|
|
1713
|
+
console.log(`Loading Atom OS from ${os}`);
|
|
1714
|
+
const data = await utils.loadData(os);
|
|
1715
|
+
const len = data.length;
|
|
1716
|
+
|
|
1717
|
+
if (len < AtomRomBlockSize || len % AtomRomBlockSize !== 0) {
|
|
1718
|
+
throw new Error(`Broken Atom ROM file (length=${len})`);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Clear the OS area and load the kernel at osOffset (mapped to 0xF000)
|
|
1722
|
+
this.ramRomOs.fill(0x00, this.osOffset, this.osOffset + 0x4000);
|
|
1723
|
+
this.ramRomOs.set(data.subarray(0, AtomRomBlockSize), this.osOffset);
|
|
1724
|
+
|
|
1725
|
+
// Load extra ROMs (BASIC, FP, DOS) into 4KB blocks.
|
|
1726
|
+
// romIndex counts down from 5: slot 4 maps to 0xE000 (romOffset+0x4000),
|
|
1727
|
+
// slot 3 to 0xD000 (romOffset+0x3000), slot 2 to 0xC000 (romOffset+0x2000).
|
|
1728
|
+
let romIndex = 5;
|
|
1729
|
+
const awaiting = [];
|
|
1730
|
+
for (const rom of extraRoms) {
|
|
1731
|
+
romIndex--;
|
|
1732
|
+
if (romIndex < 2)
|
|
1733
|
+
throw new Error("Too many extra ROMs for Atom (max 3 addressable slots at 0xC000-0xEFFF)");
|
|
1734
|
+
if (rom !== "") {
|
|
1735
|
+
awaiting.push(this.loadRom(rom, this.romOffset + romIndex * AtomRomBlockSize));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Load Branquart bank ROMs (max 16 banks)
|
|
1740
|
+
const numBanks = Math.min(bankRoms.length, NumBranquartBanks);
|
|
1741
|
+
for (let bankIndex = 0; bankIndex < numBanks; bankIndex++) {
|
|
1742
|
+
if (bankRoms[bankIndex] !== "") {
|
|
1743
|
+
awaiting.push(this.loadRom(bankRoms[bankIndex], BranquartOffset + bankIndex * BranquartBankSize));
|
|
1744
|
+
this.branquartRam[bankIndex] = false; // mark as ROM
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
return await Promise.all(awaiting);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Override loadRom to accept 4KB ROMs
|
|
1752
|
+
async loadRom(name, offset) {
|
|
1753
|
+
const data = await utils.loadData("roms/" + name);
|
|
1754
|
+
const len = data.length;
|
|
1755
|
+
if (len !== 16384 && len !== 8192 && len !== 4096) {
|
|
1756
|
+
throw new Error(`Broken ROM file ${name} (length=${len})`);
|
|
1757
|
+
}
|
|
1758
|
+
this.ramRomOs.set(data, offset);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Atom key layout goes through PPIA, not SysVia
|
|
1762
|
+
updateKeyLayout() {
|
|
1763
|
+
this.atomppia.setKeyLayout(this.config.keyLayout);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
snapshotState(options) {
|
|
1767
|
+
const state = super.snapshotState(options);
|
|
1768
|
+
// Atom-specific state
|
|
1769
|
+
state.branquartLatch = this.branquartLatch;
|
|
1770
|
+
state.branquartRam = [...this.branquartRam];
|
|
1771
|
+
// Save Branquart bank contents
|
|
1772
|
+
state.branquartBanks = this.ramRomOs.slice(
|
|
1773
|
+
BranquartOffset,
|
|
1774
|
+
BranquartOffset + NumBranquartBanks * BranquartBankSize,
|
|
1775
|
+
);
|
|
1776
|
+
state.atomppia = this.atomppia.snapshotState();
|
|
1777
|
+
return state;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// No-op: the Atom doesn't have BBC sideways ROM banking.
|
|
1781
|
+
// This prevents super.restoreState() from corrupting the Atom memory map.
|
|
1782
|
+
romSelect() {}
|
|
1783
|
+
|
|
1784
|
+
restoreState(state) {
|
|
1785
|
+
super.restoreState(state);
|
|
1786
|
+
|
|
1787
|
+
// Restore Atom-specific state after the parent has handled
|
|
1788
|
+
// CPU registers, memory, cycle tracking, and sub-components.
|
|
1789
|
+
if (state.branquartRam) this.branquartRam = [...state.branquartRam];
|
|
1790
|
+
if (state.branquartBanks) {
|
|
1791
|
+
this.ramRomOs.set(state.branquartBanks, BranquartOffset);
|
|
1792
|
+
}
|
|
1793
|
+
this.branquartLatch = state.branquartLatch || 0;
|
|
1794
|
+
this.selectBranquartBank(this.branquartLatch & 0x0f);
|
|
1795
|
+
if (state.atomppia) this.atomppia.restoreState(state.atomppia);
|
|
1796
|
+
}
|
|
1797
|
+
}
|