Compare commits

..

No commits in common. "c59c1116258b29e8d7031fc707168fc74d42339d" and "6e49066162a449c8753515cc88d1acdab445ac12" have entirely different histories.

31 changed files with 7400 additions and 758 deletions

View File

@ -11,9 +11,7 @@ jobs:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v2
with:
submodules: "true"
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
@ -22,6 +20,6 @@ jobs:
run: |
npm ci
npm run build --if-present
# npm test
npm test
env:
CI: true

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "submodules/cpu6502"]
path = submodules/cpu6502
url = https://github.com/whscullin/cpu6502

View File

@ -9,8 +9,6 @@ Things are still a little rough around the edges right now, hopefully I will hav
First
```sh
git submodule init
git submodule update
npm install
```

View File

@ -12,7 +12,7 @@
import MicroModal from 'micromodal';
import Apple1IO from './apple1io';
import { CPU6502, word, byte } from '@whscullin/cpu6502';
import CPU6502 from './cpu6502';
import Prefs from './prefs';
import RAM from './ram';
import { TextPage } from './canvas1';
@ -25,6 +25,7 @@ import Krusader from './roms/krusader';
import ACI from './cards/aci';
import { mapKeyEvent, KeyBoard } from './ui/keyboard';
import { address, byte } from './types';
// eslint-disable-next-line prefer-const
let DEBUG = false;
@ -196,7 +197,7 @@ export function setTurboTape(val: boolean) {
turbotape = val;
}
function run(pc?: word) {
function run(pc?: address) {
if (runTimer) {
clearInterval(runTimer);
}
@ -363,7 +364,7 @@ export function doLoadText() {
lines.forEach(function (line) {
const parts = line.split(': ');
if (parts.length === 2) {
let addr: word = 0;
let addr: address = 0;
if (parts[0].length > 0) {
addr = parseInt(parts[0], 16);
}

View File

@ -10,7 +10,7 @@
*/
import { TextPage } from './canvas1';
import type { byte } from '@whscullin/cpu6502';
import type { byte } from './types';
const LOC = {
KBD: 0x10,

View File

@ -1,4 +1,4 @@
import { memory } from '@whscullin/cpu6502';
import { memory } from './types';
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

View File

@ -10,7 +10,7 @@
*/
import { charset } from './roms/apple1char';
import type { byte } from '@whscullin/cpu6502';
import type { byte } from './types';
/*
0: A9 9 AA 20 EF FF E8 8A 4C 2 0

View File

@ -1,40 +1,28 @@
import { CPU6502, byte } from '@whscullin/cpu6502';
import CPU6502 from '../cpu6502';
import { debug } from '../util';
import { byte } from '../types';
// prettier-ignore
const rom = [
0xA9,0xAA,0x20,0xEF,0xFF,0xA9,0x8D,0x20,
0xEF,0xFF,0xA0,0xFF,0xC8,0xAD,0x11,0xD0,
0x10,0xFB,0xAD,0x10,0xD0,0x99,0x00,0x02,
0x20,0xEF,0xFF,0xC9,0x9B,0xF0,0xE1,0xC9,
0x8D,0xD0,0xE9,0xA2,0xFF,0xA9,0x00,0x85,
0x24,0x85,0x25,0x85,0x26,0x85,0x27,0xE8,
0xBD,0x00,0x02,0xC9,0xD2,0xF0,0x56,0xC9,
0xD7,0xF0,0x35,0xC9,0xAE,0xF0,0x27,0xC9,
0x8D,0xF0,0x20,0xC9,0xA0,0xF0,0xE8,0x49,
0xB0,0xC9,0x0A,0x90,0x06,0x69,0x88,0xC9,
0xFA,0x90,0xAD,0x0A,0x0A,0x0A,0x0A,0xA0,
0x04,0x0A,0x26,0x24,0x26,0x25,0x88,0xD0,
0xF8,0xF0,0xCC,0x4C,0x1A,0xFF,0xA5,0x24,
0x85,0x26,0xA5,0x25,0x85,0x27,0xB0,0xBF,
0xA9,0x40,0x20,0xCC,0xC1,0x88,0xA2,0x00,
0xA1,0x26,0xA2,0x10,0x0A,0x20,0xDB,0xC1,
0xD0,0xFA,0x20,0xF1,0xC1,0xA0,0x1E,0x90,
0xEC,0xA6,0x28,0xB0,0x98,0x20,0xBC,0xC1,
0xA9,0x16,0x20,0xCC,0xC1,0x20,0xBC,0xC1,
0xA0,0x1F,0x20,0xBF,0xC1,0xB0,0xF9,0x20,
0xBF,0xC1,0xA0,0x3A,0xA2,0x08,0x48,0x20,
0xBC,0xC1,0x68,0x2A,0xA0,0x39,0xCA,0xD0,
0xF5,0x81,0x26,0x20,0xF1,0xC1,0xA0,0x35,
0x90,0xEA,0xB0,0xCD,0x20,0xBF,0xC1,0x88,
0xAD,0x81,0xC0,0xC5,0x29,0xF0,0xF8,0x85,
0x29,0xC0,0x80,0x60,0x86,0x28,0xA0,0x42,
0x20,0xE0,0xC1,0xD0,0xF9,0x69,0xFE,0xB0,
0xF5,0xA0,0x1E,0x20,0xE0,0xC1,0xA0,0x2C,
0x88,0xD0,0xFD,0x90,0x05,0xA0,0x2F,0x88,
0xD0,0xFD,0xBC,0x00,0xC0,0xA0,0x29,0xCA,
0x60,0xA5,0x26,0xC5,0x24,0xA5,0x27,0xE5,
0x25,0xE6,0x26,0xD0,0x02,0xE6,0x27,0x60
0xa9, 0xaa, 0x20, 0xef, 0xff, 0xa9, 0x8d, 0x20, 0xef, 0xff, 0xa0, 0xff, 0xc8,
0xad, 0x11, 0xd0, 0x10, 0xfb, 0xad, 0x10, 0xd0, 0x99, 0x00, 0x02, 0x20, 0xef,
0xff, 0xc9, 0x9b, 0xf0, 0xe1, 0xc9, 0x8d, 0xd0, 0xe9, 0xa2, 0xff, 0xa9, 0x00,
0x85, 0x24, 0x85, 0x25, 0x85, 0x26, 0x85, 0x27, 0xe8, 0xbd, 0x00, 0x02, 0xc9,
0xd2, 0xf0, 0x56, 0xc9, 0xd7, 0xf0, 0x35, 0xc9, 0xae, 0xf0, 0x27, 0xc9, 0x8d,
0xf0, 0x20, 0xc9, 0xa0, 0xf0, 0xe8, 0x49, 0xb0, 0xc9, 0x0a, 0x90, 0x06, 0x69,
0x88, 0xc9, 0xfa, 0x90, 0xad, 0x0a, 0x0a, 0x0a, 0x0a, 0xa0, 0x04, 0x0a, 0x26,
0x24, 0x26, 0x25, 0x88, 0xd0, 0xf8, 0xf0, 0xcc, 0x4c, 0x1a, 0xff, 0xa5, 0x24,
0x85, 0x26, 0xa5, 0x25, 0x85, 0x27, 0xb0, 0xbf, 0xa9, 0x40, 0x20, 0xcc, 0xc1,
0x88, 0xa2, 0x00, 0xa1, 0x26, 0xa2, 0x10, 0x0a, 0x20, 0xdb, 0xc1, 0xd0, 0xfa,
0x20, 0xf1, 0xc1, 0xa0, 0x1e, 0x90, 0xec, 0xa6, 0x28, 0xb0, 0x98, 0x20, 0xbc,
0xc1, 0xa9, 0x16, 0x20, 0xcc, 0xc1, 0x20, 0xbc, 0xc1, 0xa0, 0x1f, 0x20, 0xbf,
0xc1, 0xb0, 0xf9, 0x20, 0xbf, 0xc1, 0xa0, 0x3a, 0xa2, 0x08, 0x48, 0x20, 0xbc,
0xc1, 0x68, 0x2a, 0xa0, 0x39, 0xca, 0xd0, 0xf5, 0x81, 0x26, 0x20, 0xf1, 0xc1,
0xa0, 0x35, 0x90, 0xea, 0xb0, 0xcd, 0x20, 0xbf, 0xc1, 0x88, 0xad, 0x81, 0xc0,
0xc5, 0x29, 0xf0, 0xf8, 0x85, 0x29, 0xc0, 0x80, 0x60, 0x86, 0x28, 0xa0, 0x42,
0x20, 0xe0, 0xc1, 0xd0, 0xf9, 0x69, 0xfe, 0xb0, 0xf5, 0xa0, 0x1e, 0x20, 0xe0,
0xc1, 0xa0, 0x2c, 0x88, 0xd0, 0xfd, 0x90, 0x05, 0xa0, 0x2f, 0x88, 0xd0, 0xfd,
0xbc, 0x00, 0xc0, 0xa0, 0x29, 0xca, 0x60, 0xa5, 0x26, 0xc5, 0x24, 0xa5, 0x27,
0xe5, 0x25, 0xe6, 0x26, 0xd0, 0x02, 0xe6, 0x27, 0x60,
] as const;
export interface ACICallback {

3265
js/cpu6502.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
import { base64_decode, base64_encode } from './base64';
import { allocMemPages } from './util';
import type { byte, memory } from '@whscullin/cpu6502';
import type { byte } from './types';
export interface RAMState {
start: byte;
@ -20,7 +20,7 @@ export interface RAMState {
}
export default class RAM {
mem: memory;
mem: Uint8Array | byte[];
constructor(
private start_page: byte,

View File

@ -1,4 +1,4 @@
import { byte } from '@whscullin/cpu6502';
import { byte } from 'js/types';
export default class Basic {
ram = [

View File

@ -1,4 +1,4 @@
import { byte } from '@whscullin/cpu6502';
import { byte } from 'js/types';
export default class Bios {
rom = [

View File

@ -1,4 +1,4 @@
import { byte } from '@whscullin/cpu6502';
import { byte } from 'js/types';
export default class EnhancedBasic {
rom = [

View File

@ -1,4 +1,4 @@
import { byte } from '@whscullin/cpu6502';
import { byte } from 'js/types';
export default class Krusader {
rom = [

View File

@ -9,9 +9,9 @@
* implied warranty.
*/
import type { word } from '@whscullin/cpu6502';
import type { address } from './types';
export const SYMBOLS: Record<word, string> = {
export const SYMBOLS: Record<address, string> = {
0xd010: 'KBD',
0xd011: 'KBDCR',
0xd012: 'DSP',

43
js/types.ts Normal file
View File

@ -0,0 +1,43 @@
/* Copyright 2023 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
export type byte = number;
export type word = number;
export type address = word;
export type memory = Uint8Array | byte[];
export interface Memory {
/** Read a byte. */
read(page: byte, offset: byte): byte;
/** Write a byte. */
write(page: byte, offset: byte, value: byte): void;
}
/** A mapped region of memory. */
export interface MemoryPages extends Memory {
/** Start page. */
start(): byte;
/** End page, inclusive. */
end(): byte;
}
/**
* Extracts the members of a constant array as a type. Used as:
*
* @example
* const SOME_VALUES = ['a', 'b', 1, 2] as const;
* type SomeValues = MemberOf<typeof SOME_VALUES>; // 'a' | 'b' | 1 | 2
*/
export type MemberOf<T extends ReadonlyArray<unknown>> =
T extends ReadonlyArray<infer E> ? E : never;

View File

@ -9,10 +9,11 @@
* implied warranty.
*/
import { CPU6502, byte } from '@whscullin/cpu6502';
import CPU6502 from 'js/cpu6502';
import { debug, toHex } from '../util';
import Apple1IO from 'js/apple1io';
import { TextPage } from 'js/canvas1';
import { byte } from 'js/types';
// keycode: [plain, cntl, shift]

View File

@ -9,12 +9,12 @@
* implied warranty.
*/
import { byte, word } from '@whscullin/cpu6502';
import { byte, word } from './types';
const hex_digits = '0123456789ABCDEF';
const bin_digits = '01';
export function allocMem(size: word): Uint8Array | byte[] {
export function allocMem(size: word) {
let result;
if (window.Uint8Array) {
result = new Uint8Array(size);

1229
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,6 @@
"webpack-dev-server": "^4.6.0"
},
"dependencies": {
"@whscullin/cpu6502": "file:submodules/cpu6502",
"micromodal": "^0.4.9"
}
}

@ -1 +0,0 @@
Subproject commit 4e1031d1bfeaa930b95292269e02d5c31a3440a8

48
test/cpu.spec.ts Normal file
View File

@ -0,0 +1,48 @@
import CPU6502, { FLAVOR_ROCKWELL_65C02 } from '../js/cpu6502';
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
import Test6502 from './roms/6502test';
import Test65C02 from './roms/65C02test';
import { toHex } from '../js/util';
describe('CPU', function () {
let cpu: CPU6502;
let lastPC = 0;
let done = false;
function traceCB() {
const pc = cpu.getPC();
done = lastPC === pc;
lastPC = pc;
}
describe('6502', function () {
it('completes the test ROM', function () {
cpu = new CPU6502();
const test = new Test6502();
cpu.addPageHandler(test);
cpu.setPC(0x400);
do {
cpu.stepCyclesDebug(1000, traceCB);
} while (!done);
expect(toHex(lastPC)).toEqual(toHex(0x3469));
});
});
describe('65C02', function () {
it('completes the test ROM', function () {
cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 });
const test = new Test65C02();
cpu.addPageHandler(test);
cpu.setPC(0x400);
do {
cpu.stepCyclesDebug(1000, traceCB);
} while (!done);
expect(toHex(lastPC)).toEqual(toHex(0x24f1));
});
});
});

3254
test/cpu6502.spec.ts Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

29
test/roms/6502test.ts Normal file
View File

@ -0,0 +1,29 @@
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
import fs from 'fs';
import path from 'path';
import { MemoryPages, byte } from '../../js/types';
export default class Test6502 implements MemoryPages {
private data: Buffer;
constructor() {
this.data = fs.readFileSync(path.join(__dirname, '6502_functional_test.bin'));
}
start = () => {
return 0x00;
};
end = () => {
return 0xff;
};
read = (page: byte, off: byte) => {
return this.data[page << 8 | off];
};
write = (page: byte, off: byte, val: byte) => {
this.data[page << 8 | off] = val;
};
}

Binary file not shown.

29
test/roms/65C02test.ts Normal file
View File

@ -0,0 +1,29 @@
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
import fs from 'fs';
import path from 'path';
import { MemoryPages, byte } from '../../js/types';
export default class Test65C02 implements MemoryPages {
private data: Buffer;
constructor() {
this.data = fs.readFileSync(path.join(__dirname, '65C02_extended_opcodes_test.bin'));
}
start = () => {
return 0x00;
};
end = () => {
return 0xff;
};
read = (page: byte, off: byte) => {
return this.data[page << 8 | off];
};
write = (page: byte, off: byte, val: byte) => {
this.data[page << 8 | off] = val;
};
}

6
test/util/asserts.ts Normal file
View File

@ -0,0 +1,6 @@
import { byte } from "../../js/types";
export const assertByte = (b: byte) => {
expect(b <= 0xff).toEqual(true);
expect(b >= 0x00).toEqual(true);
};

56
test/util/bios.ts Normal file
View File

@ -0,0 +1,56 @@
import { MemoryPages, byte } from "../../js/types";
import { assertByte } from "./asserts";
export class Program implements MemoryPages {
private data: Buffer;
constructor(private page: byte, code: byte[]) {
this.data = Buffer.from(code);
}
start() {
return this.page;
}
end() {
return this.page;
}
read(page: byte, off: byte) {
assertByte(page);
assertByte(off);
return this.data[off];
}
write(_page: byte, _off: byte, _val: byte) {
// do nothing
}
}
export const bios = new Program(
0xff,
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x45, 0x4c, 0x4c,
0x4f, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
0x00, 0x04, 0x00, 0xff,
]
);

34
test/util/cpu.ts Normal file
View File

@ -0,0 +1,34 @@
import { flags, CpuState } from "js/cpu6502";
import { byte } from "js/types";
import { toHex } from "js/util";
export const dumpStatusRegister = (sr: byte) =>
[
sr & flags.N ? "N" : "-",
sr & flags.V ? "V" : "-",
sr & flags.X ? "X" : "-",
sr & flags.B ? "B" : "-",
sr & flags.D ? "D" : "-",
sr & flags.I ? "I" : "-",
sr & flags.Z ? "Z" : "-",
sr & flags.C ? "C" : "-",
].join("");
const detail = !!process.env.JEST_DETAIL;
export function toReadableState(state: CpuState) {
if (detail) {
const { pc, sp, a, x, y, s } = state;
return {
pc: toHex(pc, 4),
sp: toHex(sp),
a: toHex(a),
x: toHex(x),
y: toHex(y),
s: dumpStatusRegister(s),
};
} else {
return state;
}
}

60
test/util/memory.ts Normal file
View File

@ -0,0 +1,60 @@
import { MemoryPages, byte, word } from "js/types";
import { assertByte } from "./asserts";
export type Log = [address: word, value: byte, types: "read" | "write"];
export class TestMemory implements MemoryPages {
private data: Buffer;
private logging: boolean = false;
private log: Log[] = [];
constructor(private size: number) {
this.data = Buffer.alloc(size << 8);
}
start() {
return 0;
}
end() {
return this.size - 1;
}
read(page: byte, off: byte) {
assertByte(page);
assertByte(off);
const val = this.data[(page << 8) | off];
if (this.logging) {
this.log.push([(page << 8) | off, val, "read"]);
}
return val;
}
write(page: byte, off: byte, val: byte) {
assertByte(page);
assertByte(off);
assertByte(val);
if (this.logging) {
this.log.push([(page << 8) | off, val, "write"]);
}
this.data[(page << 8) | off] = val;
}
reset() {
this.log = [];
}
logStart() {
this.log = [];
this.logging = true;
}
logStop() {
this.logging = false;
}
getLog() {
return this.log;
}
}