mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Improve debug performance, abstract debugger. (#61)
Stop stringifying opcodes during runtime and only do so upon inspection. Moves all the debugging logic to a common place to allow building an interface.
This commit is contained in:
parent
5f0d07623d
commit
bb09a1ec33
@ -41,6 +41,7 @@
|
||||
],
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": ["error"],
|
||||
"no-dupe-class-members": "off",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
|
37
js/apple2.ts
37
js/apple2.ts
@ -20,9 +20,10 @@ import { Apple2IOState } from './apple2io';
|
||||
import CPU6502, { CpuState } from './cpu6502';
|
||||
import MMU, { MMUState } from './mmu';
|
||||
import RAM, { RAMState } from './ram';
|
||||
import { debug } from './util';
|
||||
|
||||
import SYMBOLS from './symbols';
|
||||
import Debugger, { DebuggerContainer } from './debugger';
|
||||
|
||||
import { Restorable, rom } from './types';
|
||||
import { processGamepad } from './ui/gamepad';
|
||||
|
||||
@ -44,13 +45,10 @@ interface State {
|
||||
ram?: RAMState[],
|
||||
}
|
||||
|
||||
export class Apple2 implements Restorable<State> {
|
||||
export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
private paused = false;
|
||||
|
||||
private DEBUG = false;
|
||||
private TRACE = false;
|
||||
private MAX_TRACE = 256;
|
||||
private trace: string[] = [];
|
||||
private theDebugger?: Debugger;
|
||||
|
||||
private runTimer: number | null = null;
|
||||
private runAnimationFrame: number | null = null;
|
||||
@ -120,6 +118,9 @@ export class Apple2 implements Restorable<State> {
|
||||
return; // already running
|
||||
}
|
||||
|
||||
this.theDebugger = new Debugger(this);
|
||||
this.theDebugger.addSymbols(SYMBOLS);
|
||||
|
||||
const interval = 30;
|
||||
|
||||
let now, last = Date.now();
|
||||
@ -134,19 +135,8 @@ export class Apple2 implements Restorable<State> {
|
||||
step = stepMax;
|
||||
}
|
||||
|
||||
if (this.DEBUG) {
|
||||
this.cpu.stepCyclesDebug(this.TRACE ? 1 : step, () => {
|
||||
const line = this.cpu.dumpRegisters() + ' ' +
|
||||
this.cpu.dumpPC(undefined, SYMBOLS);
|
||||
if (this.TRACE) {
|
||||
debug(line);
|
||||
} else {
|
||||
this.trace.push(line);
|
||||
if (this.trace.length > this.MAX_TRACE) {
|
||||
this.trace.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.theDebugger) {
|
||||
this.theDebugger.stepCycles(step);
|
||||
} else {
|
||||
this.cpu.stepCycles(step);
|
||||
}
|
||||
@ -234,7 +224,16 @@ export class Apple2 implements Restorable<State> {
|
||||
return this.io;
|
||||
}
|
||||
|
||||
getMMU() {
|
||||
return this.mmu;
|
||||
}
|
||||
|
||||
|
||||
getVideoModes() {
|
||||
return this.vm;
|
||||
}
|
||||
|
||||
getDebugger() {
|
||||
return this.theDebugger;
|
||||
}
|
||||
}
|
||||
|
275
js/cpu6502.ts
275
js/cpu6502.ts
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
* Copyright 2010-2021 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
|
||||
@ -13,8 +13,6 @@
|
||||
import { Memory, MemoryPages, byte, word } from './types';
|
||||
import { debug, toHex } from './util';
|
||||
|
||||
type symbols = { [key: number]: string };
|
||||
|
||||
export interface CpuOptions {
|
||||
'65C02'?: boolean;
|
||||
}
|
||||
@ -29,7 +27,7 @@ export interface CpuState {
|
||||
cycles: number
|
||||
}
|
||||
|
||||
type Mode =
|
||||
export type Mode =
|
||||
'accumulator' | // A (Accumulator)
|
||||
'implied' | // Implied
|
||||
'immediate' | // # Immediate
|
||||
@ -47,10 +45,10 @@ type Mode =
|
||||
'absoluteXIndirect' | // (a, X),
|
||||
'zeroPage_relative'; // zp, Relative
|
||||
|
||||
type Modes = Record<Mode, number>;
|
||||
export type Modes = Record<Mode, number>;
|
||||
|
||||
/** Addressing mode name to instruction size mapping. */
|
||||
const sizes: Modes = {
|
||||
export const sizes: Modes = {
|
||||
accumulator: 1,
|
||||
implied: 1,
|
||||
immediate: 2,
|
||||
@ -74,10 +72,20 @@ const sizes: Modes = {
|
||||
};
|
||||
|
||||
/** Status register flag numbers. */
|
||||
type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01;
|
||||
export type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01;
|
||||
|
||||
export type DebugInfo = {
|
||||
pc: word,
|
||||
ar: byte,
|
||||
xr: byte,
|
||||
yr: byte,
|
||||
sr: byte,
|
||||
sp: byte,
|
||||
cmd: byte[],
|
||||
};
|
||||
|
||||
/** Flags to status byte mask. */
|
||||
const flags: { [key: string]: flag } = {
|
||||
export const flags: { [key: string]: flag } = {
|
||||
N: 0x80, // Negative
|
||||
V: 0x40, // oVerflow
|
||||
B: 0x10, // Break
|
||||
@ -141,12 +149,12 @@ export default class CPU6502 {
|
||||
private readonly is65C02: boolean;
|
||||
|
||||
/* Registers */
|
||||
private pc = 0; // Program Counter
|
||||
private sr = 0x20; // Process Status Register
|
||||
private ar = 0; // Accumulator
|
||||
private xr = 0; // X Register
|
||||
private yr = 0; // Y Register
|
||||
private sp = 0xff; // Stack Pointer
|
||||
private pc: word = 0; // Program Counter
|
||||
private sr: byte = 0x20; // Process Status Register
|
||||
private ar: byte = 0; // Accumulator
|
||||
private xr: byte = 0; // X Register
|
||||
private yr: byte = 0; // Y Register
|
||||
private sp: byte = 0xff; // Stack Pointer
|
||||
|
||||
private memPages: Memory[] = new Array(0x100);
|
||||
private resetHandlers: ResettablePageHandler[] = [];
|
||||
@ -294,10 +302,6 @@ export default class CPU6502 {
|
||||
return this.readByte(addr) | (this.readByte(addr + 1) << 8);
|
||||
}
|
||||
|
||||
private readWordDebug(addr: word): word {
|
||||
return this.readByteDebug(addr) | (this.readByteDebug(addr + 1) << 8);
|
||||
}
|
||||
|
||||
private readWordPC(): word {
|
||||
return this.readBytePC() | (this.readBytePC() << 8);
|
||||
}
|
||||
@ -991,105 +995,24 @@ export default class CPU6502 {
|
||||
return unk;
|
||||
}
|
||||
|
||||
private dumpArgs(addr: word, m: Mode, symbols: symbols) {
|
||||
function toHexOrSymbol(v: word, n?: number) {
|
||||
if (symbols && symbols[v]) {
|
||||
return symbols[v];
|
||||
} else {
|
||||
return '$' + toHex(v, n);
|
||||
}
|
||||
}
|
||||
|
||||
let off, val;
|
||||
let result = '';
|
||||
switch (m) {
|
||||
case 'implied':
|
||||
break;
|
||||
case 'immediate':
|
||||
result = '#' + toHexOrSymbol(this.readByteDebug(addr));
|
||||
break;
|
||||
case 'absolute':
|
||||
result = '' + toHexOrSymbol(this.readWordDebug(addr), 4);
|
||||
break;
|
||||
case 'zeroPage':
|
||||
result = '' + toHexOrSymbol(this.readByteDebug(addr));
|
||||
break;
|
||||
case 'relative':
|
||||
{
|
||||
let off = this.readByteDebug(addr);
|
||||
if (off > 127) {
|
||||
off -= 256;
|
||||
}
|
||||
addr += off + 1;
|
||||
result = '' + toHexOrSymbol(addr, 4) + ' (' + off + ')';
|
||||
}
|
||||
break;
|
||||
case 'absoluteX':
|
||||
result = '' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',X';
|
||||
break;
|
||||
case 'absoluteY':
|
||||
result = '' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',Y';
|
||||
break;
|
||||
case 'zeroPageX':
|
||||
result = '' + toHexOrSymbol(this.readByteDebug(addr)) + ',X';
|
||||
break;
|
||||
case 'zeroPageY':
|
||||
result = '' + toHexOrSymbol(this.readByteDebug(addr)) + ',Y';
|
||||
break;
|
||||
case 'absoluteIndirect':
|
||||
result = '(' + toHexOrSymbol(this.readWordDebug(addr), 4) + ')';
|
||||
break;
|
||||
case 'zeroPageXIndirect':
|
||||
result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + ',X)';
|
||||
break;
|
||||
case 'zeroPageIndirectY':
|
||||
result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + '),Y';
|
||||
break;
|
||||
case 'accumulator':
|
||||
result = 'A';
|
||||
break;
|
||||
case 'zeroPageIndirect':
|
||||
result = '(' + toHexOrSymbol(this.readByteDebug(addr)) + ')';
|
||||
break;
|
||||
case 'absoluteXIndirect':
|
||||
result = '(' + toHexOrSymbol(this.readWordDebug(addr), 4) + ',X)';
|
||||
break;
|
||||
case 'zeroPage_relative':
|
||||
val = this.readByteDebug(addr);
|
||||
off = this.readByteDebug(addr + 1);
|
||||
if (off > 127) {
|
||||
off -= 256;
|
||||
}
|
||||
addr += off + 2;
|
||||
result = '' + toHexOrSymbol(val) + ',' + toHexOrSymbol(addr, 4) + ' (' + off + ')';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public step(cb: callback) {
|
||||
public step(cb?: callback) {
|
||||
this.sync = true;
|
||||
const op = this.opary[this.readBytePC()];
|
||||
this.sync = false;
|
||||
op.op(op.modeFn);
|
||||
|
||||
if (cb) {
|
||||
cb(this);
|
||||
}
|
||||
cb?.(this);
|
||||
}
|
||||
|
||||
public stepDebug(n: number, cb: callback) {
|
||||
public stepN(n: number, cb?: callback) {
|
||||
for (let idx = 0; idx < n; idx++) {
|
||||
this.sync = true;
|
||||
const op = this.opary[this.readBytePC()];
|
||||
this.sync = false;
|
||||
op.op(op.modeFn);
|
||||
|
||||
if (cb) {
|
||||
cb(this);
|
||||
}
|
||||
cb?.(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1104,7 +1027,7 @@ export default class CPU6502 {
|
||||
}
|
||||
}
|
||||
|
||||
public stepCyclesDebug(c: number, cb: callback): void {
|
||||
public stepCyclesDebug(c: number, cb?: callback): void {
|
||||
const end = this.cycles + c;
|
||||
|
||||
while (this.cycles < end) {
|
||||
@ -1113,9 +1036,7 @@ export default class CPU6502 {
|
||||
this.sync = false;
|
||||
op.op(op.modeFn);
|
||||
|
||||
if (cb) {
|
||||
cb(this);
|
||||
}
|
||||
cb?.(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1173,83 +1094,26 @@ export default class CPU6502 {
|
||||
this.pc = pc;
|
||||
}
|
||||
|
||||
public dumpPC(pc: word | undefined, symbols: symbols) {
|
||||
if (pc === undefined) {
|
||||
pc = this.pc;
|
||||
}
|
||||
const b = this.readByteDebug(pc),
|
||||
op = this.opary[b],
|
||||
size = sizes[op.mode];
|
||||
let result = toHex(pc, 4) + '- ';
|
||||
|
||||
if (symbols) {
|
||||
if (symbols[pc]) {
|
||||
result += symbols[pc] +
|
||||
' '.substring(symbols[pc].length);
|
||||
} else {
|
||||
result += ' ';
|
||||
}
|
||||
public getDebugInfo(): DebugInfo {
|
||||
const b = this.readByteDebug(this.pc);
|
||||
const op = this.opary[b];
|
||||
const size = sizes[op.mode];
|
||||
const cmd = new Array(size);
|
||||
cmd[0] = b;
|
||||
for (let idx = 1; idx < size; idx++) {
|
||||
cmd[idx] = this.readByteDebug(this.pc + idx);
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < 4; idx++) {
|
||||
if (idx < size) {
|
||||
result += toHex(this.readByteDebug(pc + idx)) + ' ';
|
||||
} else {
|
||||
result += ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (op === undefined)
|
||||
result += '??? (' + toHex(b) + ')';
|
||||
else
|
||||
result += op.name + ' ' + this.dumpArgs(pc + 1, op.mode, symbols);
|
||||
|
||||
return result;
|
||||
return {
|
||||
pc: this.pc,
|
||||
ar: this.ar,
|
||||
xr: this.xr,
|
||||
yr: this.yr,
|
||||
sr: this.sr,
|
||||
sp: this.sp,
|
||||
cmd
|
||||
};
|
||||
}
|
||||
|
||||
public dumpPage(start?: word, end?: word) {
|
||||
let result = '';
|
||||
if (start === undefined) {
|
||||
start = this.pc >> 8;
|
||||
}
|
||||
if (end === undefined) {
|
||||
end = start;
|
||||
}
|
||||
for (let page = start; page <= end; page++) {
|
||||
for (let idx = 0; idx < 16; idx++) {
|
||||
result += toHex(page) + toHex(idx << 4) + ': ';
|
||||
for (let jdx = 0; jdx < 16; jdx++) {
|
||||
const b = this.readByteDebug(page * 256 + idx * 16 + jdx);
|
||||
result += toHex(b) + ' ';
|
||||
}
|
||||
result += ' ';
|
||||
for (let jdx = 0; jdx < 16; jdx++) {
|
||||
const b = this.readByte(page * 256 + idx * 16 + jdx) & 0x7f;
|
||||
if (b >= 0x20 && b < 0x7f) {
|
||||
result += String.fromCharCode(b);
|
||||
} else {
|
||||
result += '.';
|
||||
}
|
||||
}
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public list(_pc: word, symbols: symbols) {
|
||||
if (_pc === undefined) {
|
||||
_pc = this.pc;
|
||||
}
|
||||
const results = [];
|
||||
for (let jdx = 0; jdx < 20; jdx++) {
|
||||
const b = this.readByte(_pc), op = this.opary[b];
|
||||
results.push(this.dumpPC(_pc, symbols));
|
||||
_pc += sizes[op.mode];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public getSync() {
|
||||
return this.sync;
|
||||
}
|
||||
@ -1258,8 +1122,8 @@ export default class CPU6502 {
|
||||
return this.cycles;
|
||||
}
|
||||
|
||||
public registers() {
|
||||
return [this.pc, this.ar, this.xr, this.yr, this.sr, this.sp];
|
||||
public getOpInfo(opcode: byte) {
|
||||
return this.opary[opcode];
|
||||
}
|
||||
|
||||
public getState(): CpuState {
|
||||
@ -1284,29 +1148,36 @@ export default class CPU6502 {
|
||||
this.cycles = state.cycles;
|
||||
}
|
||||
|
||||
public dumpRegisters() {
|
||||
return toHex(this.pc, 4) +
|
||||
'- A=' + toHex(this.ar) +
|
||||
' X=' + toHex(this.xr) +
|
||||
' Y=' + toHex(this.yr) +
|
||||
' P=' + toHex(this.sr) +
|
||||
' S=' + toHex(this.sp) +
|
||||
' ' +
|
||||
((this.sr & flags.N) ? 'N' : '-') +
|
||||
((this.sr & flags.V) ? 'V' : '-') +
|
||||
'-' +
|
||||
((this.sr & flags.B) ? 'B' : '-') +
|
||||
((this.sr & flags.D) ? 'D' : '-') +
|
||||
((this.sr & flags.I) ? 'I' : '-') +
|
||||
((this.sr & flags.Z) ? 'Z' : '-') +
|
||||
((this.sr & flags.C) ? 'C' : '-');
|
||||
}
|
||||
public read(addr: word): byte;
|
||||
public read(page: byte, off: byte): byte;
|
||||
|
||||
public read(page: byte, off: byte): byte {
|
||||
public read(a: number, b?: number): byte {
|
||||
let page, off;
|
||||
if (b !== undefined) {
|
||||
page = a;
|
||||
off = b;
|
||||
} else {
|
||||
page = a >> 8;
|
||||
off = a & 0xff;
|
||||
}
|
||||
return this.memPages[page].read(page, off);
|
||||
}
|
||||
|
||||
public write(page: byte, off: byte, val: byte) {
|
||||
public write(addr: word, val: byte): void;
|
||||
public write(page: byte, off: byte, val: byte): void;
|
||||
|
||||
public write(a: number, b: number, c?: byte): void {
|
||||
let page, off, val;
|
||||
|
||||
if (c !== undefined ) {
|
||||
page = a;
|
||||
off = b;
|
||||
val = c;
|
||||
} else {
|
||||
page = a >> 8;
|
||||
off = a & 0xff;
|
||||
val = b;
|
||||
}
|
||||
this.memPages[page].write(page, off, val);
|
||||
}
|
||||
|
||||
|
284
js/debugger.ts
Normal file
284
js/debugger.ts
Normal file
@ -0,0 +1,284 @@
|
||||
import { debug, toHex } from './util';
|
||||
import { byte, word } from './types';
|
||||
|
||||
import CPU6502, { DebugInfo, flags, sizes } from './cpu6502';
|
||||
|
||||
export interface DebuggerContainer {
|
||||
run: () => void;
|
||||
stop: () => void;
|
||||
getCPU: () => CPU6502;
|
||||
}
|
||||
|
||||
type symbols = { [key: number]: string };
|
||||
type breakpointFn = (info: DebugInfo) => boolean
|
||||
|
||||
const alwaysBreak = (_info: DebugInfo) => { return true; };
|
||||
|
||||
export default class Debugger {
|
||||
private cpu: CPU6502;
|
||||
private verbose = false;
|
||||
private maxTrace = 256;
|
||||
private trace: DebugInfo[] = [];
|
||||
private breakpoints: Map<word, breakpointFn> = new Map();
|
||||
private symbols: symbols = {};
|
||||
|
||||
constructor(private container: DebuggerContainer) {
|
||||
this.cpu = container.getCPU();
|
||||
}
|
||||
|
||||
stepCycles(cycles: number) {
|
||||
this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => {
|
||||
const info = this.cpu.getDebugInfo();
|
||||
|
||||
if (this.breakpoints.get(info.pc)?.(info)) {
|
||||
debug('breakpoint', this.printDebugInfo(info));
|
||||
this.container.stop();
|
||||
return;
|
||||
}
|
||||
if (this.verbose) {
|
||||
debug(this.printDebugInfo(info));
|
||||
} else {
|
||||
this.trace.push(info);
|
||||
if (this.trace.length > this.maxTrace) {
|
||||
this.trace.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
continue() {
|
||||
this.container.run();
|
||||
}
|
||||
|
||||
setVerbose(verbose: boolean) {
|
||||
this.verbose = verbose;
|
||||
}
|
||||
|
||||
setMaxTrace(maxTrace: number) {
|
||||
this.maxTrace = maxTrace;
|
||||
}
|
||||
|
||||
getTrace() {
|
||||
return this.trace.map(this.printDebugInfo).join('\n');
|
||||
}
|
||||
|
||||
printTrace() {
|
||||
debug(this.getTrace());
|
||||
}
|
||||
|
||||
setBreakpoint(addr: word, exp?: breakpointFn) {
|
||||
this.breakpoints.set(addr, exp || alwaysBreak);
|
||||
}
|
||||
|
||||
clearBreakpoint(addr: word) {
|
||||
this.breakpoints.delete(addr);
|
||||
}
|
||||
|
||||
listBreakpoints() {
|
||||
for(const [addr, fn] of this.breakpoints.entries()) {
|
||||
debug(toHex(addr, 4), fn);
|
||||
}
|
||||
}
|
||||
|
||||
addSymbols(symbols: symbols) {
|
||||
this.symbols = { ...this.symbols, ...symbols };
|
||||
}
|
||||
|
||||
printDebugInfo(info: DebugInfo) {
|
||||
const { pc, cmd } = info;
|
||||
const symbol = this.padWithSymbol(pc);
|
||||
|
||||
return [
|
||||
toHex(pc, 4),
|
||||
'- ', symbol,
|
||||
this.dumpRegisters(info),
|
||||
' ',
|
||||
this.dumpRawOp(cmd),
|
||||
' ',
|
||||
this.dumpOp(pc, cmd)
|
||||
].join('');
|
||||
}
|
||||
|
||||
dumpPC(pc: word) {
|
||||
const b = this.cpu.read(pc);
|
||||
const op = this.cpu.getOpInfo(b);
|
||||
const size = sizes[op.mode];
|
||||
let result = toHex(pc, 4) + '- ';
|
||||
|
||||
result += this.padWithSymbol(pc);
|
||||
|
||||
const cmd = new Array(size);
|
||||
for (let idx = 0, jdx = pc; idx < size; idx++, jdx++) {
|
||||
cmd[idx] = this.cpu.read(jdx);
|
||||
}
|
||||
|
||||
result += this.dumpRawOp(cmd) + ' ' + this.dumpOp(pc, cmd);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dumpRegisters(debugInfo?: DebugInfo) {
|
||||
if (debugInfo === undefined) {
|
||||
debugInfo = this.cpu.getDebugInfo();
|
||||
}
|
||||
const { pc, ar, xr, yr, sr, sp } = debugInfo;
|
||||
return [
|
||||
toHex(pc, 4),
|
||||
'- ',
|
||||
' A=' + toHex(ar),
|
||||
' X=' + toHex(xr),
|
||||
' Y=' + toHex(yr),
|
||||
' P=' + toHex(sr),
|
||||
' S=' + toHex(sp),
|
||||
' ',
|
||||
((sr & flags.N) ? 'N' : '-'),
|
||||
((sr & flags.V) ? 'V' : '-'),
|
||||
'-',
|
||||
((sr & flags.B) ? 'B' : '-'),
|
||||
((sr & flags.D) ? 'D' : '-'),
|
||||
((sr & flags.I) ? 'I' : '-'),
|
||||
((sr & flags.Z) ? 'Z' : '-'),
|
||||
((sr & flags.C) ? 'C' : '-')
|
||||
].join('');
|
||||
}
|
||||
|
||||
dumpPage(start: byte, end?: byte) {
|
||||
let result = '';
|
||||
if (end === undefined) {
|
||||
end = start;
|
||||
}
|
||||
for (let page = start; page <= end; page++) {
|
||||
for (let idx = 0; idx < 16; idx++) {
|
||||
result += toHex(page) + toHex(idx << 4) + ': ';
|
||||
for (let jdx = 0; jdx < 16; jdx++) {
|
||||
const b = this.cpu.read(page, idx * 16 + jdx);
|
||||
result += toHex(b) + ' ';
|
||||
}
|
||||
result += ' ';
|
||||
for (let jdx = 0; jdx < 16; jdx++) {
|
||||
const b = this.cpu.read(page, idx * 16 + jdx) & 0x7f;
|
||||
if (b >= 0x20 && b < 0x7f) {
|
||||
result += String.fromCharCode(b);
|
||||
} else {
|
||||
result += '.';
|
||||
}
|
||||
}
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
list(pc: word) {
|
||||
const results = [];
|
||||
for (let idx = 0; idx < 20; idx++) {
|
||||
const b = this.cpu.read(pc);
|
||||
const op = this.cpu.getOpInfo(b);
|
||||
results.push(this.dumpPC(pc));
|
||||
pc += sizes[op.mode];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private padWithSymbol(pc: word): string {
|
||||
const padding = ' ';
|
||||
const symbol = this.symbols[pc];
|
||||
|
||||
let result: string = padding;
|
||||
if (symbol) {
|
||||
result = `${symbol}${padding.substring(symbol.length)}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private dumpRawOp(parts: byte[]) {
|
||||
const result = new Array(4);
|
||||
for (let idx = 0; idx < 4; idx++) {
|
||||
if (idx < parts.length) {
|
||||
result[idx] = toHex(parts[idx]);
|
||||
} else {
|
||||
result[idx] = ' ';
|
||||
}
|
||||
}
|
||||
return result.join(' ');
|
||||
}
|
||||
|
||||
private dumpOp(pc: word, parts: byte[]) {
|
||||
const op = this.cpu.getOpInfo(parts[0]);
|
||||
const lsb = parts[1];
|
||||
const msb = parts[2];
|
||||
const addr = (msb << 8) | lsb;
|
||||
let val;
|
||||
let off;
|
||||
const toHexOrSymbol = (v: word, n?: number) => (
|
||||
this.symbols[v] || ('$' + toHex(v, n))
|
||||
);
|
||||
|
||||
let result = op.name + ' ';
|
||||
switch (op.mode) {
|
||||
case 'implied':
|
||||
break;
|
||||
case 'immediate':
|
||||
result += '#' + toHexOrSymbol(lsb);
|
||||
break;
|
||||
case 'absolute':
|
||||
result += '' + toHexOrSymbol(addr, 4);
|
||||
break;
|
||||
case 'zeroPage':
|
||||
result += '' + toHexOrSymbol(lsb);
|
||||
break;
|
||||
case 'relative':
|
||||
{
|
||||
off = lsb;
|
||||
if (off > 127) {
|
||||
off -= 256;
|
||||
}
|
||||
pc += off + 1;
|
||||
result += '' + toHexOrSymbol(pc, 4) + ' (' + off + ')';
|
||||
}
|
||||
break;
|
||||
case 'absoluteX':
|
||||
result += '' + toHexOrSymbol(addr, 4)+ ',X';
|
||||
break;
|
||||
case 'absoluteY':
|
||||
result += '' + toHexOrSymbol(addr, 4) + ',Y';
|
||||
break;
|
||||
case 'zeroPageX':
|
||||
result += '' + toHexOrSymbol(lsb) + ',X';
|
||||
break;
|
||||
case 'zeroPageY':
|
||||
result += '' + toHexOrSymbol(lsb) + ',Y';
|
||||
break;
|
||||
case 'absoluteIndirect':
|
||||
result += '(' + toHexOrSymbol(addr, 4) + ')';
|
||||
break;
|
||||
case 'zeroPageXIndirect':
|
||||
result += '(' + toHexOrSymbol(lsb) + ',X)';
|
||||
break;
|
||||
case 'zeroPageIndirectY':
|
||||
result += '(' + toHexOrSymbol(lsb) + '),Y';
|
||||
break;
|
||||
case 'accumulator':
|
||||
result += 'A';
|
||||
break;
|
||||
case 'zeroPageIndirect':
|
||||
result += '(' + toHexOrSymbol(lsb) + ')';
|
||||
break;
|
||||
case 'absoluteXIndirect':
|
||||
result += '(' + toHexOrSymbol(addr, 4) + ',X)';
|
||||
break;
|
||||
case 'zeroPage_relative':
|
||||
val = lsb;
|
||||
off = msb;
|
||||
if (off > 127) {
|
||||
off -= 256;
|
||||
}
|
||||
pc += off + 2;
|
||||
result += '' + toHexOrSymbol(val) + ',' + toHexOrSymbol(pc, 4) + ' (' + off + ')';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ var options = {
|
||||
tick: updateUI
|
||||
};
|
||||
|
||||
var apple2 = new Apple2(options);
|
||||
export var apple2 = new Apple2(options);
|
||||
var cpu = apple2.getCPU();
|
||||
var io = apple2.getIO();
|
||||
|
||||
|
@ -52,7 +52,7 @@ var options = {
|
||||
tick: updateUI
|
||||
};
|
||||
|
||||
var apple2 = new Apple2(options);
|
||||
export var apple2 = new Apple2(options);
|
||||
var io = apple2.getIO();
|
||||
var cpu = apple2.getCPU();
|
||||
|
||||
|
10
js/mmu.ts
10
js/mmu.ts
@ -172,9 +172,9 @@ export interface MMUState {
|
||||
}
|
||||
|
||||
export default class MMU implements Memory, Restorable<MMUState> {
|
||||
private _readPages = new Array(0x100);
|
||||
private _writePages = new Array(0x100);
|
||||
private _pages = new Array(0x100);
|
||||
private _readPages = new Array<Memory>(0x100);
|
||||
private _writePages = new Array<Memory>(0x100);
|
||||
private _pages = new Array<Memory[]>(0x100);
|
||||
|
||||
// Language Card RAM Softswitches
|
||||
private _bank1: boolean;
|
||||
@ -833,6 +833,10 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
||||
this._writePages[page].write(page, off, val);
|
||||
}
|
||||
|
||||
public writeBank(bank: number,page: byte, off: byte, val: byte) {
|
||||
this._pages[page][bank].write(page, off, val);
|
||||
}
|
||||
|
||||
public resetVB() {
|
||||
this._vbEnd = this.cpu.getCycles() + 1000;
|
||||
}
|
||||
|
@ -1,96 +1,6 @@
|
||||
import CPU6502 from '../js/cpu6502';
|
||||
|
||||
function assertByte(b) {
|
||||
expect(b <= 0xFF).toEqual(true);
|
||||
expect(b >= 0x00).toEqual(true);
|
||||
}
|
||||
|
||||
function Memory(size) {
|
||||
var data = Buffer.alloc(size << 8);
|
||||
|
||||
return {
|
||||
start: function() {
|
||||
return 0;
|
||||
},
|
||||
|
||||
end: function() {
|
||||
return size - 1;
|
||||
},
|
||||
|
||||
read: function(page, off) {
|
||||
assertByte(page);
|
||||
assertByte(off);
|
||||
|
||||
return data[(page << 8) | off];
|
||||
},
|
||||
|
||||
write: function(page, off, val) {
|
||||
assertByte(page);
|
||||
assertByte(off);
|
||||
assertByte(val);
|
||||
|
||||
data[(page << 8) | off] = val;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function Program(page, code) {
|
||||
var data = Buffer.from(code);
|
||||
|
||||
return {
|
||||
start: function() {
|
||||
return page;
|
||||
},
|
||||
|
||||
end: function() {
|
||||
return page;
|
||||
},
|
||||
|
||||
read: function(page, off) {
|
||||
assertByte(page);
|
||||
assertByte(off);
|
||||
return data[off];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var 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
|
||||
]);
|
||||
import { TestMemory } from './util/memory';
|
||||
import { bios, Program } from './util/bios';
|
||||
|
||||
var FLAGS = {
|
||||
N: 0x80, // Negative
|
||||
@ -178,14 +88,14 @@ function testCode(code, steps, setupState, expectedState) {
|
||||
cpu.addPageHandler(program);
|
||||
|
||||
cpu.setState(initialState);
|
||||
cpu.stepDebug(steps);
|
||||
cpu.stepN(steps);
|
||||
expectState(initialState, finalState);
|
||||
}
|
||||
|
||||
describe('CPU6502', function() {
|
||||
beforeEach(function() {
|
||||
cpu = new CPU6502();
|
||||
memory = new Memory(4);
|
||||
memory = new TestMemory(4);
|
||||
|
||||
cpu.addPageHandler(memory);
|
||||
cpu.addPageHandler(bios);
|
||||
@ -1516,34 +1426,12 @@ describe('CPU6502', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#utility', function() {
|
||||
it('should list', function() {
|
||||
var listing = cpu.list(0xff00);
|
||||
expect(listing[0]).toEqual('FF00- 00 00 BRK #$00');
|
||||
});
|
||||
|
||||
it('should list with symbols', function() {
|
||||
var listing = cpu.list(0xff00, {0x00: 'ZERO', 0xFF00: 'ENTRY'});
|
||||
expect(listing[0]).toEqual('FF00- ENTRY 00 00 BRK #ZERO');
|
||||
});
|
||||
|
||||
it('should dump page', function() {
|
||||
var page = cpu.dumpPage(0xff);
|
||||
expect(page).toContain('FF80: 48 45 4C 4C 4F 0D 00 00 00 00 00 00 00 00 00 00 HELLO...........');
|
||||
});
|
||||
|
||||
it('should dump registers', function() {
|
||||
var regs = cpu.dumpRegisters();
|
||||
expect(regs).toEqual('0000- A=00 X=00 Y=00 P=20 S=FF --------');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('65c02', function() {
|
||||
beforeEach(function() {
|
||||
cpu = new CPU6502({'65C02': true});
|
||||
memory = new Memory(4);
|
||||
memory = new TestMemory(4);
|
||||
|
||||
cpu.addPageHandler(memory);
|
||||
cpu.addPageHandler(bios);
|
||||
|
59
test/js/debugger.spec.ts
Normal file
59
test/js/debugger.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import CPU6502 from '../../js/cpu6502';
|
||||
import Debugger, { DebuggerContainer } from '../../js/debugger';
|
||||
import { MemoryPages } from '../../js/types';
|
||||
import { TestMemory } from '../util/memory';
|
||||
import { bios } from '../util/bios';
|
||||
|
||||
describe('Debugger', () => {
|
||||
let cpu: CPU6502;
|
||||
let debuggerContainer: DebuggerContainer;
|
||||
let theDebugger: Debugger;
|
||||
let memory: MemoryPages;
|
||||
|
||||
beforeEach(() => {
|
||||
cpu = new CPU6502();
|
||||
memory = new TestMemory(4);
|
||||
|
||||
cpu.addPageHandler(memory);
|
||||
cpu.addPageHandler(bios);
|
||||
|
||||
debuggerContainer = {
|
||||
getCPU: () => cpu,
|
||||
run: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
theDebugger = new Debugger(debuggerContainer);
|
||||
});
|
||||
|
||||
describe('#utility', () => {
|
||||
it('should list without symbols', () => {
|
||||
const listing = theDebugger.list(0xff00);
|
||||
expect(listing[0]).toEqual(
|
||||
'FF00- 00 00 BRK #$00'
|
||||
);
|
||||
});
|
||||
|
||||
it('should list with symbols', () => {
|
||||
theDebugger.addSymbols({0x00: 'ZERO', 0xFF00: 'ENTRY'});
|
||||
|
||||
const listing = theDebugger.list(0xff00);
|
||||
expect(listing[0]).toEqual(
|
||||
'FF00- ENTRY 00 00 BRK #ZERO'
|
||||
);
|
||||
});
|
||||
|
||||
it('should dump page', () => {
|
||||
const page = theDebugger.dumpPage(0xff);
|
||||
expect(page).toContain(
|
||||
'FF80: 48 45 4C 4C 4F 0D 00 00 00 00 00 00 00 00 00 00 HELLO...........'
|
||||
);
|
||||
});
|
||||
|
||||
it('should dump registers', () => {
|
||||
const regs = theDebugger.dumpRegisters();
|
||||
expect(regs).toEqual(
|
||||
'0000- A=00 X=00 Y=00 P=20 S=FF --------'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
6
test/util/asserts.ts
Normal file
6
test/util/asserts.ts
Normal 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);
|
||||
};
|
62
test/util/bios.ts
Normal file
62
test/util/bios.ts
Normal file
@ -0,0 +1,62 @@
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
37
test/util/memory.ts
Normal file
37
test/util/memory.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { MemoryPages, byte } from '../../js/types';
|
||||
import { assertByte } from './asserts';
|
||||
|
||||
export class TestMemory implements MemoryPages {
|
||||
private data: Buffer;
|
||||
|
||||
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);
|
||||
|
||||
return this.data[(page << 8) | off];
|
||||
}
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
assertByte(page);
|
||||
assertByte(off);
|
||||
assertByte(val);
|
||||
|
||||
this.data[(page << 8) | off] = val;
|
||||
}
|
||||
|
||||
reset() {
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user