apple2js/js/apple2io.ts
Ian Flanigan 04ae0327c2
Add the recommended eslint plugins for TypeScript (#121)
This adds both the recommended TypeScript checks, plus the recommended
TypeScript checks that require type checking.  This latter addition
means that eslint essentially has to compile all of the TypeScript in
the project, causing it to be slower. This isn't much of a problem in
VS Code because there's a lot of caching being done, but it's clearly
slower when run on the commandline.

All of the errors are either fixed or suppressed.  Some errors are
suppressed because fixing them would be too laborious for the little
value gained.

The eslint config is also slightly refactored to separate the strictly
TypeScript checks from the JavaScript checks.
2022-05-31 08:38:40 -07:00

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) {
this._button[b] = true;
}
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();
}
}