/** * @fileoverview This file implements the C1Pjs DiskController component. * @author Jeff Parsons * @copyright Jeff Parsons 2012-2016 * * This file is part of PCjs, a computer emulation software project at . * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen * that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ "use strict"; if (NODE) { var str = require("../../shared/lib/strlib"); var web = require("../../shared/lib/weblib"); var Component = require("../../shared/lib/component"); } /** * C1PDiskController(parmsDC) * * The C1PDiskController component has no component-specific parameters. * * This component is being built to supplement a C1P (aka SuperBoard II) Model 600 * single-board computer with the addition of a 610 Accessory Board, which included: * * MC6820 PIA (Peripheral Interface Adapter at $C000-$C003, decoded at $C000-$C00F) * MC6850 ACIA (Asynchronous Communications Interface Adapter at $C010-$C011, decoded at $C010-$C01F) * * From "OSI C1P Technical Report" p.4 regarding the 610 Accessory Board: * * "This board holds up to 24K of additional RAM memory, a dual mini-floppy disk controller, * a BUS expansion facility to Model 620 BUS adapter, and switching circuitry to route the * 600 board's serial interface to both the modem and printer as well as an audio cassette. * Thus, a fully expanded Challenger lP system can have BASIC-in-ROM, 32K of RAM memory, * dual mini-floppies, cassette, printer, modem, and full BUS expansion capability to the OSI * 48 line BUS through which over 40 accessories can be added (A/D, D/A, voice, I/O, more memory, * etc.)." * * On p.20, the Report says that the 610 Accessory Board contains: * * - Up to 24K of RAM * - Dual mini-floppy controller * - Real Time Clock (although elsewhere the Report says this is disabled by default) * - Expansion interface to a model 620 BUS adapter * * On p.21, the Report also says: * * "The dual mini-floppy interface is designed after Ohio Scientific's extremely popular * and successful 470 floppy disk controller. This floppy disk controller and encoding * technique has been field proven for several years in thousands of floppy disks and is * believed to be one of the most reliable floppy disk configurations in existence. Although * the Challenger lP product line is new, it has the advantage of the experience of a company * which has been building high performance microcomputers for several years." * * From "PEEK 65" Vol.2 No.3 March 1981, p.9: * * "The 470 board wired as a floppy disk controller contains two different interfaces: * a PIA and an ACIA. The PIA A and B ports are used in control circuits: raise and lower * the head, detect drive ready, detect sector hole, clear error faults, etc. The ACIA is * the interface over which the data actually travels. Typical operation is to drop the head, * reset the ACIA, wait for the index hole to come around, activate the read or write circuit, * then read or write characters through the ACIA." * * 470 Board Addressing * -------------------- * * Address Read Write * ------- ---- ----- * C000 PIA: PA0 thru PA7 PIA: PA0 thru PA7 or DDA0 thru DDA7 * C001 PIA: Port A Ctrl PIA: Port A Control * C002 PIA: PB0 thru PB7 PIA: PB0 thru PB7 or DDB0 thru DDB7 * C003 PIA: Port B Ctrl PIA: Port B Control * C010 ACIA: Status Reg. ACIA: Control Register * C011 ACIA: Data Path ACIA: Data Path * C020 Clear Real Time Clock Clear Real Time Clock * (Reset) ($FF returned) (Reset) (Data Ignored) * * PIA Data Register A Layout: * * PA7 PA6 PA5 PA4 PA3 PA2 PA1 PA0 * --- --- --- ---- --- --- --- ---- * IHD | SD2 | WP | RDY2 | SHD | FD | TZD | RDY1 * (In) (Out) (In) (In) (In) (In) (In) (In) * * PIA Data Register B Layout: * * PB7 PB6 PB5 PB4 PB3 PB2 PB1 PB0 * --- --- --- --- --- --- --- --- * HLD | LCS | SD1 | FR | ST | STI | EE | WE * (Out) (Out) (Out) (Out) (Out) (Out) (Out) (Out) * * PIA Data Register A Lines PIA Data Register B Lines * ------------------------- ------------------------- * IHD - Index Hole Detect HLD - Head Load * SD2 - Select Drive 2 (Drive B) LCS - Low Current Select * WP - Write Protected SD1 - Select Drive 1 * RDY2- Drive 2 Ready FR - Fault Reset * SHD - Sector Hole Detect ST - Step * FD - Fault Detected STI - Step In * TZD - Track Zero Detected EE - Enable Erase * RDY1- Drive 1 Ready WE - Write Enable * * NOTE: The PIA bit assignments above agree with those described, albeit somewhat less clearly, * in http://www.osiweb.org/osiweb/misc/osi-hardware.txt, under "Model 475 Floppy disk system with * 470 Controller board". * * There is apparently significant overlap with another OSI board: the Model 505 CPU Board * used in C4P/MF systems. According to http://www.osiweb.org/osiweb/misc/osi-hardware.txt, it * contained: * * CPU board w/ ROM, ACIA, Floppy Disk I/O, Real Time Clock * ROM $FDxx, $FExx, $FFxx * Floppy disk interface: 6820 PIA at $C000, 6850 ACIA at $C010 [Original says "6850 PIA"] * ACIA 6850 at $FC00 for RS-232 serial I/O. Baud jumpers for 75,150,300,600,1200,2400,4800,9600 * Disk PIA $C0xx CB1 connected to 400mSEC (2.5/sec) clock divided from system clock (RTC) * Home security - PIA $F700-F703 * * Disk Formats (from http://osi.marks-lab.com/files/winOSI/old-source-V1.2/Disk_io.cpp): * * 5.25" disk, 40 tracks, 8 sectors/track, 256 bytes/sector, 11 bits/byte (8E1) = 80K/disk. * * NOTE: 8E1 refers to "8 data bits, even parity, 1 stop bit," plus an implied start bit. * * OSI uses 8E1 to give a max unformatted capacity of 2272 bytes per track (see below). * However other bit encodings (8N1) could give up to 2500 bytes/track. * * NOTE: 8N1 refers to "8 data bits, no parity, 1 stop bit," plus an implied start bit. * * The standard speed for 5.25" drives is 300rpm. Thus one rotation of the disk is 200ms. * Stated baud-rate is 125k or 125000 bits/sec and one serial byte is 11 bits (1 start, * 8 data, 1 parity, 1 stop). So the theoretical absolute maximum storage per track is * (125000 x 0.2) / 11 = 2272 bytes or 8.8 pages. * * OS-65D loses a bit more because it doesn't write until 10ms after the index pulse, so * (125000 x 0.19) / 11 = 2159 bytes or 8.4 pages and this doesn't even allow for the length * of the index pulse (a few milliseconds?) and the speed variation between drives. * * 8" disk, 77 tracks, 12 sectors/track, 256 bytes/sector, 11 bits/byte (8E1) = 231K/disk. * OSI uses 8E1 to give a max unformatted capacity of 3772 bytes/track (see below). * However other bit encodings (8N1) could give up to 3900 bytes/track. * * The standard speed for 8" drives is 360rpm. Thus one rotation of the disk is 166.6ms. * Stated baud rate is 250K or 250000 bits/sec and one serial byte is 11 bits (1 start, * 8 data, 1 parity, 1 stop). So the theoretical absolute maximum storage per track is * (250000 x 0.166 ) / 11 = 3772 or 14.7 pages. * * OS-65D loses a bit more because it doesn't write until 10 mS after the index pulse, so * (250000 x 0.156) / 11 = 3545 bytes or 13.8 pages and this doesn't even allow for the length * of the index pulse (a few milliseconds?) and the speed variation between drives. * * Track 0 Format * -------------- * (10ms delay after index hole) * 0,1 load address of the track in hi,lo form * 2 page count of how much data is written on track 0. * 3+ sector data * * Track N Format (N > 0) * ---------------------- * (10ms delay after index hole) * 0,1 2-byte start code $43, $57 * 2 BCD track number * 3 track type code (always $58) * 4+ sector data * * Sector Format (5.25" disks) * --------------------------- * There can be any mixture of various length sectors. The total page count can not * exceed 8 pages (8*256) if more than one sector is on a track. Each sector is written * in the following format: * * previous sector length (4 if none before) times 800 microseconds of delay * sector start code $76 * sector number in binary * sector length (#pages) in binary * sector data * (end of sector mark? $47, $53? MDS) * * Directory Format * ---------------- * 2 sectors (1 & 2) on track 12 hold the directory information. * Each entry requires 8 bytes. There are a total of 64 entries. The entries are * formatted as follows: * * 0-5 ASCII 6 character filename * 6 BCD first track of file * 7 BCD Last track of file * * So far, all the 5.25" disk images I've seen are 92160 bytes, regardless whether they have an * .IMG or .65D extension. If we divide that total by 40 (tracks/disk), we get 2304 (bytes/track). * Divide 2304 by 256 (bytes/page) and we get 9 pages/track. Presumably a fixed 9 pages was chosen * to yield a consistent track size across the entire image, while also allowing room for all the * metadata that's typically present on a track as well. As explained above, the upper limit * on data per track (both sector data and metadata) is 8.8 pages in theory, or 8.4 pages in practice. * * @constructor * @extends Component */ function C1PDiskController(parmsDC) { Component.call(this, "C1PDiskController", parmsDC); this.flags.powered = false; /* * Our DiskController simulates the combination of an MC6820 PIA and an MC6850 ACIA. * This image of an OSI 470 Controller Board (http://osi.marks-lab.com/boards/images/OSI470.jpg) * shows that the chips actually used were MC68B21P and MC68B50P. * * We start with definitions for the MC6820 PIA. */ this.PORT_PDA = 0; // PIA Peripheral Data Register A this.PORT_DDA = 0; // PIA Data Direction Register A (DDA shares the same register offset as PDA) this.PORT_CRA = 1; // PIA Control Register A this.PORT_PDB = 2; // PIA Peripheral Data Register B this.PORT_DDB = 2; // PIA Data Direction Register B (DDB shares the same register offset as PDB) this.PORT_CRB = 3; // PIA Control Register B this.CR_IRQ1 = 0x80; // IRQ1 this.CR_IRQ2 = 0x40; // IRQ2 // this.CR_C2_OUT = 0x20; // C2 is designated an output // this.CR_C2_CTRL = 0x18; // C2 Control (00 and 10 mask IRQ2, 01 and 11 pass IRQ2 through to the CPU) this.CR_PD_SEL = 0x04; // set to select PD (PDA or PDB), clear to select DD (DDA or DDB) // this.CR_C1_CTRL = 0x03; // C1 Control (00 and 10 mask IRQ1, 01 and 11 pass IRQ1 through to the CPU) /* * The PDA bits have the following hard-wired connections in the OSI Floppy Disk Controller. * Each line marked INPUT should have its corresponding Data Direction bit clear (0), and each line * marked OUTPUT should have its Data Direction bit set (1); however, we do not currently verify that * the Data Direction bits are actually initialized to match these specs (and in fact, in the case * of PDA_SD2, they may not be). */ this.PDA_RDY1 = 0x01; // INPUT: 0 = Drive 1 Ready this.PDA_TZD = 0x02; // INPUT: 0 = Track Zero Detected this.PDA_FD = 0x04; // INPUT: 0 = Fault Detected this.PDA_SHD = 0x08; // INPUT: 0 = Sector Hole Detect this.PDA_RDY2 = 0x10; // INPUT: 0 = Drive 2 Ready this.PDA_WP = 0x20; // INPUT: 0 = Write Protected this.PDA_SD2 = 0x40; // OUTPUT: 0 = Select Drive 2 (Drive B) this.PDA_IHD = 0x80; // INPUT: 0 = Index Hole Detect // this.PDB_WE = 0x01; // OUTPUT: 0 = Write Enable // this.PDB_EE = 0x02; // OUTPUT: 0 = Erase Enable (set to 1) this.PDB_STI = 0x04; // OUTPUT: 0 = Step In (away from track 0) this.PDB_ST = 0x08; // OUTPUT: 0 = Step (on 1-to-0 transition) // this.PDB_FR = 0x10; // OUTPUT: 0 = Fault Reset (set to 1) this.PDB_SD1 = 0x20; // OUTPUT: 0 = Select Drive 1 // this.PDB_LCS = 0x40; // OUTPUT: 0 = Low Current Select (set to 1) // this.PDB_HLD = 0x80; // OUTPUT: 0 = Head Load (head on disk) /* * Next, definitions for the MC6850 ACIA. * * For reference, here are all the possible CTRL_WSEL (Word Select) values: * * 000 0x00 7 bits, even parity, 2 stop bits * 001 0x04 7 bits, odd parity, 2 stop bits * 010 0x08 7 bits, even parity, 1 stop bit * 011 0x0C 7 bits, odd parity, 1 stop bit * 100 0x10 8 bits, 2 stop bits * 101 0x14 8 bits, 1 stop bit * 110 0x18 8 bits, even parity, 1 stop bit * 111 0x1C 8 bits, odd parity, 1 stop bit * * And here are all the possible CTRL_TCTL (Transmit Control) values: * * 00 0x00 RTS=Low, Transmitting Interrupt Disabled * 01 0x20 RTS=Low, Transmitting Interrupt Enabled * 10 0x40 RTS=High, Transmitting Interrupt Disabled * 11 0x60 RTS=Low, Transmits a Break level on the Transmit Data Output; Transmitting Interrupt Disabled */ this.PORT_CTRL = 0x10; // ACIA Control Register (WRITE-only) this.PORT_STAT = 0x10; // ACIA Status Register (READ-only) this.PORT_DATA = 0x11; // ACIA Data Register (Transmit Data Register on WRITE, Receive Data Register on READ) this.CTRL_CDIV = 0x03; // Counter Divide (CR1,CR0) [OSI sets both, performing a "Master Reset", then immediately clears both, for a divide ratio of 1] // this.CTRL_WSEL = 0x1C; // Word Select (CR4,CR3,CR2), determining word length, parity and stop bits [OSI selects 0x18 for "8 bits, even parity, 1 stop bit"] // this.CTRL_TCTL = 0x60; // Transmit Control (CR6,CR5) [OSI selects 0x40 for "RTS=High, Transmitting Interrupt Disabled"] // this.CTRL_RINT = 0x80; // Receive Interrupt Enable (CR7) [OSI selects 0x00 for interrupts disabled] this.STAT_RDRF = 0x01; // Receive Data Register Full this.STAT_TDRE = 0x02; // Transmit Data Register Empty this.STAT_DCD = 0x04; // Data Carrier Detect this.STAT_CTS = 0x08; // Clear To Send // this.STAT_FE = 0x10; // Framing Error (ie, the received character is improperly framed by a start and a stop bit and is detected by the absence of the first stop bit) // this.STAT_OVRN = 0x20; // Receiver Overrun (ie, one or more characters in the data stream were lost due to not being read from the Receive Data Register in time) // this.STAT_PE = 0x40; // Parity Error (ie, the number of highs (ones) in the character does not agree with the preselected odd or even parity) // this.STAT_IRQ = 0x80; // Interrupt Request (ie, state of the IRQ output; cleared by a read operation to the Receive Data Register or a write operation to the Transmit Data Register) /* * Last but not least, some internal state definitions and hard-coded assumptions */ this.DRIVETYPE_5INCH = 0; // this.DRIVETYPE_8INCH = 1; this.MAXTRACKS_5INCH = 40; // this.MAXTRACKS_8INCH = 77; /* * Some random OS-65D notes * * Version 3.3 Initialization Code * ------------------------------- * * The following code (where X is 0x00): * * 2217 8E 01 F4 STX $F401 * 221A 8E 00 F4 STX $F400 * 221D 8E 03 F4 STX $F403 * * is intended to reset a Printer PIA located at 0xF400. * * It then takes a detour to "SET KEYBOARD SOUND GENERATOR TO LOWEST FREQUENCY (192.753 HZ)" * with X set to 0xFF; the sound generator is supposed to be turned off a bit later, presumably * at the same time it sets "64 char/line" mode -- well, that's what v3.2 did anyway. * * 2220 CA DEX * 2221 8E 01 DF STX $DF01 * * While X is still 0xFF, it continues initializing the Printer PIA: * * 2224 8E 02 F4 STX $F402 * * Then the code fiddles a bit with a mystery serial port (perhaps the "Model 430B Cassette & Analog I/O" * interface?) * * 2227 AD 06 FB LDA $FB06 * 222A 8E 05 FB STX $FB05 * * And then it's back to more Printer PIA initialization: * * 222D A9 04 LDA #$04 * 222F 8D 01 F4 STA $F401 * 2232 8D 03 F4 STA $F403 * * Then it does some disk resetting (with A still 0x04 and Y set to 0x00): * * 2235 8C 01 C0 STY $C001 * 2238 A0 40 LDY #$40 ;'@' * 223A 8C 00 C0 STY $C000 * 223D 8D 01 C0 STA $C001 * * This code supposedly selects DRIVE 1: * * 2240 A9 01 LDA #$01 * 2242 20 C6 29 JSR $29C6 * * Then it "resets" and "sets" the TERMINAL ACIA. Note that the C1P serial port is addressed * at 0xF000-0xF0FF, and the C1P has ROM mapped to 0xF800-0xFFFF, so we know nothing of the serial * port mentioned above at 0xFBxx, nor this terminal ACIA port at 0xFCxx. * * 2245 A9 03 LDA #$03 * 2247 8D 00 FC STA $FC00 * 224A A0 11 LDY #$11 * 224C 8C 00 FC STY $FC00 * * Next, there's some code to "SET CA-10X 16 WAY SERIAL BOARD" at 0xCF00-0xCF1F; again, something * we know nothing about: * * 224F A2 1E LDX #$1E * 2251 9D 00 CF STA $CF00,X * 2254 98 TYA * 2255 9D 00 CF STA $CF00,X * 2258 A9 03 LDA #$03 * 225A CA DEX * 225B CA DEX * 225C 10 F3 BPL $2251 * * Then it clears 8 pages of video memory (ie, it simply ASSUMES that this is a Model 540 video board * with 2K of video memory): * * 225E A2 08 LDX #$08 * 2260 A9 D0 LDA #$D0 * 2262 85 FF STA $FF * 2264 A0 00 LDY #$00 * 2266 84 FE STY $FE * 2268 A9 20 LDA #$20 ;' ' * 226A 91 FE STA ($FE),Y * 226C C8 INY * 226D D0 FB BNE $226A * 226F E6 FF INC $FF * 2271 CA DEX * 2272 D0 F6 BNE $226A * * Then it performs a memory test, starting with a high page of 0xBF, and stores the highest page of * available RAM at 0x2300: * * 2276 A0 BF LDY #$BF * 2278 20 EC 22 JSR $22EC * 227B F0 03 BEQ $2280 * 227D 88 DEY * 227E D0 F8 BNE $2278 * 2280 8C 00 23 STY $2300 * * Now it checks for "SERIAL OR VIDEO (EITHER 65-A OR 65-V PROM)" (the byte at 0xFE01 on a C1P is 0x28, * so X will be 2, implying "VIDEO"): * * 2283 A2 01 LDX #$01 * 2285 AD 01 FE LDA $FE01 * 2288 F0 01 BEQ $228B * 228A E8 INX * 228B 8E C6 2A STX $2AC6 * * Finally, there's some code that's a little different from v3.2; in 3.2, it would set X to 0x01 * and then store X at 0xDE00, effectively forcing the video board into "64 char/line" mode -- which was * originally EXACTLY what I was looking for in the video emulation component. But v3.3 doesn't do that. * Here's what it does instead: * * 228F A2 00 LDX #$00 * 2291 8E 80 DC STX $DC80 * * So, what's supposed to be at 0xDC80? */ this.reset(true); } Component.subclass(C1PDiskController); /** * @this {C1PDiskController} * @param {boolean|undefined} [fPowerOn] is true for the initial reset only */ C1PDiskController.prototype.reset = function(fPowerOn) { this.resetRegs(); this.iDriveSelect = -1; if (fPowerOn) { this.aDrives = []; this.resetDrive(0, this.DRIVETYPE_5INCH, this.MAXTRACKS_5INCH); } }; /** * @this {C1PDiskController} */ C1PDiskController.prototype.resetRegs = function() { this.regDDA = { bits: this.PDA_SD2, // clear all DDA bits, indicating that all PDA bits represent INPUT lines (well, except for PDA_SD2) read: function() {}, update: function(controller) { return function(b) { if (b !== undefined) this.bits = b; if (!(controller.regCRA.bits & controller.CR_PD_SEL)) { controller.writePort(controller.PORT_DDA, this); } }; }(this) }; this.regPDA = { bits: 0xff, read: function() { this.update(); }, update: function(controller) { return function(b) { this.bits = controller.updatePDA(b); if (controller.regCRA.bits & controller.CR_PD_SEL) { controller.writePort(controller.PORT_PDA, this); } }; }(this) }; this.regCRA = { bits: 0, read: function() {}, update: function(controller) { return function(b) { /* * Most bits written to CRA should be left as-is (the CPU should read back what it wrote); * bits 7 and 6 (IRQ1 and IRQ2) are exceptions, since those are tied to peripheral "Control Lines" * C1 and C2, which can in theory generate an interrupt depending on how the C1_CTRL and C2_CTRL bits * in CRA are set. However, assuming there's no need to simulate interrupts for this particular * controller hardware, all we'll do is simply insure those two bits are always off. */ if (b !== undefined) this.bits = (b & ~(controller.CR_IRQ1 | controller.CR_IRQ2)); controller.writePort(controller.PORT_CRA, this); /* * Since a CRA write may have also changed which register (PDA or DDA) is enabled via the corresponding * PDA port, we simply ask ask both to update (only the one that's enabled will write itself to memory). */ controller.regPDA.update(); controller.regDDA.update(); }; }(this) }; this.regDDB = { bits: 0xff, // set all DDB bits, indicating that all PDB bits represent OUTPUT lines read: function() {}, update: function(controller) { return function(b) { if (b !== undefined) this.bits = b; if (!(controller.regCRB.bits & controller.CR_PD_SEL)) { controller.writePort(controller.PORT_DDB, this); } }; }(this) }; this.regPDB = { bits: 0xff, read: function() {}, update: function(controller) { return function(b) { this.bits = controller.updatePDB(b); if (controller.regCRB.bits & controller.CR_PD_SEL) { controller.writePort(controller.PORT_PDB, this); } }; }(this) }; this.regCRB = { bits: 0, read: function() {}, update: function(controller) { return function(b) { /* * Most bits written to CRB should be left as-is (the CPU should read back what it wrote); * bits 7 and 6 (IRQ1 and IRQ2) are exceptions, since those are tied to peripheral "Control Lines" * C1 and C2, which can in theory generate an interrupt depending on how the C1_CTRL and C2_CTRL bits * in CRB are set. However, assuming there's no need to simulate interrupts for this particular * controller hardware, all we'll do is simply insure those two bits are always off. */ if (b !== undefined) this.bits = (b & ~(controller.CR_IRQ1 | controller.CR_IRQ2)); controller.writePort(controller.PORT_CRB, this); /* * Since a CRB write may have also changed which register (PDB or DDB) is enabled via the corresponding * PDB port, we simply ask ask both to update (only the one that's enabled will write itself to memory). */ controller.regPDB.update(); controller.regDDB.update(); }; }(this) }; this.regCTRL = { bits: 0, read: function() {}, update: function(controller) { return function(b) { if (b !== undefined) { if ((b & controller.CTRL_CDIV) == controller.CTRL_CDIV) { /* * Setting both CTRL_CDIV bits (CR0 and CR1) constitutes a "Master Reset" of the ACIA */ controller.regSTAT.bits = (controller.STAT_TDRE | controller.STAT_DCD | controller.STAT_CTS); } this.bits = b; } // regCTRL isn't readable; instead, we ensure regSTAT is rewritten in its place controller.regSTAT.update(); }; }(this) }; this.regSTAT = { bits: (this.STAT_TDRE | this.STAT_DCD | this.STAT_CTS), read: function() {}, update: function(controller) { return function(b) { this.bits = controller.updateSTAT(b); controller.writePort(controller.PORT_STAT, this); }; }(this) }; this.regDATA = { bits: 0, read: function(controller) { return function() { controller.advanceDriveData(); }; }(this), update: function(controller) { return function(b) { if (b !== undefined) this.bits = b; controller.writePort(controller.PORT_DATA, this); }; }(this) }; this.regUnknown = { bits: 0, read: function() {}, update: function(controller) { return function(b) {}; }(this) }; if (DEBUG) { this.regDDA.sName = "DDA", this.regDDA.aBitIDs = {0x80:"DD7",0x40:"DD6",0x20:"DD5",0x10:"DD4",0x08:"DD3",0x04:"DD2",0x02:"DD1",0x01:"DD0"}; // jshint ignore:line this.regPDA.sName = "PDA"; this.regPDA.aBitIDs = {0x80:"IHD",0x40:"SD2",0x20:"WP",0x10:"RDY2",0x08:"SHD",0x04:"FD",0x02:"TZD",0x01:"RDY1"}; this.regCRA.sName = "CRA"; this.regCRA.aBitIDs = {0x80:"IRQ1",0x40:"IRQ2",0x20:"C2OUT",0x10:"C2:1",0x08:"C2:0",0x04:"PDS",0x02:"C1:1",0x01:"C1:0"}; this.regDDB.sName = "DDB"; this.regDDB.aBitIDs = {0x80:"DD7",0x40:"DD6",0x20:"DD5",0x10:"DD4",0x08:"DD3",0x04:"DD2",0x02:"DD1",0x01:"DD0"}; this.regPDB.sName = "PDB"; this.regPDB.aBitIDs = {0x80:"HLD",0x40:"LCS",0x20:"SD1",0x10:"FR",0x08:"ST",0x04:"STI",0x02:"EE",0x01:"WE"}; this.regCRB.sName = "CRB"; this.regCRB.aBitIDs = {0x80:"IRQ1",0x40:"IRQ2",0x20:"C2OUT",0x10:"C2:1",0x08:"C2:0",0x04:"PDS",0x02:"C1:1",0x01:"C1:0"}; this.regCTRL.sName = "CTRL"; this.regCTRL.aBitIDs = {0x80:"CR7",0x40:"CR6",0x20:"CR5",0x10:"CR4",0x08:"CR3",0x04:"CR2",0x02:"CR1",0x01:"CR0"}; this.regSTAT.sName = "STAT"; this.regDATA.sName = "DATA"; this.regUnknown.sName = "unknown"; } }; /** * @class Drive * @property {number} iType * @property {number} nTracks * @property {boolean} fProtected * @property {number} nIndexPulse * @property {number} iTrackSelect * @property {number} iTrackOffset * @property {Array} aTracks */ /** * @this {C1PDiskController} * @param {number} iDrive * @param {number} iDriveType * @param {number} nMaxTracks */ C1PDiskController.prototype.resetDrive = function(iDrive, iDriveType, nMaxTracks) { this.aDrives[iDrive] = { iType: iDriveType, nTracks: nMaxTracks, fProtected: true, // fake for now nIndexPulse: 20, // nIndex (20 is initial index pulse) iTrackSelect: 0, // nTrack iTrackOffset: -1, // nSector /* * Our disk data consists of an array of tracks, where each track is an array of sectors; * as long as aTracks.length == 0 (empty array), the drive is not considered "loaded" with a disk. */ aTracks: [] }; }; /** * @this {C1PDiskController} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisk") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ C1PDiskController.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) { switch(sBinding) { case "listDisk": this.bindings[sBinding] = control; return true; case "loadDisk": this.bindings[sBinding] = control; control.onclick = function(controller) { return function() { if (controller.bindings["listDisk"]) { var sFilePath = controller.bindings["listDisk"].value; var sFileURL = sFilePath; /* * If the selected disk image has a ".json" extension, then we assume it's a pre-converted * JSON-encoded disk image, so we load it as-is; otherwise, we ask our server-side disk image * converter to return the corresponding JSON-encoded data, in compact form (ie, minimal whitespace, * no ASCII data comments, etc). */ if (sFilePath.substr(sFilePath.length-5) != ".json") { /* * TODO: This code was using a deprecated parameter (compact=1); make sure things still work. * * TODO: Convert this code to use the new shared Disk API definitions and weblib functions; eg: * * sDiskURL = web.getHost() + DumpAPI.ENDPOINT + "?" + DumpAPI.QUERY.DISK + "=" + sDiskPath; */ sFileURL = "http://" + window.location.host + "/api/v1/dump?disk=" + sFilePath; } controller.println("loading " + str.getBaseName(sFilePath) + "..."); web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { controller.loadDisk(sURL, sResponse, nErrorCode); }); } }; }(this); return true; default: break; } return false; }; /** * @this {C1PDiskController} * @param {Array} abMemory * @param {number} start * @param {number} end * @param {C1PCPU} cpu */ C1PDiskController.prototype.setBuffer = function(abMemory, start, end, cpu) { this.abMem = abMemory; this.addrController = start; // this.addrControllerLimit = end + 1; if ((this.cpu = cpu)) { cpu.addReadNotify(start, end, this, this.getByte); cpu.addWriteNotify(start, end, this, this.setByte); } this.setReady(); }; /** * @this {C1PDiskController} * @param {boolean} fOn * @param {C1PComputer} cmp * * We need We make a note of the Computer component, so that we can invoke its reset() method whenever we need to * simulate a warm start, and we query the Keyboard component so that we can use its injectKeys() function. */ C1PDiskController.prototype.setPower = function(fOn, cmp) { if (fOn && !this.flags.powered) { this.flags.powered = true; if (DEBUGGER) this.dbg = cmp.getComponentByType("debugger"); } }; /** * @this {C1PDiskController} * @param {string} sDiskName * @param {string} sDiskData * @param {number} nErrorCode (response from server if anything other than 200) * * NOTE: Although I've expanded the JSON disk-image format to support multiple heads (ie, platters or disk surfaces), * this controller implementation currently supports only single-head drives, and therefore only single-sided images. * So, if the image contains more than one entry in head data array, all we use is the first entry; data for any remaining * heads is discarded. * * WARNING: The disk-image format should match that used by PCjs, where the image is an array of cylinders, each of which * is an array of heads. That's also more typical, because it maintains the original data's physical locality. */ C1PDiskController.prototype.loadDisk = function(sDiskName, sDiskData, nErrorCode) { if (nErrorCode) { this.println("disk load error (" + nErrorCode + ")"); return; } var aHeads = []; this.println("mounting " + sDiskName + "..."); try { /* * The most likely source of any exception will be right here, where we're parsing * the JSON-encoded disk data. */ aHeads = eval("(" + sDiskData + ")"); // jshint ignore:line if (!aHeads.length) { this.println("no data: " + sDiskName); return; } if (!aHeads[0].length) { this.println("no tracks: " + sDiskName); return; } var aTracks = aHeads[0]; if (aTracks[0]['trackNum'] === undefined) { this.println("data error: " + aTracks[0]); return; } /* * NOTE: This should never happen, otherwise we shouldn't have initiated the load * in the first place. Can we guarantee that and eliminate this test? */ if (!this.aDrives[0]) { this.println("no available drives"); return; } /* * To make disk access more efficient, we need to supplement every track object with a * simple byte-array (trackData) containing all the data bytes for the entire track. */ for (var iTrack=0; iTrack < aTracks.length; iTrack++) { var iTrackNum; var track = aTracks[iTrack]; var sectors = track['sectors']; /* * WARNING: There are MANY other ways the track data could be malformed, but we'll * start with the most egregious, and worry about the rest later. */ if ((iTrackNum = track['trackNum']) === undefined || sectors === undefined) { throw new Error("track " + iTrack + " missing data"); } /* * WARNING: We allow out-of-order tracks, because we store each track's data according * to its trackNum index, but just in case that wasn't intended, we're going to mention it. */ if (iTrackNum != iTrack) { Component.warning("track " + iTrackNum + " out of order (expected " + iTrack + ")"); } /* * For each track, we start with an empty trackData array and "push" (ie, append) all the * sector data onto it. Most of the data is already in byte form and can simply use Array.push(), * but there is also some metadata (signatures, types, lengths, etc), for which we have assorted * helpers below: pushBCD, pushBin, and pushSig. */ var trackData = [], sector, sectorData, i; if (!iTrackNum) { sector = sectors[0]; sectorData = sector['sectorData']; this.pushBin(trackData, track, 'trackLoad', 2); this.pushBin(trackData, sector, 'sectorPages'); for (i = 0; i < sectorData.length; i++) { trackData.push(sectorData[i]); } } else { this.pushSig(trackData, track, 'trackSig'); this.pushBCD(trackData, track, 'trackNum'); this.pushBin(trackData, track, 'trackType'); for (var iSector=0; iSector < sectors.length; iSector++) { sector = sectors[iSector]; sectorData = sector['sectorData']; this.pushBin(trackData, sector, 'sectorSig'); this.pushBin(trackData, sector, 'sectorNum'); this.pushBin(trackData, sector, 'sectorPages'); for (i = 0; i < sectorData.length; i++) { trackData.push(sectorData[i]); } this.pushSig(trackData, sector, 'sectorEndSig'); } } /* * Finally, here's where we add the newly-created chunk of track data to the current track object */ aTracks[iTrackNum].trackData = trackData; if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_DISK)) { this.dbg.message("track " + iTrackNum + ": " + trackData.length + " bytes"); } } this.aDrives[0].aTracks = aTracks; this.println("mount of " + sDiskName + " complete"); } catch (e) { this.println("disk data error: " + e.message); } }; /** * @this {C1PDiskController} * @param {Array.} a * @param {Object} o is the object containing the key * @param {string} k is the key of 8-bit value to convert to BCD (ie, two 4-bit BCD digits) and push */ C1PDiskController.prototype.pushBCD = function(a, o, k) { var n = o[k]; if (n === undefined) { throw new Error("missing bcd value: " + k); } var bcd = (Math.floor(n / 10) << 4) | (n % 10); a.push(bcd); }; /** * @this {C1PDiskController} * @param {Array.} a * @param {Object} o is the object containing the key * @param {string} k is the key of the value * @param {number} [cb] is the number of bytes to push (only 1 or 2 is supported, and the default is 1) */ C1PDiskController.prototype.pushBin = function(a, o, k, cb) { var n = o[k]; if (n === undefined) { throw new Error("missing binary value: " + k); } if (cb == 2) { a.push((n >> 8) & 0xff); } a.push(n & 0xff); }; /** * @this {C1PDiskController} * @param {Array.} a * @param {Object} o is the object containing the key * @param {string} k is the key of the signature string to push */ C1PDiskController.prototype.pushSig = function(a, o, k) { var s = o[k]; if (s === undefined) { throw new Error("missing signature: " + k); } for (var i=0; i < s.length; i++) { a.push(s.charCodeAt(i)); } }; /** * @this {C1PDiskController} * @param {number} port address (0x0000-0x00FF) relative to addrController (0xC000) * @param {boolean} fWrite is true if port write, false if port read * @return {Object} reg will always be a valid register object, but it may be the "unknown" register if we don't recognize the port. */ C1PDiskController.prototype.getReg = function(port, fWrite) { var reg; port &= 0x3F; /* * Now that we've masked the full port range of 0x00-0xFF down to 0x00-0x3F, we further mask the * PIA port range (0x00-0x0F) to 0x00-0x03, and the ACIA port range (0x10-0x1F) to 0x10-0x11. * The rest of the masked range (0x20-0x3F) is unmapped, so we map it to our global unknown register. */ if (port < 0x10) port &= 0x03; else if (port < 0x20) port &= 0x11; switch(port) { case this.PORT_PDA: reg = (this.regCRA.bits & this.CR_PD_SEL)? this.regPDA : this.regDDA; break; case this.PORT_CRA: reg = this.regCRA; break; case this.PORT_PDB: reg = (this.regCRB.bits & this.CR_PD_SEL)? this.regPDB : this.regDDB; break; case this.PORT_CRB: reg = this.regCRB; break; case this.PORT_CTRL: reg = (fWrite? this.regCTRL : this.regSTAT); break; case this.PORT_DATA: reg = this.regDATA; break; default: reg = this.regUnknown; break; } return reg; }; /** * @this {C1PDiskController} * @param {number} addr * @param {number|undefined} addrFrom (not defined whenever the Debugger tries to read the specified addr) */ C1PDiskController.prototype.getByte = function(addr, addrFrom) { /* * Don't trigger any further hardware emulation (beyond what we've already stored in memory) if * the Debugger performed this read (need a special Debugger I/O command if/when you really want to do that). */ if (addrFrom !== undefined) { var port = addr - this.addrController; var reg = this.getReg(port, false); if (DEBUGGER && this.dbg) this.dbg.messageIO(this, addr, addrFrom, this.dbg.MESSAGE_DISK, false, reg.sName); reg.read(); } }; /** * @this {C1PDiskController} * @param {number} addr * @param {number|undefined} addrFrom (not defined whenever the Debugger tries to write the specified addr) */ C1PDiskController.prototype.setByte = function(addr, addrFrom) { /* * Don't trigger any further hardware emulation (beyond what we've already stored in memory) if * the Debugger performed this write (need a special Debugger I/O command if/when you really want to do that). */ if (addrFrom !== undefined) { var b = this.cpu.getByte(addr); var port = addr - this.addrController; var reg = this.getReg(port, true); if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_DISK | this.dbg.MESSAGE_PORT)) { this.dbg.messageIO(this, addr, addrFrom, this.dbg.MESSAGE_DISK, true, reg.sName); if (reg.aBitIDs) { var bTest = 0x80; var bChanged = reg.bits ^ b; while (bChanged && bTest) { if (bChanged & bTest) { this.dbg.message(" changed " + reg.sName + "." + reg.aBitIDs[bTest] + " to " + ((b & bTest)? "1" : "0")); } bTest >>= 1; } } } reg.update(b); } }; /** * @this {C1PDiskController} * @param {number} bPDA * @param {number} bPDB */ C1PDiskController.prototype.setSelectedDrive = function(bPDA, bPDB) { var iDriveSelect = -1; if (bPDA !== undefined && bPDB !== undefined) { iDriveSelect = 0; if (!(bPDB & this.PDB_SD1)) iDriveSelect |= 0x02; if (!(this.regPDA.bits & this.PDA_SD2)) iDriveSelect |= 0x01; } if (this.iDriveSelect != iDriveSelect) { this.iDriveSelect = iDriveSelect; this.regSTAT.update(); } }; /** * @this {C1PDiskController} * @param {boolean} fLoaded is true if the selected drive must be loaded, false if don't care * @return {Object} drive reference to the selected drive, or null if no drive is selected or it doesn't meet the fLoaded requirement * C1PDiskController.prototype.getSelectedDrive = function(fLoaded) { var drive = null; if (this.iDriveSelect >= 0) { if (this.aDrives[this.iDriveSelect]) { if (!fLoaded || this.aDrives[this.iDriveSelect].aTracks.length) drive = this.aDrives[this.iDriveSelect]; } } return drive; }; */ /** * @this {C1PDiskController} */ C1PDiskController.prototype.startDriveData = function() { if (this.iDriveSelect >= 0) { this.aDrives[this.iDriveSelect].iTrackOffset = 0; this.advanceDriveData(); } }; /** * @this {C1PDiskController} */ C1PDiskController.prototype.stopDriveData = function() { if (this.iDriveSelect >= 0) { this.aDrives[this.iDriveSelect].iTrackOffset = -1; this.regDATA.update(0xFF); // QUESTION: Is this necessary or helpful in some way? this.regSTAT.update(); } }; /** * @this {C1PDiskController} * @return {number} current byte of data from the currently selected drive, or null if no (more) data available */ C1PDiskController.prototype.advanceDriveData = function() { var b = null; if (this.iDriveSelect >= 0) { var drive = this.aDrives[this.iDriveSelect]; var track = drive.aTracks[drive.iTrackSelect]; if (track !== undefined) { if (drive.iTrackOffset >= 0 && drive.iTrackOffset < track.trackData.length) { drive.nIndexPulse = 100; // QUESTION: Necessary? b = track.trackData[drive.iTrackOffset++]; this.regDATA.update(b); this.regSTAT.update(); } else { drive.nIndexPulse = 10; // QUESTION: Valid or necessary to force index pulse on next poll of $C000? this.stopDriveData(); } } } return b; }; /** * updatePDA() calculates an updated value for the PDA register. * * In the process, this may also update iDriveSelect and assorted drive internal variables. * * @this {C1PDiskController} * @param {number|undefined} bPDA * @return {number} updated bits for PDA */ C1PDiskController.prototype.updatePDA = function(bPDA) { if (bPDA === undefined) bPDA = this.regPDA.bits; else this.setSelectedDrive(bPDA, this.regPDB.bits); /* * We start by turning ON most bits, except for PDA_RDY1, which we always leave * OFF (indicating ready). We leave PDA_SD2 alone, so that it reflects whatever * the CPU had set. * * Then we dive into the update logic, which will turn OFF any of the bits we * originally turned ON if the corresponding condition is true (because an OFF bit * signals an active condition). */ bPDA |= (this.PDA_IHD | this.PDA_WP | this.PDA_SHD | this.PDA_FD | this.PDA_TZD | this.PDA_RDY2); bPDA &= ~(this.PDA_RDY1); if (this.iDriveSelect >= 0) { if (this.aDrives[this.iDriveSelect].aTracks.length) { var drive = this.aDrives[this.iDriveSelect]; if (drive.fProtected) { bPDA &= ~this.PDA_WP; } if (!drive.iTrackSelect) { bPDA &= ~this.PDA_TZD; } /* * Simulate PHD_IHD (Index Hole Detect) */ if (--drive.nIndexPulse <= 10) { if (drive.nIndexPulse > 0) { bPDA &= ~this.PDA_IHD; this.stopDriveData(); } else { drive.nIndexPulse = 100; this.startDriveData(); } } } } return bPDA; }; /** * updatePDB() calculates an updated value for the PDB register. However, since the * PDB consists entirely of OUTPUT bits, none of the given bits should actually be modified. * * In the process, this may also update iDriveSelect and assorted drive internal variables, * as well as selected PDA INPUT bits (hence the call to regPDA.update()). * * @this {C1PDiskController} * @param {number|undefined} bPDB * @return {number} updated bits for PDB */ C1PDiskController.prototype.updatePDB = function(bPDB) { if (bPDB === undefined) bPDB = this.regPDB.bits; else this.setSelectedDrive(this.regPDA.bits, bPDB); if (this.iDriveSelect >= 0 && this.iDriveSelect < this.aDrives.length) { var drive = this.aDrives[this.iDriveSelect]; if (drive.aTracks.length) { /* * Is PDB_ST transitioning from 1 to 0? */ if ((this.regPDB.bits & this.PDB_ST) && !(bPDB & this.PDB_ST)) { /* * PDB_STI == 0? step toward track 39 : step toward track 0 */ if (bPDB & this.PDB_STI) drive.iTrackSelect--; else drive.iTrackSelect++; if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_DISK)) { this.dbg.message("stepping " + ((bPDB & this.PDB_STI)? "down" : "up") + " to track " + drive.iTrackSelect); } if (drive.iTrackSelect >= drive.nTracks) drive.iTrackSelect = drive.nTracks; if (drive.iTrackSelect < 0) drive.iTrackSelect = 0; drive.nIndexPulse = 20; this.regPDA.update(this.regPDA.bits | this.PDA_IHD); this.stopDriveData(); } } } else if (DEBUG && this.iDriveSelect >= 0) { this.println("updatePDB(" + str.toHexByte(bPDB) + "): invalid drive: " + this.iDriveSelect); } return bPDB; }; /** * updateSTAT() calculates an updated value for the ACIA Status register. * * @this {C1PDiskController} * @param {number|undefined} bSTAT * @return {number} updated bits for STAT */ C1PDiskController.prototype.updateSTAT = function(bSTAT) { if (bSTAT === undefined) bSTAT = this.regSTAT.bits; bSTAT &= ~this.STAT_RDRF; if (this.iDriveSelect >= 0 && this.aDrives[this.iDriveSelect].iTrackOffset >= 0) bSTAT |= this.STAT_RDRF; return bSTAT; }; /** * @this {C1PDiskController} * @param {number} port * @param {Object} reg */ C1PDiskController.prototype.writePort = function(port, reg) { this.cpu.setByte(port + this.addrController, reg.bits); }; /** * C1PDiskController.init() * * This function operates on every HTML element of class "disk", extracting the * JSON-encoded parameters for the C1PDiskController constructor from the element's "data-value" * attribute, invoking the constructor to create a C1PDiskController component, and then binding * any associated HTML controls to the new component. */ C1PDiskController.init = function() { var aeDC = Component.getElementsByClass(document, C1PJS.APPCLASS, "disk"); for (var iDC=0; iDC < aeDC.length; iDC++) { var eDC = aeDC[iDC]; var parmsDC = Component.getComponentParms(eDC); var controller = new C1PDiskController(parmsDC); Component.bindComponentControls(controller, eDC, C1PJS.APPCLASS); } }; /* * Initialize every DiskController module on the page. */ web.onInit(C1PDiskController.init);