import { CPU6502 } from '@whscullin/cpu6502'; import RAM, { RAMState } from './ram'; import ROM, { ROMState } from './roms/rom'; import { debug } from './util'; import { byte, Memory, Restorable } from './types'; import Apple2IO from './apple2io'; import { HiresPage, LoresPage, VideoModes } from './videomodes'; /* * I/O Switch locations */ const LOC = { // 80 Column memory _80STOREOFF: 0x00, _80STOREON: 0x01, // Aux RAM RAMRDOFF: 0x02, RAMRDON: 0x03, RAMWROFF: 0x04, RAMWRON: 0x05, // Bank switched ROM INTCXROMOFF: 0x06, INTCXROMON: 0x07, ALTZPOFF: 0x08, ALTZPON: 0x09, SLOTC3ROMOFF: 0x0a, SLOTC3ROMON: 0x0b, // 80 Column video CLR80VID: 0x0c, // clear 80 column mode SET80VID: 0x0d, // set 80 column mode CLRALTCH: 0x0e, // clear mousetext SETALTCH: 0x0f, // set mousetext // Status BSRBANK2: 0x11, BSRREADRAM: 0x12, RAMRD: 0x13, RAMWRT: 0x14, INTCXROM: 0x15, ALTZP: 0x16, SLOTC3ROM: 0x17, _80STORE: 0x18, VERTBLANK: 0x19, RDTEXT: 0x1a, // using text mode RDMIXED: 0x1b, // using mixed mode RDPAGE2: 0x1c, // using text/graphics page2 RDHIRES: 0x1d, // using Hi-res graphics mode RDALTCH: 0x1e, // using alternate character set RD80VID: 0x1f, // using 80-column display mode // Graphics PAGE1: 0x54, // select text/graphics page1 main/aux PAGE2: 0x55, // select text/graphics page2 main/aux RESET_HIRES: 0x56, SET_HIRES: 0x57, DHIRESON: 0x5e, // Enable double hires (CLRAN3) DHIRESOFF: 0x5f, // Disable double hires (SETAN3) // Misc BANK: 0x73, // Back switched RAM card bank IOUDISON: 0x7e, // W IOU Disable on / R7 IOU Disable IOUDISOFF: 0x7f, // W IOU Disable off / R7 Double Hires // Language Card // Bank 2 READBSR2: 0x80, WRITEBSR2: 0x81, OFFBSR2: 0x82, READWRBSR2: 0x83, // Bank 1 READBSR1: 0x88, WRITEBSR1: 0x89, OFFBSR1: 0x8a, READWRBSR1: 0x8b, }; class Switches implements Memory { constructor(private mmu: MMU) {} read(_page: byte, off: byte) { return this.mmu._access(off) || 0; } write(_page: byte, off: byte, val: byte) { this.mmu._access(off, val); } } class AuxRom implements Memory { constructor( private readonly mmu: MMU, private readonly rom: ROM ) {} _access(page: byte, off: byte) { if (page === 0xc3) { this.mmu._setIntc8rom(true); this.mmu._updateBanks(); } if (page === 0xcf && off === 0xff) { this.mmu._setIntc8rom(false); this.mmu._updateBanks(); } } read(page: byte, off: byte) { this._access(page, off); return this.rom.read(page, off); } write(page: byte, off: byte, _val: byte) { this._access(page, off); } } export interface MMUState { bank1: boolean; readbsr: boolean; writebsr: boolean; prewrite: boolean; intcxrom: boolean; slot3rom: boolean; intc8rom: boolean; auxRamRead: boolean; auxRamWrite: boolean; altzp: boolean; _80store: boolean; page2: boolean; hires: boolean; mem00_01: [RAMState, RAMState]; mem02_03: [RAMState, RAMState]; mem0C_1F: [RAMState, RAMState]; mem60_BF: [RAMState, RAMState]; memD0_DF: [ROMState, RAMState, RAMState, RAMState, RAMState]; memE0_FF: [ROMState, RAMState, RAMState]; } export default class MMU implements Memory, Restorable { private _readPages = new Array(0x100); private _writePages = new Array(0x100); private _pages = new Array(0x100); // Language Card RAM Softswitches private _bank1: boolean; private _readbsr: boolean; private _writebsr: boolean; private _prewrite: boolean; // Auxillary ROM private _intcxrom: boolean; private _slot3rom: boolean; private _intc8rom: boolean; // Auxillary RAM private _auxRamRead: boolean; private _auxRamWrite: boolean; private _altzp: boolean; // Video private __80store: boolean; private _page2: boolean; private _hires: boolean; private _iouDisable: boolean; private _vbEnd = 0; private switches = new Switches(this); private auxRom = new AuxRom(this, this.rom); // These fields represent the bank-switched memory ranges. private mem00_01 = [new RAM(0x0, 0x1), new RAM(0x0, 0x1)]; private mem02_03 = [new RAM(0x2, 0x3), new RAM(0x2, 0x3)]; private mem04_07 = [this.lores1.bank0(), this.lores1.bank1()]; private mem08_0B = [this.lores2.bank0(), this.lores2.bank1()]; private mem0C_1F = [new RAM(0xc, 0x1f), new RAM(0xc, 0x1f)]; private mem20_3F = [this.hires1.bank0(), this.hires1.bank1()]; private mem40_5F = [this.hires2.bank0(), this.hires2.bank1()]; private mem60_BF = [new RAM(0x60, 0xbf), new RAM(0x60, 0xbf)]; private memC0_C0 = [this.switches]; private memC1_CF = [this.io, this.auxRom]; private memD0_DF: [ROM, RAM, RAM, RAM, RAM] = [ this.rom, new RAM(0xd0, 0xdf), new RAM(0xd0, 0xdf), new RAM(0xd0, 0xdf), new RAM(0xd0, 0xdf), ]; private memE0_FF: [ROM, RAM, RAM] = [ this.rom, new RAM(0xe0, 0xff), new RAM(0xe0, 0xff), ]; constructor( private readonly cpu: CPU6502, private readonly vm: VideoModes, private readonly lores1: LoresPage, private readonly lores2: LoresPage, private readonly hires1: HiresPage, private readonly hires2: HiresPage, private readonly io: Apple2IO, private readonly rom: ROM ) { /* * Initialize read/write banks */ // Zero Page/Stack for (let idx = 0x0; idx < 0x2; idx++) { this._pages[idx] = this.mem00_01; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // 0x300-0x400 for (let idx = 0x2; idx < 0x4; idx++) { this._pages[idx] = this.mem02_03; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Text Page 1 for (let idx = 0x4; idx < 0x8; idx++) { this._pages[idx] = this.mem04_07; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Text Page 2 for (let idx = 0x8; idx < 0xc; idx++) { this._pages[idx] = this.mem08_0B; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // 0xC00-0x2000 for (let idx = 0xc; idx < 0x20; idx++) { this._pages[idx] = this.mem0C_1F; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Hires Page 1 for (let idx = 0x20; idx < 0x40; idx++) { this._pages[idx] = this.mem20_3F; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Hires Page 2 for (let idx = 0x40; idx < 0x60; idx++) { this._pages[idx] = this.mem40_5F; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // 0x6000-0xc000 for (let idx = 0x60; idx < 0xc0; idx++) { this._pages[idx] = this.mem60_BF; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // I/O Switches { const idx = 0xc0; this._pages[idx] = this.memC0_C0; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Slots for (let idx = 0xc1; idx < 0xd0; idx++) { this._pages[idx] = this.memC1_CF; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Basic ROM for (let idx = 0xd0; idx < 0xe0; idx++) { this._pages[idx] = this.memD0_DF; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } // Monitor ROM for (let idx = 0xe0; idx < 0x100; idx++) { this._pages[idx] = this.memE0_FF; this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } } private _initSwitches() { this._bank1 = false; this._readbsr = false; this._writebsr = false; this._prewrite = false; this._auxRamRead = false; this._auxRamWrite = false; this._altzp = false; this._intcxrom = false; this._slot3rom = false; this._intc8rom = false; this.__80store = false; this._page2 = false; this._hires = false; this._iouDisable = true; } private _debug(..._args: unknown[]) { // debug.apply(this, _args); } _setIntc8rom(on: boolean) { this._intc8rom = on; } _updateBanks() { if (this._auxRamRead) { for (let idx = 0x02; idx < 0xc0; idx++) { this._readPages[idx] = this._pages[idx][1]; } } else { for (let idx = 0x02; idx < 0xc0; idx++) { this._readPages[idx] = this._pages[idx][0]; } } if (this._auxRamWrite) { for (let idx = 0x02; idx < 0xc0; idx++) { this._writePages[idx] = this._pages[idx][1]; } } else { for (let idx = 0x02; idx < 0xc0; idx++) { this._writePages[idx] = this._pages[idx][0]; } } if (this.__80store) { if (this._page2) { for (let idx = 0x4; idx < 0x8; idx++) { this._readPages[idx] = this._pages[idx][1]; this._writePages[idx] = this._pages[idx][1]; } if (this._hires) { for (let idx = 0x20; idx < 0x40; idx++) { this._readPages[idx] = this._pages[idx][1]; this._writePages[idx] = this._pages[idx][1]; } } } else { for (let idx = 0x4; idx < 0x8; idx++) { this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } if (this._hires) { for (let idx = 0x20; idx < 0x40; idx++) { this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } } } } if (this._intcxrom) { for (let idx = 0xc1; idx < 0xd0; idx++) { this._readPages[idx] = this._pages[idx][1]; this._writePages[idx] = this._pages[idx][1]; } } else { for (let idx = 0xc1; idx < 0xd0; idx++) { this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } if (!this._slot3rom) { this._readPages[0xc3] = this._pages[0xc3][1]; this._writePages[0xc3] = this._pages[0xc3][1]; } if (this._intc8rom) { for (let idx = 0xc8; idx < 0xd0; idx++) { this._readPages[idx] = this._pages[idx][1]; this._writePages[idx] = this._pages[idx][1]; } } } if (this._altzp) { for (let idx = 0x0; idx < 0x2; idx++) { this._readPages[idx] = this._pages[idx][1]; this._writePages[idx] = this._pages[idx][1]; } } else { for (let idx = 0x0; idx < 0x2; idx++) { this._readPages[idx] = this._pages[idx][0]; this._writePages[idx] = this._pages[idx][0]; } } if (this._readbsr) { if (this._bank1) { for (let idx = 0xd0; idx < 0xe0; idx++) { this._readPages[idx] = this._pages[idx][this._altzp ? 2 : 1]; } } else { for (let idx = 0xd0; idx < 0xe0; idx++) { this._readPages[idx] = this._pages[idx][this._altzp ? 4 : 3]; } } for (let idx = 0xe0; idx < 0x100; idx++) { this._readPages[idx] = this._pages[idx][this._altzp ? 2 : 1]; } } else { for (let idx = 0xd0; idx < 0x100; idx++) { this._readPages[idx] = this._pages[idx][0]; } } if (this._writebsr) { if (this._bank1) { for (let idx = 0xd0; idx < 0xe0; idx++) { this._writePages[idx] = this._pages[idx][this._altzp ? 2 : 1]; } } else { for (let idx = 0xd0; idx < 0xe0; idx++) { this._writePages[idx] = this._pages[idx][this._altzp ? 4 : 3]; } } for (let idx = 0xe0; idx < 0x100; idx++) { this._writePages[idx] = this._pages[idx][this._altzp ? 2 : 1]; } } else { for (let idx = 0xd0; idx < 0x100; idx++) { this._writePages[idx] = this._pages[idx][0]; } } } // Apple //e memory management private _accessMMUSet(off: byte, _val?: byte) { switch (off) { case LOC._80STOREOFF: this.__80store = false; this._debug('80 Store Off', _val); this.vm.page(this._page2 ? 2 : 1); break; case LOC._80STOREON: this.__80store = true; this._debug('80 Store On', _val); break; case LOC.RAMRDOFF: this._auxRamRead = false; this._debug('Aux RAM Read Off'); break; case LOC.RAMRDON: this._auxRamRead = true; this._debug('Aux RAM Read On'); break; case LOC.RAMWROFF: this._auxRamWrite = false; this._debug('Aux RAM Write Off'); break; case LOC.RAMWRON: this._auxRamWrite = true; this._debug('Aux RAM Write On'); break; case LOC.INTCXROMOFF: this._intcxrom = false; if (this._slot3rom) { this._intc8rom = false; } this._debug('Int CX ROM Off'); break; case LOC.INTCXROMON: this._intcxrom = true; this._debug('Int CX ROM On'); break; case LOC.ALTZPOFF: // 0x08 this._altzp = false; this._debug('Alt ZP Off'); break; case LOC.ALTZPON: // 0x09 this._altzp = true; this._debug('Alt ZP On'); break; case LOC.SLOTC3ROMOFF: // 0x0A this._slot3rom = false; this._debug('Slot 3 ROM Off'); break; case LOC.SLOTC3ROMON: // 0x0B this._slot3rom = true; this._debug('Slot 3 ROM On'); break; // Graphics Switches case LOC.CLR80VID: this._debug('80 Column Mode off'); this.vm._80col(false); break; case LOC.SET80VID: this._debug('80 Column Mode on'); this.vm._80col(true); break; case LOC.CLRALTCH: this._debug('Alt Char off'); this.vm.altChar(false); break; case LOC.SETALTCH: this._debug('Alt Char on'); this.vm.altChar(true); break; } this._updateBanks(); } // Status registers private _accessStatus(off: byte, val?: byte) { let result = undefined; switch (off) { case LOC.BSRBANK2: this._debug(`Bank 2 Read ${!this._bank1 ? 'true' : 'false'}`); result = !this._bank1 ? 0x80 : 0x00; break; case LOC.BSRREADRAM: this._debug( `Bank SW RAM Read ${this._readbsr ? 'true' : 'false'}` ); result = this._readbsr ? 0x80 : 0x00; break; case LOC.RAMRD: // 0xC013 this._debug( `Aux RAM Read ${this._auxRamRead ? 'true' : 'false'}` ); result = this._auxRamRead ? 0x80 : 0x0; break; case LOC.RAMWRT: // 0xC014 this._debug( `Aux RAM Write ${this._auxRamWrite ? 'true' : 'false'}` ); result = this._auxRamWrite ? 0x80 : 0x0; break; case LOC.INTCXROM: // 0xC015 // _debug('Int CX ROM ' + _intcxrom); result = this._intcxrom ? 0x80 : 0x00; break; case LOC.ALTZP: // 0xC016 this._debug(`Alt ZP ${this._altzp ? 'true' : 'false'}`); result = this._altzp ? 0x80 : 0x0; break; case LOC.SLOTC3ROM: // 0xC017 this._debug(`Slot C3 ROM ${this._slot3rom ? 'true' : 'false'}`); result = this._slot3rom ? 0x80 : 0x00; break; case LOC._80STORE: // 0xC018 this._debug(`80 Store ${this.__80store ? 'true' : 'false'}`); result = this.__80store ? 0x80 : 0x00; break; case LOC.VERTBLANK: // 0xC019 // result = cpu.getCycles() % 20 < 5 ? 0x80 : 0x00; result = this.cpu.getCycles() < this._vbEnd ? 0x80 : 0x00; break; case LOC.RDTEXT: result = this.vm.isText() ? 0x80 : 0x0; break; case LOC.RDMIXED: result = this.vm.isMixed() ? 0x80 : 0x0; break; case LOC.RDPAGE2: result = this._page2 ? 0x80 : 0x0; break; case LOC.RDHIRES: result = this.vm.isHires() ? 0x80 : 0x0; break; case LOC.RD80VID: result = this.vm.is80Col() ? 0x80 : 0x0; break; case LOC.RDALTCH: result = this.vm.isAltChar() ? 0x80 : 0x0; break; default: result = this.io.ioSwitch(off, val); } return result; } private _accessIOUDisable(off: byte, val?: byte) { const writeMode = val !== undefined; let result; switch (off) { case LOC.IOUDISON: if (writeMode) { this._iouDisable = true; } else { result = this._iouDisable ? 0x00 : 0x80; } break; case LOC.IOUDISOFF: if (writeMode) { this._iouDisable = false; } else { result = this.vm.isDoubleHires() ? 0x80 : 0x00; } break; default: result = this.io.ioSwitch(off, val); } return result; } private _accessGraphics(off: byte, val?: byte) { let result: byte | undefined = 0; switch (off) { case LOC.PAGE1: this._page2 = false; if (!this.__80store) { result = this.io.ioSwitch(off, val); } this._debug('Page 2 off'); break; case LOC.PAGE2: this._page2 = true; if (!this.__80store) { result = this.io.ioSwitch(off, val); } this._debug('Page 2 on'); break; case LOC.RESET_HIRES: this._hires = false; result = this.io.ioSwitch(off, val); this._debug('Hires off'); break; case LOC.DHIRESON: if (this._iouDisable) { this.vm.doubleHires(true); } else { result = this.io.ioSwitch(off, val); // an3 } break; case LOC.DHIRESOFF: if (this._iouDisable) { this.vm.doubleHires(false); } else { result = this.io.ioSwitch(off, val); // an3 } break; case LOC.SET_HIRES: this._hires = true; result = this.io.ioSwitch(off, val); this._debug('Hires on'); break; default: result = this.io.ioSwitch(off, val); break; } this._updateBanks(); return result; } private _accessLangCard(off: byte, val?: byte) { const readMode = val === undefined; const result = readMode ? 0 : undefined; const writeSwitch = off & 0x01; const offSwitch = off & 0x02; const bank1Switch = off & 0x08; let bankStr; let rwStr; if (writeSwitch) { // 0xC081, 0xC083 if (readMode) { if (this._prewrite) { this._writebsr = true; } } this._prewrite = readMode; if (offSwitch) { // 0xC08B this._readbsr = true; rwStr = 'Read/Write'; } else { this._readbsr = false; rwStr = 'Write'; } } else { // 0xC080, 0xC082 this._writebsr = false; this._prewrite = false; if (offSwitch) { // 0xC082 this._readbsr = false; rwStr = 'Off'; } else { // 0xC080 this._readbsr = true; rwStr = 'Read'; } } if (bank1Switch) { this._bank1 = true; bankStr = 'Bank 1'; } else { this._bank1 = false; bankStr = 'Bank 2'; } this._debug(bankStr, rwStr); this._updateBanks(); return result; } /* * The Big Switch */ _access(off: byte, val?: byte) { let result; const writeMode = val !== undefined; const highNibble = off >> 4; switch (highNibble) { case 0x0: if (writeMode) { this._accessMMUSet(off, val); } else { result = this.io.ioSwitch(off); } break; case 0x1: if (writeMode) { this.io.ioSwitch(off, val); } else { result = this._accessStatus(off, val); } break; case 0x5: result = this._accessGraphics(off, val); break; case 0x7: result = this._accessIOUDisable(off, val); break; case 0x8: result = this._accessLangCard(off, val); break; default: result = this.io.ioSwitch(off, val); } return result; } public start() { this.lores1.start(); this.lores2.start(); return 0x00; } public end() { return 0xff; } public reset() { debug('reset'); this._initSwitches(); this._updateBanks(); this.vm.reset(); this.io.reset(); } public read(page: byte, off: byte) { return this._readPages[page].read(page, off); } public write(page: byte, off: byte, val: byte) { 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; } public get bank1() { return this._bank1; } public get readbsr() { return this._readbsr; } public get writebsr() { return this._writebsr; } public get auxread() { return this._auxRamRead; } public get auxwrite() { return this._auxRamWrite; } public get altzp() { return this._altzp; } public get _80store() { return this.__80store; } public get page2() { return this._page2; } public get hires() { return this._hires; } public get intcxrom() { return this._intcxrom; } public getState(): MMUState { return { bank1: this._bank1, readbsr: this._readbsr, writebsr: this._writebsr, prewrite: this._prewrite, intcxrom: this._intcxrom, slot3rom: this._slot3rom, intc8rom: this._intc8rom, auxRamRead: this._auxRamRead, auxRamWrite: this._auxRamWrite, altzp: this._altzp, _80store: this.__80store, page2: this._page2, hires: this._hires, mem00_01: [ this.mem00_01[0].getState(), this.mem00_01[1].getState(), ], mem02_03: [ this.mem02_03[0].getState(), this.mem02_03[1].getState(), ], mem0C_1F: [ this.mem0C_1F[0].getState(), this.mem0C_1F[1].getState(), ], mem60_BF: [ this.mem60_BF[0].getState(), this.mem60_BF[1].getState(), ], memD0_DF: [ this.memD0_DF[0].getState(), this.memD0_DF[1].getState(), this.memD0_DF[2].getState(), this.memD0_DF[3].getState(), this.memD0_DF[4].getState(), ], memE0_FF: [ this.memE0_FF[0].getState(), this.memE0_FF[1].getState(), this.memE0_FF[2].getState(), ], }; } public setState(state: MMUState) { this._readbsr = state.readbsr; this._writebsr = state.writebsr; this._bank1 = state.bank1; this._prewrite = state.prewrite; this._intcxrom = state.intcxrom; this._slot3rom = state.slot3rom; this._intc8rom = state.intc8rom; this._auxRamRead = state.auxRamRead; this._auxRamWrite = state.auxRamWrite; this._altzp = state.altzp; this.__80store = state._80store; this._page2 = state.page2; this._hires = state.hires; this.mem00_01[0].setState(state.mem00_01[0]); this.mem00_01[1].setState(state.mem00_01[1]); this.mem02_03[0].setState(state.mem02_03[0]); this.mem02_03[1].setState(state.mem02_03[1]); this.mem0C_1F[0].setState(state.mem0C_1F[0]); this.mem0C_1F[1].setState(state.mem0C_1F[1]); this.mem60_BF[0].setState(state.mem60_BF[0]); this.mem60_BF[1].setState(state.mem60_BF[1]); this.memD0_DF[0].setState(state.memD0_DF[0]); this.memD0_DF[1].setState(state.memD0_DF[1]); this.memD0_DF[2].setState(state.memD0_DF[2]); this.memD0_DF[3].setState(state.memD0_DF[3]); this.memD0_DF[4].setState(state.memD0_DF[4]); this.memE0_FF[0].setState(state.memE0_FF[0]); this.memE0_FF[1].setState(state.memE0_FF[1]); this.memE0_FF[2].setState(state.memE0_FF[2]); this._updateBanks(); } }