mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
486 lines
15 KiB
TypeScript
486 lines
15 KiB
TypeScript
import CPU6502 from './cpu6502';
|
|
import { Card, Memory, MemoryPages, TapeData, byte, Restorable } from './types';
|
|
import { debug, garbage } from './util';
|
|
import { VideoModes } from './videomodes';
|
|
|
|
export type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
export type button = 0 | 1 | 2;
|
|
export type paddle = 0 | 1 | 2 | 3;
|
|
export type annunciator = 0 | 1 | 2 | 3;
|
|
|
|
type Annunciators = Record<annunciator, boolean>;
|
|
|
|
export interface Apple2IOState {
|
|
annunciators: Annunciators;
|
|
cards: Array<unknown | null>;
|
|
}
|
|
|
|
export type SampleListener = (sample: number[]) => void;
|
|
|
|
const LOC = {
|
|
KEYBOARD: 0x00, // keyboard data (latched) (Read),
|
|
STROBE: 0x10, // clear bit 7 of keyboard data ($C000)
|
|
TAPEOUT: 0x20, // toggle the cassette output.
|
|
SPEAKER: 0x30, // toggle speaker diaphragm
|
|
C040STB: 0x40, // trigger game port sync
|
|
CLRTEXT: 0x50, // display graphics
|
|
SETTEXT: 0x51, // display text
|
|
CLRMIXED: 0x52, // clear mixed mode- enable full graphics
|
|
SETMIXED: 0x53, // enable graphics/text mixed mode
|
|
PAGE1: 0x54, // select text/graphics page1
|
|
PAGE2: 0x55, // select text/graphics page2
|
|
CLRHIRES: 0x56, // select Lo-res
|
|
SETHIRES: 0x57, // select Hi-res
|
|
CLRAN0: 0x58, // Set annunciator-0 output to 0
|
|
SETAN0: 0x59, // Set annunciator-0 output to 1
|
|
CLRAN1: 0x5A, // Set annunciator-1 output to 0
|
|
SETAN1: 0x5B, // Set annunciator-1 output to 1
|
|
CLRAN2: 0x5C, // Set annunciator-2 output to 0
|
|
SETAN2: 0x5D, // Set annunciator-2 output to 1
|
|
CLRAN3: 0x5E, // Set annunciator-3 output to 0
|
|
SETAN3: 0x5F, // Set annunciator-3 output to 1
|
|
TAPEIN: 0x60, // bit 7: data from cassette
|
|
PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
|
|
PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
|
|
PB2: 0x63, // game Pushbutton 2 (read)
|
|
PADDLE0: 0x64, // bit 7: status of pdl-0 timer (read)
|
|
PADDLE1: 0x65, // bit 7: status of pdl-1 timer (read)
|
|
PADDLE2: 0x66, // bit 7: status of pdl-2 timer (read)
|
|
PADDLE3: 0x67, // bit 7: status of pdl-3 timer (read)
|
|
PDLTRIG: 0x70, // trigger paddles
|
|
ACCEL: 0x74, // CPU Speed control
|
|
};
|
|
|
|
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
|
|
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
|
|
private _auxRom: Memory | null = null;
|
|
|
|
private _khz = 1023;
|
|
private _rate = 44000;
|
|
private _sample_size = 4096;
|
|
|
|
private _cycles_per_sample: number;
|
|
|
|
private _buffer: string[] = [];
|
|
private _key = 0;
|
|
private _keyDown = false;
|
|
private _button = [false, false, false];
|
|
private _paddle = [0.0, 0.0, 0.0, 0.0];
|
|
private _phase = -1;
|
|
private _sample: number[] = [];
|
|
private _sampleIdx = 0;
|
|
private _sampleTime = 0;
|
|
private _didAudio = false;
|
|
|
|
private _high = 0.5;
|
|
private _low = -0.5;
|
|
|
|
private _audioListener: SampleListener | undefined;
|
|
|
|
private _trigger = 0;
|
|
private _annunciators: Annunciators = [false, false, false, false];
|
|
|
|
private _tape: TapeData = [];
|
|
private _tapeOffset: number = 0;
|
|
private _tapeNext: number = 0;
|
|
private _tapeCurrent = false;
|
|
|
|
constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
_debug(..._args: unknown[]) {
|
|
// debug.apply(this, arguments);
|
|
}
|
|
|
|
_tick() {
|
|
const now = this.cpu.getCycles();
|
|
const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
|
|
for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
|
|
this._sample[this._sampleIdx++] = phase;
|
|
if (this._sampleIdx === this._sample_size) {
|
|
if (this._audioListener) {
|
|
this._audioListener(this._sample);
|
|
}
|
|
this._sample = new Array<number>(this._sample_size);
|
|
this._sampleIdx = 0;
|
|
}
|
|
}
|
|
this._didAudio = false;
|
|
}
|
|
|
|
_calcSampleRate() {
|
|
this._cycles_per_sample = this._khz * 1000 / this._rate;
|
|
}
|
|
|
|
_updateKHz(khz: number) {
|
|
this._khz = khz;
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
_access(off: byte, val?: byte): byte | undefined {
|
|
let result: number | undefined = 0;
|
|
const now = this.cpu.getCycles();
|
|
const writeMode = val === undefined;
|
|
const delta = now - this._trigger;
|
|
switch (off) {
|
|
case LOC.CLRTEXT:
|
|
this._debug('Graphics Mode');
|
|
this.vm.text(false);
|
|
break;
|
|
case LOC.SETTEXT:
|
|
this._debug('Text Mode');
|
|
this.vm.text(true);
|
|
break;
|
|
case LOC.CLRMIXED:
|
|
this._debug('Mixed Mode off');
|
|
this.vm.mixed(false);
|
|
break;
|
|
case LOC.SETMIXED:
|
|
this._debug('Mixed Mode on');
|
|
this.vm.mixed(true);
|
|
break;
|
|
case LOC.CLRHIRES:
|
|
this._debug('LoRes Mode');
|
|
this.vm.hires(false);
|
|
break;
|
|
case LOC.SETHIRES:
|
|
this._debug('HiRes Mode');
|
|
this.vm.hires(true);
|
|
break;
|
|
case LOC.PAGE1:
|
|
this.vm.page(1);
|
|
break;
|
|
case LOC.PAGE2:
|
|
this.vm.page(2);
|
|
break;
|
|
case LOC.SETAN0:
|
|
this._debug('Annunciator 0 on');
|
|
this._annunciators[0] = true;
|
|
break;
|
|
case LOC.SETAN1:
|
|
this._debug('Annunciator 1 on');
|
|
this._annunciators[1] = true;
|
|
break;
|
|
case LOC.SETAN2:
|
|
this._debug('Annunciator 2 on');
|
|
this._annunciators[2] = true;
|
|
break;
|
|
case LOC.SETAN3:
|
|
this._debug('Annunciator 3 on');
|
|
this._annunciators[3] = true;
|
|
break;
|
|
case LOC.CLRAN0:
|
|
this._debug('Annunciator 0 off');
|
|
this._annunciators[0] = false;
|
|
break;
|
|
case LOC.CLRAN1:
|
|
this._debug('Annunciator 1 off');
|
|
this._annunciators[1] = false;
|
|
break;
|
|
case LOC.CLRAN2:
|
|
this._debug('Annunciator 2 off');
|
|
this._annunciators[2] = false;
|
|
break;
|
|
case LOC.CLRAN3:
|
|
this._debug('Annunciator 3 off');
|
|
this._annunciators[3] = false;
|
|
break;
|
|
case LOC.PB0:
|
|
result = this._button[0] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PB1:
|
|
result = this._button[1] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PB2:
|
|
result = this._button[2] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PADDLE0:
|
|
result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE1:
|
|
result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE2:
|
|
result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE3:
|
|
result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.ACCEL:
|
|
if (val !== undefined) {
|
|
this._updateKHz(val & 0x01 ? 1023 : 4096);
|
|
}
|
|
break;
|
|
case LOC.TAPEIN:
|
|
if (this._tapeOffset === -1) {
|
|
this._tapeOffset = 0;
|
|
this._tapeNext = now;
|
|
}
|
|
|
|
if (this._tapeOffset < this._tape.length) {
|
|
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
|
while (now >= this._tapeNext) {
|
|
if ((this._tapeOffset % 1000) === 0) {
|
|
debug(`Read ${this._tapeOffset / 1000}`);
|
|
}
|
|
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
|
this._tapeNext += this._tape[this._tapeOffset++][0];
|
|
}
|
|
|
|
}
|
|
|
|
result = this._tapeCurrent ? 0x80 : 0x00;
|
|
break;
|
|
|
|
default:
|
|
switch (off & 0xf0) {
|
|
case LOC.KEYBOARD: // C00x
|
|
result = this._key;
|
|
break;
|
|
case LOC.STROBE: // C01x
|
|
if (off === LOC.STROBE || writeMode) {
|
|
this._key &= 0x7f;
|
|
}
|
|
if (this._buffer.length > 0) {
|
|
let val = this._buffer.shift() as string;
|
|
if (val === '\n') {
|
|
val = '\r';
|
|
}
|
|
this._key = val.charCodeAt(0) | 0x80;
|
|
}
|
|
result = this._key & 0x7f;
|
|
if (off === LOC.STROBE) {
|
|
result |= this._keyDown ? 0x80 : 0x00;
|
|
}
|
|
break;
|
|
case LOC.TAPEOUT: // C02x
|
|
// this._phase = -this._phase;
|
|
// this._didAudio = true;
|
|
// this._tick();
|
|
break;
|
|
case LOC.SPEAKER: // C03x
|
|
this._phase = -this._phase;
|
|
this._didAudio = true;
|
|
this._tick();
|
|
break;
|
|
case LOC.C040STB: // C04x
|
|
// I/O Strobe
|
|
break;
|
|
case LOC.PDLTRIG: // C07x
|
|
this._trigger = this.cpu.getCycles();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (val !== undefined) {
|
|
result = undefined;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
start() {
|
|
return 0xc0;
|
|
}
|
|
|
|
end() {
|
|
return 0xcf;
|
|
}
|
|
|
|
ioSwitch(off: byte, val?: byte) {
|
|
let result;
|
|
if (off < 0x80) {
|
|
result = this._access(off, val);
|
|
} else {
|
|
const slot = (off & 0x70) >> 4;
|
|
const card = this._slot[slot];
|
|
if (card && card.ioSwitch) {
|
|
result = card.ioSwitch(off, val);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
reset() {
|
|
for (let slot = 0; slot < 8; slot++) {
|
|
const card = this._slot[slot];
|
|
if (card) {
|
|
card.reset?.();
|
|
}
|
|
}
|
|
this.vm.reset();
|
|
}
|
|
|
|
blit() {
|
|
const card = this._slot[3];
|
|
if (card) {
|
|
return card.blit?.();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
read(page: byte, off: byte) {
|
|
let result: number = 0;
|
|
let slot;
|
|
let card;
|
|
|
|
switch (page) {
|
|
case 0xc0:
|
|
result = this.ioSwitch(off, undefined) || 0;
|
|
break;
|
|
case 0xc1:
|
|
case 0xc2:
|
|
case 0xc3:
|
|
case 0xc4:
|
|
case 0xc5:
|
|
case 0xc6:
|
|
case 0xc7:
|
|
slot = page & 0x0f;
|
|
card = this._slot[slot];
|
|
if (this._auxRom !== card) {
|
|
// _debug('Setting auxRom to slot', slot);
|
|
this._auxRom = card;
|
|
}
|
|
if (card) {
|
|
result = card.read(page, off);
|
|
} else {
|
|
result = garbage();
|
|
}
|
|
break;
|
|
default:
|
|
if (this._auxRom) {
|
|
result = this._auxRom.read(page, off);
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
write(page: byte, off: byte, val: byte) {
|
|
let slot;
|
|
let card;
|
|
|
|
switch (page) {
|
|
case 0xc0:
|
|
this.ioSwitch(off, val);
|
|
break;
|
|
case 0xc1:
|
|
case 0xc2:
|
|
case 0xc3:
|
|
case 0xc4:
|
|
case 0xc5:
|
|
case 0xc6:
|
|
case 0xc7:
|
|
slot = page & 0x0f;
|
|
card = this._slot[slot];
|
|
if (this._auxRom !== card) {
|
|
// _debug('Setting auxRom to slot', slot);
|
|
this._auxRom = card;
|
|
}
|
|
if (card) {
|
|
card.write(page, off, val);
|
|
}
|
|
break;
|
|
default:
|
|
if (this._auxRom) {
|
|
this._auxRom.write(page, off, val);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
getState(): Apple2IOState {
|
|
// TODO vet more potential state
|
|
return {
|
|
annunciators: this._annunciators,
|
|
cards: this._slot.map((card) => card ? card.getState() : null)
|
|
};
|
|
}
|
|
|
|
setState(state: Apple2IOState) {
|
|
this._annunciators = state.annunciators;
|
|
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
|
|
}
|
|
|
|
setSlot(slot: slot, card: Card) {
|
|
this._slot[slot] = card;
|
|
}
|
|
|
|
keyDown(ascii: byte) {
|
|
this._keyDown = true;
|
|
this._key = ascii | 0x80;
|
|
}
|
|
|
|
keyUp() {
|
|
this._keyDown = false;
|
|
}
|
|
|
|
buttonDown(b: button, state = true) {
|
|
this._button[b] = state;
|
|
}
|
|
|
|
buttonUp(b: button) {
|
|
this._button[b] = false;
|
|
}
|
|
|
|
paddle(p: paddle, v: byte) {
|
|
this._paddle[p] = v;
|
|
}
|
|
|
|
updateKHz(khz: number) {
|
|
this._updateKHz(khz);
|
|
}
|
|
|
|
getKHz() {
|
|
return this._khz;
|
|
}
|
|
|
|
setKeyBuffer(buffer: string) {
|
|
this._buffer = buffer.split(''); // split to charaters
|
|
if (this._buffer.length > 0) {
|
|
this._keyDown = true;
|
|
const key = this._buffer.shift() as string; // never undefined
|
|
this._key = key.charCodeAt(0) | 0x80;
|
|
}
|
|
}
|
|
|
|
setTape(tape: TapeData) {
|
|
debug(`Tape length: ${tape.length}`);
|
|
this._tape = tape;
|
|
this._tapeOffset = -1;
|
|
}
|
|
|
|
sampleRate(rate: number, sample_size: number) {
|
|
this._rate = rate;
|
|
this._sample_size = sample_size;
|
|
this._sample = new Array<number>(this._sample_size);
|
|
this._sampleIdx = 0;
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
tick() {
|
|
this._tick();
|
|
for (let idx = 0; idx < 8; idx++) {
|
|
this._slot[idx]?.tick?.();
|
|
}
|
|
}
|
|
|
|
addSampleListener(cb: SampleListener) {
|
|
this._audioListener = cb;
|
|
}
|
|
|
|
annunciator(idx: annunciator) {
|
|
return this._annunciators[idx];
|
|
}
|
|
|
|
cycles() {
|
|
return this.cpu.getCycles();
|
|
}
|
|
}
|