import { MOS6502, MOS6502State } from "../common/cpu/MOS6502"; import { Bus, BasicScanlineMachine, SavesState, AcceptsBIOS } from "../common/devices"; import { KeyFlags } from "../common/emu"; // TODO import { hex, lzgmini, stringToByteArray, RGBA, printFlags } from "../common/util"; // TODO: read prodos/ca65 header? const VM_BASE = 0x803; // where to JMP after pr#6 const LOAD_BASE = VM_BASE; const PGM_BASE = VM_BASE; const HDR_SIZE = PGM_BASE - LOAD_BASE; interface AppleIIStateBase { ram : Uint8Array; soundstate : number; auxRAMselected,writeinhibit : boolean; auxRAMbank : number; } interface AppleIIControlsState { inputs : Uint8Array; // unused? kbdlatch : number; } interface AppleIIState extends AppleIIStateBase, AppleIIControlsState { c : MOS6502State; grswitch : number; slots: SlotDevice[]; } interface SlotDevice extends Bus { readROM(address: number) : number; readConst(address: number) : number; } export class AppleII extends BasicScanlineMachine implements AcceptsBIOS { // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ cpuFrequency = 1022727; sampleRate = this.cpuFrequency; cpuCyclesPerLine = 65; cpuCyclesPerFrame = this.cpuCyclesPerLine * 262; canvasWidth = 280; numVisibleScanlines = 192; numTotalScanlines = 262; defaultROMSize = 0xbf00-0x803; // TODO ram = new Uint8Array(0x13000); // 64K + 16K LC RAM - 4K hardware + 12K ROM bios : Uint8Array; cpu = new MOS6502(); grdirty = new Array(0xc000 >> 7); grparams = {dirty:this.grdirty, grswitch:GR_TXMODE, mem:this.ram}; ap2disp; kbdlatch = 0; soundstate = 0; // language card switches auxRAMselected = false; auxRAMbank = 1; writeinhibit = true; // value to add when reading & writing each of these banks // bank 1 is E000-FFFF, bank 2 is D000-DFFF bank2rdoffset=0; bank2wroffset=0; // disk II slots : SlotDevice[] = new Array(8); // fake disk drive that loads program into RAM fakeDrive : SlotDevice = { readROM: (a) => { var pc = this.cpu.getPC(); if (pc >= 0xC600 && pc < 0xC700) { // We're reading code to EXECUTE. // Load the built program directly into memory, and "read" // a JMP directly to it. //console.log(`fakeDrive (EXEC): ${a.toString(16)}`); switch (a) { // JMP VM_BASE case 0: // SHOULD load program into RAM here, but have to do it // below instead. return 0; case 1: return VM_BASE&0xff; case 2: return (VM_BASE>>8)&0xff; default: return 0; } } else { // We're reading code, but not executing it. // This is probably the Monitor routine to identify whether // this slot is a Disk ][ drive, so... give it what it wants. //console.log(`fakeDrive (NOEX): ${a.toString(16)}`); switch (a) { case 0: // Actually, if we get here, we probably ARE being // executed. For some reason, the instruction at $C600 // gets read for execution, BEFORE the PC gets set to // the correct location. So we handle loading the program // into RAM and returning the JMP here, instead of above // where it would otherwise belong. if (this.rom) { console.log(`Loading program into Apple ][ RAM at \$${PGM_BASE.toString(16)}`); this.ram.set(this.rom.slice(HDR_SIZE), PGM_BASE); } return 0x4c; // JMP case 1: return 0x20; case 3: return 0x00; case 5: return 0x03; case 7: return 0x3c; default: return 0; } } }, readConst: (a) => { return 0; }, read: (a) => { return this.floatbus(); }, write: (a,v) => { } }; constructor() { super(); this.loadBIOS(new lzgmini().decode(stringToByteArray(atob(APPLEIIGO_LZG)))); this.connectCPUMemoryBus(this); // This line is inappropriate for real ROMs, but was there for // the APPLE][GO ROM, so keeping it only in the constructor, for // that special case (in case it really is important for this // address to be an RTS). this.bios[0xD39A - (0x10000 - this.bios.length)] = 0x60; // $d39a = RTS } saveState() : AppleIIState { // TODO: automagic return { c: this.cpu.saveState(), ram: this.ram.slice(), kbdlatch: this.kbdlatch, soundstate: this.soundstate, grswitch: this.grparams.grswitch, auxRAMselected: this.auxRAMselected, auxRAMbank: this.auxRAMbank, writeinhibit: this.writeinhibit, slots: this.slots.map((slot) => { return slot && slot['saveState'] && slot['saveState']() }), inputs: null }; } loadState(s:AppleIIState) { this.cpu.loadState(s.c); this.ram.set(s.ram); this.kbdlatch = s.kbdlatch; this.soundstate = s.soundstate; this.grparams.grswitch = s.grswitch; this.auxRAMselected = s.auxRAMselected; this.auxRAMbank = s.auxRAMbank; this.writeinhibit = s.writeinhibit; this.setupLanguageCardConstants(); for (var i=0; i>8) == 0xc6) break; } // get out of $c600 boot for (var i=0; i<2000000; i++) { this.cpu.advanceClock(); if ((this.cpu.getPC()>>8) < 0xc6) break; } } readConst(address: number): number { if (address < 0xc000) { return this.ram[address]; } else if (address >= 0xd000) { if (!this.auxRAMselected) return this.bios[address - (0x10000 - this.bios.length)]; else if (address >= 0xe000) return this.ram[address]; else return this.ram[address + this.bank2rdoffset]; } else if (address >= 0xc100 && address < 0xc800) { var slot = (address >> 8) & 7; return (this.slots[slot] && this.slots[slot].readConst(address & 0xff)) | 0; } else { return 0; } } read(address:number) : number { address &= 0xffff; if (address < 0xc000 || address >= 0xd000) { return this.readConst(address); } else if (address < 0xc100) { this.probe.logIORead(address, 0); // TODO: value var slot = (address >> 4) & 0x0f; switch (slot) { case 0: return this.kbdlatch; case 1: this.kbdlatch &= 0x7f; break; case 3: this.soundstate = this.soundstate ^ 1; break; case 5: if ((address & 0x0f) < 8) { // graphics if ((address & 1) != 0) this.grparams.grswitch |= 1 << ((address >> 1) & 0x07); else this.grparams.grswitch &= ~(1 << ((address >> 1) & 0x07)); } break; case 6: // tapein, joystick, buttons switch (address & 7) { // buttons (off) case 1: case 2: case 3: return this.floatbus() & 0x7f; // joystick case 4: case 5: return this.floatbus() | 0x80; default: return this.floatbus(); } case 7: // joy reset if (address == 0xc070) return this.floatbus() | 0x80; case 8: return this.doLanguageCardIO(address); case 9: case 10: case 11: case 12: case 13: case 14: case 15: return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0; } } else if (address >= 0xc100 && address < 0xc800) { var slot = (address >> 8) & 7; return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0; } return this.floatbus(); } write(address:number, val:number) : void { address &= 0xffff; val &= 0xff; if (address < 0xc000) { this.ram[address] = val; this.grdirty[address>>7] = 1; this.probe.logIOWrite(address, val); } else if (address < 0xc080) { this.read(address); // strobe address, discard result } else if (address < 0xc100) { var slot = (address >> 4) & 0x0f; this.slots[slot-8] && this.slots[slot-8].write(address & 0xf, val); } else if (address >= 0xd000 && !this.writeinhibit) { if (address >= 0xe000) this.ram[address] = val; else this.ram[address + this.bank2wroffset] = val; } } // http://www.deater.net/weave/vmwprod/megademo/vapor_lock.html // https://retrocomputing.stackexchange.com/questions/14012/what-is-dram-refresh-and-why-is-the-weird-apple-ii-video-memory-layout-affected // http://www.apple-iigs.info/doc/fichiers/TheappleIIcircuitdescription1.pdf // http://rich12345.tripod.com/aiivideo/softalk.html // https://github.com/MiSTer-devel/Apple-II_MiSTer/blob/master/rtl/timing_generator.vhd floatbus() : number { var fcyc = this.frameCycles; var yline = Math.floor(fcyc / 65); var xcyc = Math.floor(fcyc % 65); var addr = this.ap2disp.getAddressForScanline(yline); return this.readConst(addr + xcyc); } connectVideo(pixels:Uint32Array) { super.connectVideo(pixels); this.ap2disp = this.pixels && new Apple2Display(this.pixels, this.grparams); } startScanline() { } drawScanline() { // TODO: draw scanline via ap2disp } advanceFrame(trap) : number { var clocks = super.advanceFrame(trap); this.ap2disp && this.ap2disp.updateScreen(); return clocks; } advanceCPU() { this.audio.feedSample(this.soundstate, 1); return super.advanceCPU(); } setKeyInput(key:number, code:number, flags:number) : void { //console.log(`setKeyInput: ${key} ${code} ${flags}`); if (flags & KeyFlags.KeyDown) { code = 0; switch (key) { case 16: case 17: case 18: break; // ignore shift/ctrl/etc case 8: code=8; // left if (flags & KeyFlags.Shift) { // (possibly) soft reset this.cpu.reset(); return; } break; case 13: code=13; break; // return case 27: code=27; break; // escape case 37: code=8; break; // left case 39: code=21; break; // right case 38: code=11; break; // up case 40: code=10; break; // down case 48: if (flags & KeyFlags.Shift) code = 0x29; break; // ) case 49: if (flags & KeyFlags.Shift) code = 0x21; break; // ! case 50: if (flags & KeyFlags.Shift) code = 0x40; break; // @ case 51: if (flags & KeyFlags.Shift) code = 0x23; break; // # case 52: if (flags & KeyFlags.Shift) code = 0x24; break; // $ case 53: if (flags & KeyFlags.Shift) code = 0x25; break; // % case 54: if (flags & KeyFlags.Shift) code = 0x5e; break; // ^ case 55: if (flags & KeyFlags.Shift) code = 0x26; break; // & case 56: if (flags & KeyFlags.Shift) code = 0x2a; break; // * case 57: if (flags & KeyFlags.Shift) code = 0x28; break; // ( case 61: code = (flags & KeyFlags.Shift) ? 0x2b : 0x3d; break; // + case 173: code = (flags & KeyFlags.Shift) ? 0x5f : 0x2d; break; // _ case 59: code = (flags & KeyFlags.Shift) ? 0x3a : 0x3b; break; case 188: code = (flags & KeyFlags.Shift) ? 0x3c : 0x2c; break; case 190: code = (flags & KeyFlags.Shift) ? 0x3e : 0x2e; break; case 191: code = (flags & KeyFlags.Shift) ? 0x3f : 0x2f; break; case 222: code = (flags & KeyFlags.Shift) ? 0x22 : 0x27; break; default: code = key; // convert to uppercase for Apple ][ if (code >= 0x61 && code <= 0x7a) code -= 32; // convert to control codes if Ctrl pressed if (code >= 65 && code < 65+26) { if (flags & KeyFlags.Ctrl) code -= 64; // ctrl } } if (code) { this.kbdlatch = (code | 0x80) & 0xff; } } } doLanguageCardIO(address:number) { switch (address & 0x0f) { // Select aux RAM bank 2, write protected. case 0x0: case 0x4: this.auxRAMselected = true; this.auxRAMbank = 2; this.writeinhibit = true; break; // Select ROM, write enable aux RAM bank 2. case 0x1: case 0x5: this.auxRAMselected = false; this.auxRAMbank = 2; this.writeinhibit = false; break; // Select ROM, write protect aux RAM (either bank). case 0x2: case 0x6: case 0xA: case 0xE: this.auxRAMselected = false; this.writeinhibit = true; break; // Select aux RAM bank 2, write enabled. case 0x3: case 0x7: this.auxRAMselected = true; this.auxRAMbank = 2; this.writeinhibit = false; break; // Select aux RAM bank 1, write protected. case 0x8: case 0xC: this.auxRAMselected = true; this.auxRAMbank = 1; this.writeinhibit = true; break; // Select ROM, write enable aux RAM bank 1. case 0x9: case 0xD: this.auxRAMselected = false; this.auxRAMbank = 1; this.writeinhibit = false; break; // Select aux RAM bank 1, write enabled. case 0xB: case 0xF: this.auxRAMselected = true; this.auxRAMbank = 1; this.writeinhibit = false; break; } this.setupLanguageCardConstants(); return this.floatbus(); } setupLanguageCardConstants() { // reset language card constants if (this.auxRAMbank == 2) this.bank2rdoffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff else this.bank2rdoffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff if (this.auxRAMbank == 2) this.bank2wroffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff else this.bank2wroffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff } getDebugCategories() { return ['CPU','Stack','I/O','Disk']; } getDebugInfo(category:string, state:AppleIIState) { switch (category) { case 'I/O': return "AUX RAM Bank: " + state.auxRAMbank + "\nAUX RAM Select: " + state.auxRAMselected + "\nAUX RAM Write: " + !state.writeinhibit + "\n\nGR Switches: " + printFlags(state.grswitch, ["Graphics","Mixed","Page2","Hires"], false) + "\n"; case 'Disk': return (this.slots[6] && this.slots[6]['toLongString'] && this.slots[6]['toLongString']()) || "\n"; } } } const GR_TXMODE = 1; const GR_MIXMODE = 2; const GR_PAGE1 = 4; const GR_HIRES = 8; type AppleGRParams = {dirty:boolean[], grswitch:number, mem:Uint8Array}; var Apple2Display = function(pixels : Uint32Array, apple : AppleGRParams) { var XSIZE = 280; var YSIZE = 192; var PIXELON = 0xffffffff; var PIXELOFF = 0xff000000; var oldgrmode = -1; var textbuf = new Array(40*24); const flashInterval = 500; // https://mrob.com/pub/xapple2/colors.html const loresColor = [ RGBA(0, 0, 0), RGBA(227, 30, 96), RGBA(96, 78, 189), RGBA(255, 68, 253), RGBA(0, 163, 96), RGBA(156, 156, 156), RGBA(20, 207, 253), RGBA(208, 195, 255), RGBA(96, 114, 3), RGBA(255, 106, 60), RGBA(156, 156, 156), RGBA(255, 160, 208), RGBA(20, 245, 60), RGBA(208, 221, 141), RGBA(114, 255, 208), RGBA(255, 255, 255) ]; const text_lut = [ 0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, 0x028, 0x0a8, 0x128, 0x1a8, 0x228, 0x2a8, 0x328, 0x3a8, 0x050, 0x0d0, 0x150, 0x1d0, 0x250, 0x2d0, 0x350, 0x3d0 ]; const hires_lut = [ 0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x0080, 0x0480, 0x0880, 0x0c80, 0x1080, 0x1480, 0x1880, 0x1c80, 0x0100, 0x0500, 0x0900, 0x0d00, 0x1100, 0x1500, 0x1900, 0x1d00, 0x0180, 0x0580, 0x0980, 0x0d80, 0x1180, 0x1580, 0x1980, 0x1d80, 0x0200, 0x0600, 0x0a00, 0x0e00, 0x1200, 0x1600, 0x1a00, 0x1e00, 0x0280, 0x0680, 0x0a80, 0x0e80, 0x1280, 0x1680, 0x1a80, 0x1e80, 0x0300, 0x0700, 0x0b00, 0x0f00, 0x1300, 0x1700, 0x1b00, 0x1f00, 0x0380, 0x0780, 0x0b80, 0x0f80, 0x1380, 0x1780, 0x1b80, 0x1f80, 0x0028, 0x0428, 0x0828, 0x0c28, 0x1028, 0x1428, 0x1828, 0x1c28, 0x00a8, 0x04a8, 0x08a8, 0x0ca8, 0x10a8, 0x14a8, 0x18a8, 0x1ca8, 0x0128, 0x0528, 0x0928, 0x0d28, 0x1128, 0x1528, 0x1928, 0x1d28, 0x01a8, 0x05a8, 0x09a8, 0x0da8, 0x11a8, 0x15a8, 0x19a8, 0x1da8, 0x0228, 0x0628, 0x0a28, 0x0e28, 0x1228, 0x1628, 0x1a28, 0x1e28, 0x02a8, 0x06a8, 0x0aa8, 0x0ea8, 0x12a8, 0x16a8, 0x1aa8, 0x1ea8, 0x0328, 0x0728, 0x0b28, 0x0f28, 0x1328, 0x1728, 0x1b28, 0x1f28, 0x03a8, 0x07a8, 0x0ba8, 0x0fa8, 0x13a8, 0x17a8, 0x1ba8, 0x1fa8, 0x0050, 0x0450, 0x0850, 0x0c50, 0x1050, 0x1450, 0x1850, 0x1c50, 0x00d0, 0x04d0, 0x08d0, 0x0cd0, 0x10d0, 0x14d0, 0x18d0, 0x1cd0, 0x0150, 0x0550, 0x0950, 0x0d50, 0x1150, 0x1550, 0x1950, 0x1d50, 0x01d0, 0x05d0, 0x09d0, 0x0dd0, 0x11d0, 0x15d0, 0x19d0, 0x1dd0, 0x0250, 0x0650, 0x0a50, 0x0e50, 0x1250, 0x1650, 0x1a50, 0x1e50, 0x02d0, 0x06d0, 0x0ad0, 0x0ed0, 0x12d0, 0x16d0, 0x1ad0, 0x1ed0, 0x0350, 0x0750, 0x0b50, 0x0f50, 0x1350, 0x1750, 0x1b50, 0x1f50, 0x03d0, 0x07d0, 0x0bd0, 0x0fd0, 0x13d0, 0x17d0, 0x1bd0, 0x1fd0, // just for floating bus, y >= 192 0x0078, 0x0478, 0x0878, 0x0c78, 0x1078, 0x1478, 0x1878, 0x1c78, 0x00f8, 0x04f8, 0x08f8, 0x0cf8, 0x10f8, 0x14f8, 0x18f8, 0x1cf8, 0x0178, 0x0578, 0x0978, 0x0d78, 0x1178, 0x1578, 0x1978, 0x1d78, 0x01f8, 0x05f8, 0x09f8, 0x0df8, 0x11f8, 0x15f8, 0x19f8, 0x1df8, 0x0278, 0x0678, 0x0a78, 0x0e78, 0x1278, 0x1678, 0x1a78, 0x1e78, 0x02f8, 0x06f8, 0x0af8, 0x0ef8, 0x12f8, 0x16f8, 0x1af8, 0x1ef8, 0x0378, 0x0778, 0x0b78, 0x0f78, 0x1378, 0x1778, 0x1b78, 0x1f78, 0x03f8, 0x07f8, 0x0bf8, 0x0ff8, 0x13f8, 0x17f8, 0x1bf8, 0x1ff8, 0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, ]; var colors_lut; /** * This function makes the color lookup table for hires mode. * We make a table of 1024 * 2 * 7 entries. * Why? Because we assume each color byte has 10 bits * (8 real bits + 1 on each side) and we need different colors * for odd and even addresses (2) and each byte displays 7 pixels. */ { colors_lut = new Array(256*4*2*7); var i,j; var c1,c2,c3 = 15; var base = 0; // go thru odd and even for (j=0; j<2; j++) { // go thru 1024 values for (var b1=0; b1<1024; b1++) { // see if the hi bit is set if ((b1 & 0x80) == 0) { c1 = 3; c2 = 12; // purple & green } else { c1 = 6; c2 = 9; // blue & orange } // make a value consisting of: // the 8th bit, then bits 0-7, then the 9th bit var b = ((b1 & 0x100) >> 8) | ((b1 & 0x7f) << 1) | ((b1 & 0x200) >> 1); // go through each pixel for (i=0; i<7; i++) { var c; // is this pixel lit? if (((2<> 4]; for (i=0; i<4; i++) { pixels[base] = pixels[base+1] = pixels[base+2] = pixels[base+3] = pixels[base+4] = pixels[base+5] = pixels[base+6] = c; base += XSIZE; } } function drawTextChar(x, y, b, invert) { var base = (y<<3)*XSIZE + x*7; // (x<<2) + (x<<1) + x var on,off; if (invert) { on = PIXELOFF; off = PIXELON; } else { on = PIXELON; off = PIXELOFF; } for (var yy=0; yy<8; yy++) { var chr = apple2_charset[(b<<3)+yy]; pixels[base] = ((chr & 64) > 0)?on:off; pixels[base+1] = ((chr & 32) > 0)?on:off; pixels[base+2] = ((chr & 16) > 0)?on:off; pixels[base+3] = ((chr & 8) > 0)?on:off; pixels[base+4] = ((chr & 4) > 0)?on:off; pixels[base+5] = ((chr & 2) > 0)?on:off; pixels[base+6] = ((chr & 1) > 0)?on:off; base += XSIZE; } } this.getAddressForScanline = function(y:number) : number { var base = hires_lut[y]; if ((apple.grswitch & GR_HIRES) && (y < 160 || !(apple.grswitch & GR_MIXMODE))) base = base | ((apple.grswitch & GR_PAGE1) ? 0x4000 : 0x2000); else base = (base & 0x3ff) | ((apple.grswitch & GR_PAGE1) ? 0x800 : 0x400); return base; } function drawHiresLines(y, maxy) { var yb = y*XSIZE; for (; y < maxy; y++) { var base = hires_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x4000 : 0x2000); if (!apple.dirty[base >> 7]) { yb += XSIZE; continue; } var c1, c2; var b = 0; var b1 = apple.mem[base] & 0xff; for (var x1=0; x1<20; x1++) { var b2 = apple.mem[base+1] & 0xff; var b3 = apple.mem[base+2] & 0xff; var d1 = (((b&0x40)<<2) | b1 | b2<<9) & 0x3ff; for (var i=0; i<7; i++) pixels[yb+i] = colors_lut[d1*7+i]; var d2 = (((b1&0x40)<<2) | b2 | b3<<9) & 0x3ff; for (var i=0; i<7; i++) pixels[yb+7+i] = colors_lut[d2*7+7168+i]; yb += 14; base += 2; b = b2; b1 = b3; } } } function drawLoresLine(y) { // get the base address of this line var base = text_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400); // if (!dirty[base >> 7]) // return; for (var x=0; x<40; x++) { var b = apple.mem[base+x] & 0xff; // if the char. changed, draw it if (b != textbuf[y*40+x]) { drawLoresChar(x, y, b); textbuf[y*40+x] = b; } } } function drawTextLine(y, flash) { // get the base address of this line var base = text_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400); // if (!dirty[base >> 7]) // return; for (var x=0; x<40; x++) { var b = apple.mem[base+x] & 0xff; var invert; // invert flash characters 1/2 of the time if (b >= 0x80) { invert = false; } else if (b >= 0x40) { invert = flash; if (flash) b -= 0x40; else b += 0x40; } else invert = true; // if the char. changed, draw it if (b != textbuf[y*40+x]) { drawTextChar(x, y, b & 0x7f, invert); textbuf[y*40+x] = b; } } } this.updateScreen = function(totalrepaint) { var y; var flash = (new Date().getTime() % (flashInterval<<1)) > flashInterval; // if graphics mode changed, repaint whole screen if (apple.grswitch != oldgrmode) { oldgrmode = apple.grswitch; totalrepaint = true; } if (totalrepaint) { // clear textbuf if in text mode if ((apple.grswitch & GR_TXMODE) != 0 || (apple.grswitch & GR_MIXMODE) != 0) { for (y=0; y<24; y++) for (var x=0; x<40; x++) textbuf[y*40+x] = -1; } for (var i=0; i { emu : AppleII; track_data : Uint8Array; constructor(emu : AppleII, image : Uint8Array) { super(); this.emu = emu; this.data = new Array(NUM_TRACKS); for (var i=0; i>1]; else this.track_data = null; } toLongString() { return "Track: " + (this.track / 2) + "\nOffset: " + (this.track_index) + "\nMode: " + (this.read_mode ? "READ" : "WRITE") + "\nMotor: " + this.motor + "\nData: " + (this.track_data ? hex(this.track_data[this.track_index]) : '-') + "\n"; } read_latch() : number { this.track_index = (this.track_index + 1) % TRACK_SIZE; if (this.track_data) { return (this.track_data[this.track_index] & 0xff); } else return this.emu.floatbus() | 0x80; } write_latch(value: number) { this.track_index = (this.track_index + 1) % TRACK_SIZE; if (this.track_data != null) this.track_data[this.track_index] = value; } readROM(address) { return DISKII_PROM[address]; } readConst(address) { return DISKII_PROM[address]; } read(address) { return this.doIO(address, 0); } write(address, value) { this.doIO(address, value); } doIO(address, value) : number { switch (address & 0x0f) { /* * Turn motor phases 0 to 3 on. Turning on the previous phase + 1 * increments the track position, turning on the previous phase - 1 * decrements the track position. In this scheme phase 0 and 3 are * considered to be adjacent. The previous phase number can be * computed as the track number % 4. */ case 0x1: case 0x3: case 0x5: case 0x7: var phase, lastphase, new_track; new_track = this.track; phase = (address >> 1) & 3; // if new phase is even and current phase is odd if (phase == ((new_track - 1) & 3)) { if (new_track > 0) new_track--; } else if (phase == ((new_track + 1) & 3)) { if (new_track < NUM_TRACKS*2-1) new_track++; } if ((new_track & 1) == 0) { this.track_data = this.data[new_track>>1]; console.log('track', new_track/2); } else this.track_data = null; this.track = new_track; break; /* * Turn drive motor off. */ case 0x8: this.motor = false; break; /* * Turn drive motor on. */ case 0x9: this.motor = true; break; /* * Select drive 1. */ case 0xa: //drive = 0; break; /* * Select drive 2. */ case 0xb: //drive = 1; break; /* * Select write mode. */ case 0xf: this.read_mode = false; /* * Read a disk byte if read mode is active. */ case 0xC: if (this.read_mode) return this.read_latch(); break; /* * Select read mode and read the write protect status. */ case 0xE: this.read_mode = true; /* * Write a disk byte if write mode is active and the disk is not * write protected. */ case 0xD: if (value >= 0 && !this.read_mode && !this.write_protect) this.write_latch(value); /* * Read the write protect status only. */ return this.write_protect ? 0x80 : 0x00; } return this.emu.floatbus(); } } /* --------------- TRACK CONVERSION ROUTINES ---------------------- */ /* * Normal byte (lower six bits only) -> disk byte translation table. */ const byte_translation = [ 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3, 0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff ]; /* * Sector skewing table. */ const skewing_table = [ 0,7,14,6,13,5,12,4,11,3,10,2,9,1,8,15 ]; /* * Encode a 256-byte sector as SECTOR_SIZE disk bytes as follows: * * 14 sync bytes * 3 address header bytes * 8 address block bytes * 3 address trailer bytes * 6 sync bytes * 3 data header bytes * 343 data block bytes * 3 data trailer bytes */ function nibblizeSector(vol, trk, sector, inn, in_ofs, out, i) { var loop, checksum, prev_value, value; var sector_buffer = new Uint8Array(258); value = 0; /* * Step 1: write 6 sync bytes (0xff's). Normally these would be * written as 10-bit bytes with two extra zero bits, but for the * purpose of emulation normal 8-bit bytes will do, since the * emulated drive will always be in sync. */ for (loop = 0; loop < 14; loop++) out[i++] = 0xff; /* * Step 2: write the 3-byte address header (0xd5 0xaa 0x96). */ out[i++] = 0xd5; out[i++] = 0xaa; out[i++] = 0x96; /* * Step 3: write the address block. Use 4-and-4 encoding to convert * the volume, track and sector and checksum into 2 disk bytes each. * The checksum is a simple exclusive OR of the first three values. */ out[i++] = ((vol >> 1) | 0xaa); out[i++] = (vol | 0xaa); checksum = vol; out[i++] = ((trk >> 1) | 0xaa); out[i++] = (trk | 0xaa); checksum ^= trk; out[i++] = ((sector >> 1) | 0xaa); out[i++] = (sector | 0xaa); checksum ^= sector; out[i++] = ((checksum >> 1) | 0xaa); out[i++] = (checksum | 0xaa); /* * Step 4: write the 3-byte address trailer (0xde 0xaa 0xeb). */ out[i++] = (0xde); out[i++] = (0xaa); out[i++] = (0xeb); /* * Step 5: write another 6 sync bytes. */ for (loop = 0; loop < 6; loop++) out[i++] = (0xff); /* * Step 6: write the 3-byte data header. */ out[i++] = (0xd5); out[i++] = (0xaa); out[i++] = (0xad); /* * Step 7: read the next 256-byte sector from the old disk image file, * and add two zero bytes to bring the number of bytes up to a multiple * of 3. */ for (loop = 0; loop < 256; loop++) sector_buffer[loop] = inn[loop + in_ofs] & 0xff; sector_buffer[256] = 0; sector_buffer[257] = 0; /* * Step 8: write the first 86 disk bytes of the data block, which * encodes the bottom two bits of each sector byte into six-bit * values as follows: * * disk byte n, bit 0 = sector byte n, bit 1 * disk byte n, bit 1 = sector byte n, bit 0 * disk byte n, bit 2 = sector byte n + 86, bit 1 * disk byte n, bit 3 = sector byte n + 86, bit 0 * disk byte n, bit 4 = sector byte n + 172, bit 1 * disk byte n, bit 5 = sector byte n + 172, bit 0 * * The scheme allows each pair of bits to be shifted to the right out * of the disk byte, then shifted to the left into the sector byte. * * Before the 6-bit value is translated to a disk byte, it is exclusive * ORed with the previous 6-bit value, hence the values written are * really a running checksum. */ prev_value = 0; for (loop = 0; loop < 86; loop++) { value = (sector_buffer[loop] & 0x01) << 1; value |= (sector_buffer[loop] & 0x02) >> 1; value |= (sector_buffer[loop + 86] & 0x01) << 3; value |= (sector_buffer[loop + 86] & 0x02) << 1; value |= (sector_buffer[loop + 172] & 0x01) << 5; value |= (sector_buffer[loop + 172] & 0x02) << 3; out[i++] = (byte_translation[value ^ prev_value]); prev_value = value; } /* * Step 9: write the last 256 disk bytes of the data block, which * encodes the top six bits of each sector byte. Again, each value * is exclusive ORed with the previous value to create a running * checksum (the first value is exclusive ORed with the last value of * the previous step). */ for (loop = 0; loop < 256; loop++) { value = (sector_buffer[loop] >> 2); out[i++] = (byte_translation[value ^ prev_value]); prev_value = value; } /* * Step 10: write the last value as the checksum. */ out[i++] = (byte_translation[value]); /* * Step 11: write the 3-byte data trailer. */ out[i++] = (0xde); out[i++] = (0xaa); out[i++] = (0xeb); } function nibblizeTrack(vol, trk, inn) { var out = new Uint8Array(TRACK_SIZE); var out_pos = 0; for (var sector = 0; sector < 16; sector++) { nibblizeSector(vol, trk, sector, inn, skewing_table[sector] << 8, out, out_pos); out_pos += SECTOR_SIZE; } while (out_pos < TRACK_SIZE) out[out_pos++] = (0xff); return out; }