/** * @fileoverview Implements the PCjs Floppy Drive Controller (FDC) component. * @author Jeff Parsons * @version 1.0 * @suppress {missingProperties} * Created 2012-Aug-09 * * Copyright © 2012-2014 Jeff Parsons * * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) * at and . * * 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 Computer.sCopyright). * * 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 the * PCjs program for purposes of the GNU General Public License, and the author does not claim * any copyright as to their contents. */ "use strict"; if (typeof module !== 'undefined') { var str = require("../../shared/lib/strlib"); var web = require("../../shared/lib/weblib"); var DiskAPI = require("../../shared/lib/diskapi"); var Component = require("../../shared/lib/component"); var ChipSet = require("./chipset"); var Disk = require("./disk"); var Computer = require("./computer"); var State = require("./state"); } /* * FDC Terms (see FDC.TERMS) * * C Cylinder Number the current or selected cylinder number * * D Data the data pattern to be written to a sector * * DS Drive Select the selected driver number encoded the same as bits 0 and 1 of the Digital Output * Register (DOR); eg, DS0, DS1, DS2, or DS3 * * DTL Data Length when N is 00, DTL is the data length to be read from or written to a sector * * EOT End Of Track the final sector number on a cylinder * * GPL Gap Length the length of gap 3 (spacing between sectors excluding the VCO synchronous field) * * H Head Address the head number, either 0 or 1, as specified in the ID field * * HD Head the selected head number, 0 or 1 (H = HD in all command words) * * HLT Head Load Time the head load time in the selected drive (2 to 256 milliseconds in 2-millisecond * increments for the 1.2M-byte drive and 4 to 512 milliseconds in 4 millisecond increments * for the 320K-byte drive) * * HUT Head Unload Time the head unload time after a read or write operation (0 to 240 milliseconds in * 16-millisecond increments for the 1.2M-byte drive and 0 to 480 milliseconds in * 32-millisecond increments for the 320K-byte drive) * * MF FM or MFM Mode 0 selects FM mode and 1 selects MFM (MFM is selected only if it is implemented) * * MT Multitrack 1 selects multitrack operation (both HD0 and HD1 will be read or written) * * N Number the number of data bytes written in a sector * * NCN New Cylinder Number the new cylinder number for a Seek operation * * ND Non-Data Mode indicates an operation in the non-data mode * * PCN Present Cylinder Number the cylinder number at the completion of a Sense Interrupt Status command * (present position of the head) * * R Record the sector number to be read or written * * SC Sectors Per Cylinder the number of sectors per cylinder * * SK Skip this stands for skip deleted-data address mark * * SRT Stepping Rate this 4 bit byte indicates the stepping rate for the diskette drive as follows: * 1.2M-Byte Diskette Drive: 1111=1ms, 1110=2ms, 1101=3ms * 320K-Byte Diskette Drive: 1111=2ms, 1110=4ms, 1101=6ms * * STP STP Scan Test if STP is 1, the data in contiguous sectors is compared with the data sent * by the processor during a scan operation; if STP is 2, then alternate sections * are read and compared */ /** * FDC(parmsFDC) * * The FDC component simulates an NEC PD765A, and has one component-specific property: * * autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties * * Regarding early diskette drives: the IBM PC Model 5150 originally shipped with single-sided drives, * and therefore supported only 160Kb diskettes. That's the only diskette format PC-DOS 1.00 supported, too. * * At some point, 5150's started shipping with double-sided drives, but I'm not sure whether the ROMs changed; * they probably did NOT change, because the original ROM BIOS already supported drives with multiple heads. * However, what the ROM BIOS did NOT do was provide any indication of drive type, which as far as I can tell, * meant you had to simply read/write/format tracks with the second head and check for errors. * * Presumably at the same time double-sided drives started shipping, PC-DOS 1.10 shipped, which added * support for 320Kb diskettes. And the FORMAT command changed as well, defaulting to a double-sided format * operation UNLESS you specified "FORMAT /1". If I run PC-DOS 1.10 and try to simulate a single-sided drive * (by setting drive.nHeads = 1 in initDrive), FORMAT will balk with "Track 0 bad - disk unusable". I have to * wonder if everyone with single-sided drives who upgraded to PC-DOS 1.10 also got that error, forcing them * to always specify "FORMAT /1", or if I'm doing something wrong wrt single-sided drive simulation. * * I've noticed that if I turn FDC messages on ("m fdc on"), and then run "FORMAT B:/1", the command still * tries to format head 1/track 0, followed by head 0/track 0, and then the FDC is reset, and the format operation * proceeds with only head 0 for all tracks 0 through 39. FORMAT successfully creates a 160Kb single-sided diskette, * but why it also tries to initially format track 0 using the second head remains a bit of a mystery. * * @constructor * @extends Component * @param {Object} parmsFDC */ function FDC(parmsFDC) { /* * TODO: Indicate the type of diskette image being loaded (this might help folks understand what's going * on when they try to load a diskette image that's larger than what the selected operating system supports). */ Component.call(this, "FDC", parmsFDC, FDC); this['dmaRead'] = this.dmaRead; this['dmaWrite'] = this.dmaWrite; this['dmaFormat'] = this.dmaFormat; this.pAutoMount = null; if (parmsFDC['autoMount']) { this.pAutoMount = parmsFDC['autoMount']; if (typeof this.pAutoMount == "string") { try { /* * The most likely source of any exception will be right here, where we're parsing * the JSON-encoded diskette data. */ this.pAutoMount = eval("(" + parmsFDC['autoMount'] + ")"); } catch (e) { Component.error("FDC auto-mount error: " + e.message + " (" + parmsFDC['autoMount'] + ")"); this.pAutoMount = null; } } } /* * The following array keeps track of every disk image we've ever mounted. Each entry in the * array is another array whose elements are: * * [0]: name of disk * [1]: path of disk * [2]: array of deltas, uninitialized until the disk is unmounted and/or all state is saved * * See functions addDiskHistory() and updateDiskHistory(). */ this.aDiskHistory = []; /* * The remainder of FDC initialization now takes place in our initBus() handler, largely because we * want initController() to have access to the ChipSet component, so that it can query switches and/or CMOS * settings that determine the number of drives and their characteristics (eg, 40-track vs. 80-track), * which it can then pass on to initDrive(). */ } Component.subclass(Component, FDC); FDC.DEFAULT_DRIVE_NAME = "Floppy Drive"; if (DEBUG) { FDC.TERMS = { C: "C", // Cylinder Number D: "D", // Data (eg, pattern to be written to a sector) H: "H", // Head Address R: "R", // Record (ie, sector number to be read or written) N: "N", // Number (ie, number of data bytes to write) DS: "DS", // Drive Select SC: "SC", // Sectors per Cylinder DTL: "DTL", // Data Length EOT: "EOT", // End of Track GPL: "GPL", // Gap Length HLT: "HLT", // Head Load Time NCN: "NCN", // New Cylinder Number PCN: "PCN", // Present Cylinder Number SRT: "SRT", // Stepping Rate ST0: "ST0", // Status Register 0 ST1: "ST1", // Status Register 1 ST2: "ST2", // Status Register 2 ST3: "ST3" // Status Register 3 } } else { FDC.TERMS = {}; } /* * FDC Digital Output Register (DOR) (0x3F2, write-only) * * NOTE: Reportedly, a drive's MOTOR had to be ON before the drive could be selected; however, outFDCOutput() no * longer verifies that. Also, motor start time for original drives was 500ms, but we make no attempt to simulate that. * * On the MODEL_5170 "PC AT Fixed Disk and Diskette Drive Adapter", this port is called the Digital Output Register * or DOR. It uses the same bit definitions as the original FDC Output Register, except that only two diskette drives * are supported, hence bit 1 is always 0 (ie, FDC.REG_OUTPUT.DS2 and FDC.REG_OUTPUT.DS3 are not supported) and bits * 6 and 7 are unused (FDC.REG_OUTPUT.MOTOR_D2 and FDC.REG_OUTPUT.MOTOR_D3 are not supported). */ FDC.REG_OUTPUT = {}; FDC.REG_OUTPUT.PORT = 0x3F2; FDC.REG_OUTPUT.DS = 0x03; // drive select bits FDC.REG_OUTPUT.DS0 = 0x00; FDC.REG_OUTPUT.DS1 = 0x01; FDC.REG_OUTPUT.DS2 = 0x02; // reserved on the MODEL_5170 FDC.REG_OUTPUT.DS3 = 0x03; // reserved on the MODEL_5170 FDC.REG_OUTPUT.ENABLE = 0x04; // clearing this bit resets the FDC FDC.REG_OUTPUT.INT_ENABLE = 0x08; // enables both FDC and DMA (Channel 2) interrupt requests (IRQ 6) FDC.REG_OUTPUT.MOTOR_D0 = 0x10; FDC.REG_OUTPUT.MOTOR_D1 = 0x20; FDC.REG_OUTPUT.MOTOR_D2 = 0x40; // reserved on the MODEL_5170 FDC.REG_OUTPUT.MOTOR_D3 = 0x80; // reserved on the MODEL_5170 /* * FDC Main Status Register (0x3F4, read-only) * * On the MODEL_5170 "PC AT Fixed Disk and Diskette Drive Adapter", bits 2 and 3 are reserved, since that adapter * supported a maximum of two diskette drives. */ FDC.REG_STATUS = {}; FDC.REG_STATUS.PORT = 0x3F4; FDC.REG_STATUS.BUSY_A = 0x01; FDC.REG_STATUS.BUSY_B = 0x02; FDC.REG_STATUS.BUSY_C = 0x04; // reserved on the MODEL_5170 FDC.REG_STATUS.BUSY_D = 0x08; // reserved on the MODEL_5170 FDC.REG_STATUS.BUSY = 0x10; // a read or write command is in progress FDC.REG_STATUS.NON_DMA = 0x20; // FDC is in non-DMA mode FDC.REG_STATUS.READ_DATA = 0x40; // transfer is from FDC Data Register to processor (if clear, then transfer is from processor to the FDC Data Register) FDC.REG_STATUS.RQM = 0x80; // indicates FDC Data Register is ready to send or receive data to or from the processor (Request for Master) /* * FDC Data Register (0x3F5, read-write) */ FDC.REG_DATA = {}; FDC.REG_DATA.PORT = 0x3F5; /* * FDC "Fixed Disk" Register (0x3F6, write-only) * * Since this register's functions are all specific to the Hard Disk Controller, see the HDC component for details. * The fact that this HDC register is in the middle of the FDC I/O port range is an oddity of the "HFCOMBO" controller. */ /* * FDC Digital Input Register (0x3F7, read-only, MODEL_5170 only) * * Bit 7 indicates a diskette change (the MODEL_5170 introduced change-line support). Bits 0-6 are for the selected * hard disk drive, so this port must be shared with the HDC; bits 0-6 are valid for 50 microseconds after a write to * the Drive Head Register. */ FDC.REG_INPUT = {}; FDC.REG_INPUT.PORT = 0x3F7; FDC.REG_INPUT.DS0 = 0x01; // Drive Select 0 FDC.REG_INPUT.DS1 = 0x02; // Drive Select 1 FDC.REG_INPUT.HS0 = 0x04; // Head Select 0 FDC.REG_INPUT.HS1 = 0x08; // Head Select 1 FDC.REG_INPUT.HS2 = 0x10; // Head Select 2 FDC.REG_INPUT.HS3 = 0x20; // Head Select 3 FDC.REG_INPUT.WRITE_GATE = 0x40; // Write Gate FDC.REG_INPUT.DISK_CHANGE = 0x80; // Diskette Change /* * FDC Diskette Control Register (0x3F7, write-only, MODEL_5170 only) * * Only bits 0-1 are used; bits 2-7 are reserved. */ FDC.REG_CONTROL = {}; FDC.REG_CONTROL.PORT = 0x3F7; FDC.REG_CONTROL.RATE500K = 0x00; // 500,000 bps FDC.REG_CONTROL.RATE300K = 0x02; // 300,000 bps FDC.REG_CONTROL.RATE250K = 0x01; // 250,000 bps FDC.REG_CONTROL.RATEUNUSED = 0x03; /* * FDC Commands * * NOTE: FDC command bytes need to be masked with FDC.REG_DATA.CMD.MASK before comparing to the values below, since a * number of commands use the following additional bits as follows: * * SK (0x20): Skip Deleted Data Address Mark * MF (0x40): Modified Frequency Modulation (as opposed to FM or Frequency Modulation) * MT (0x80): multi-track operation (ie, data processed under both head 0 and head 1) * * We don't support MT (Multi-Track) operations at this time, and the MF and SK designations cannot be supported as long * as our diskette images contain only the original data bytes without any formatting information. */ FDC.REG_DATA.CMD = {}; FDC.REG_DATA.CMD.READ_TRACK = 0x02; FDC.REG_DATA.CMD.SPECIFY = 0x03; FDC.REG_DATA.CMD.SENSE_DRIVE = 0x04; FDC.REG_DATA.CMD.WRITE_DATA = 0x05; FDC.REG_DATA.CMD.READ_DATA = 0x06; FDC.REG_DATA.CMD.RECALIBRATE = 0x07; FDC.REG_DATA.CMD.SENSE_INT = 0x08; // this command is used to clear the FDC interrupt following the clearing/setting of FDC.REG_OUTPUT.ENABLE FDC.REG_DATA.CMD.WRITE_DEL_DATA = 0x09; FDC.REG_DATA.CMD.READ_ID = 0x0A; FDC.REG_DATA.CMD.READ_DEL_DATA = 0x0C; FDC.REG_DATA.CMD.FORMAT_TRACK = 0x0D; FDC.REG_DATA.CMD.SEEK = 0x0F; FDC.REG_DATA.CMD.SCAN_EQUAL = 0x11; FDC.REG_DATA.CMD.SCAN_LO_EQUAL = 0x19; FDC.REG_DATA.CMD.SCAN_HI_EQUAL = 0x1D; FDC.REG_DATA.CMD.MASK = 0x1F; FDC.REG_DATA.CMD.SK = 0x20; // SK (Skip Deleted Data Address Mark) FDC.REG_DATA.CMD.MF = 0x40; // MF (Modified Frequency Modulation) FDC.REG_DATA.CMD.MT = 0x80; // MT (Multi-Track; ie, data under both heads will be processed) /* * FDC status/error results, generally assigned according to the corresponding ST0, ST1, ST2 or ST3 status bit. * * TODO: Determine when EQUIP_CHECK is *really* set; "77 step pulses" sounds suspiciously like a typo. */ FDC.REG_DATA.RES = {}; FDC.REG_DATA.RES.NONE = 0x00000000; // ST0 (IC): Normal termination of command (NT) FDC.REG_DATA.RES.NOT_READY = 0x00000008; // ST0 (NR): When the FDD is in the not-ready state and a read or write command is issued, this flag is set; if a read or write command is issued to side 1 of a single sided drive, then this flag is set FDC.REG_DATA.RES.EQUIP_CHECK = 0x00000010; // ST0 (EC): If a fault signal is received from the FDD, or if the track 0 signal fails to occur after 77 step pulses (recalibrate command), then this flag is set FDC.REG_DATA.RES.SEEK_END = 0x00000020; // ST0 (SE): When the FDC completes the Seek command, this flag is set to 1 (high) FDC.REG_DATA.RES.INCOMPLETE = 0x00000040; // ST0 (IC): Abnormal termination of command (AT); execution of command was started, but was not successfully completed FDC.REG_DATA.RES.RESET = 0x000000C0; // ST0 (IC): Abnormal termination because during command execution the ready signal from the drive changed state FDC.REG_DATA.RES.INVALID = 0x00000080; // ST0 (IC): Invalid command issue (IC); command which was issued was never started FDC.REG_DATA.RES.ST0 = 0x000000FF; FDC.REG_DATA.RES.NO_ID_MARK = 0x00000100; // ST1 (MA): If the FDC cannot detect the ID Address Mark, this flag is set; at the same time, the MD (Missing Address Mark in Data Field) of Status Register 2 is set FDC.REG_DATA.RES.NOT_WRITABLE = 0x00000200; // ST1 (NW): During Execution of a Write Data, Write Deleted Data, or Format a Cylinder command, if the FDC detects a write protect signal from the FDD, then this flag is set FDC.REG_DATA.RES.NO_DATA = 0x00000400; // ST1 (ND): FDC cannot find specified sector (or specified ID if READ_ID command) FDC.REG_DATA.RES.DMA_OVERRUN = 0x00001000; // ST1 (OR): If the FDC is not serviced by the main systems during data transfers within a certain time interval, this flag is set FDC.REG_DATA.RES.CRC_ERROR = 0x00002000; // ST1 (DE): When the FDC detects a CRC error in either the ID field or the data field, this flag is set FDC.REG_DATA.RES.END_OF_CYL = 0x00008000; // ST1 (EN): When the FDC tries to access a sector beyond the final sector of a cylinder, this flag is set FDC.REG_DATA.RES.ST1 = 0x0000FF00; FDC.REG_DATA.RES.NO_DATA_MARK = 0x00010000; // ST2 (MD): When data is read from the medium, if the FDC cannot find a Data Address Mark or Deleted Data Address Mark, then this flag is set FDC.REG_DATA.RES.BAD_CYL = 0x00020000; // ST2 (BC): This bit is related to the ND bit, and when the contents of C on the medium are different from that stored in the ID Register, and the content of C is FF, then this flag is set FDC.REG_DATA.RES.SCAN_FAILED = 0x00040000; // ST2 (SN): During execution of the Scan command, if the FDC cannot find a sector on the cylinder which meets the condition, then this flag is set FDC.REG_DATA.RES.SCAN_EQUAL = 0x00080000; // ST2 (SH): During execution of the Scan command, if the condition of "equal" is satisfied, this flag is set FDC.REG_DATA.RES.WRONG_CYL = 0x00100000; // ST2 (WC): This bit is related to the ND bit, and when the contents of C on the medium are different from that stored in the ID Register, this flag is set FDC.REG_DATA.RES.DATA_FIELD = 0x00200000; // ST2 (DD): If the FDC detects a CRC error in the data, then this flag is set FDC.REG_DATA.RES.STRL_MARK = 0x00400000; // ST2 (CM): During execution of the Read Data or Scan command, if the FDC encounters a sector which contains a Deleted Data Address Mark, this flag is set FDC.REG_DATA.RES.ST2 = 0x00FF0000; FDC.REG_DATA.RES.DRIVE = 0x03000000; // ST3 (Ux): Status of the "Drive Select" signals from the diskette drive FDC.REG_DATA.RES.HEAD = 0x04000000; // ST3 (HD): Status of the "Side Select" signal from the diskette drive FDC.REG_DATA.RES.TWOSIDE = 0x08000000; // ST3 (TS): Status of the "Two Side" signal from the diskette drive FDC.REG_DATA.RES.TRACK0 = 0x10000000; // ST3 (T0): Status of the "Track 0" signal from the diskette drive FDC.REG_DATA.RES.READY = 0x20000000; // ST3 (RY): Status of the "Ready" signal from the diskette drive FDC.REG_DATA.RES.WRITEPROT = 0x40000000; // ST3 (WP): Status of the "Write Protect" signal from the diskette drive FDC.REG_DATA.RES.FAULT = 0x80000000; // ST3 (FT): Status of the "Fault" signal from the diskette drive FDC.REG_DATA.RES.ST3 = 0xFF000000; /* * FDC Command Sequences * * For each command, cbReq indicates the total number of bytes in the command request sequence, * including the first (command) byte; cbRes indicates total number of bytes in the response sequence. */ if (DEBUG) { FDC.CMDS = { SPECIFY: "SPECIFY", SENSE_DRIVE: "SENSE DRIVE", WRITE_DATA: "WRITE DATA", READ_DATA: "READ DATA", RECALIBRATE: "RECALIBRATE", SENSE_INT: "SENSE INTERRUPT", READ_ID: "READ ID", FORMAT: "FORMAT", SEEK: "SEEK" } } else { FDC.CMDS = {}; } FDC.aCmdInfo = { 0x03: {cbReq: 3, cbRes: 0, name: FDC.CMDS.SPECIFY}, 0x04: {cbReq: 2, cbRes: 1, name: FDC.CMDS.SENSE_DRIVE}, 0x05: {cbReq: 9, cbRes: 7, name: FDC.CMDS.WRITE_DATA}, 0x06: {cbReq: 9, cbRes: 7, name: FDC.CMDS.READ_DATA}, 0x07: {cbReq: 2, cbRes: 0, name: FDC.CMDS.RECALIBRATE}, 0x08: {cbReq: 1, cbRes: 2, name: FDC.CMDS.SENSE_INT}, 0x0A: {cbReq: 2, cbRes: 7, name: FDC.CMDS.READ_ID}, 0x0D: {cbReq: 6, cbRes: 7, name: FDC.CMDS.FORMAT}, 0x0F: {cbReq: 3, cbRes: 0, name: FDC.CMDS.SEEK} }; /* * FDC BIOS interrupts, functions, and other parameters */ FDC.BIOS = { INT_DISKETTE: 0x13 }; /** * setBinding(sHTMLClass, sHTMLType, sBinding, control) * * @this {FDC} * @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output") * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisks") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @return {boolean} true if binding was successful, false if unrecognized binding request */ FDC.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control) { switch (sBinding) { case "listDisks": this.bindings[sBinding] = control; /* * Add the special path of "?" to the list, which will prompt the user for a URL. */ var controlOption = window.document.createElement("option"); controlOption['value'] = "?"; controlOption.innerHTML = "User-defined URL..."; control.appendChild(controlOption); /* * Now add an 'onchange' handler. */ control.onchange = function(fdc, controlDisks) { return function onChangeListDisks() { var controlDesc = fdc.bindings["descDisk"]; if (controlDesc) { var controlOption = controlDisks.options[controlDisks.selectedIndex]; if (controlOption) { var dataValue = {}; var sValue = controlOption.getAttribute("data-value"); if (sValue) { try { dataValue = eval("({" + sValue + "})"); } catch (e) { Component.error("FDC option error: " + (e.message || e)); } } var sDesc = dataValue['desc']; if (sDesc === undefined) sDesc = ""; var sHRef = dataValue['href']; if (sHRef !== undefined) sDesc = "" + sDesc + ""; controlDesc.innerHTML = sDesc; } } }; }(this, control); return true; case "descDisk": case "listDrives": this.bindings[sBinding] = control; /* * I tried going with onclick instead of onchange, so that if you wanted to confirm what's * loaded in a particular drive, you could click the drive control without having to change it. * However, that doesn't seem to work for all browsers, so I've reverted to onchange. */ control.onchange = function(fdc, controlDrives) { return function onChangeListDrives() { var iDrive = parseInt(controlDrives.value, 10); if (!isNaN(iDrive)) fdc.displayDiskette(iDrive); }; }(this, control); return true; case "loadDrive": this.bindings[sBinding] = control; control.onclick = function(fdc) { return function onClickLoadDrive() { var iDrive; var controlDisks = fdc.bindings["listDisks"]; var controlDrives = fdc.bindings["listDrives"]; if (controlDisks && controlDrives && !isNaN(iDrive = parseInt(controlDrives.value, 10)) && iDrive >= 0 && iDrive < fdc.aDrives.length) { var sDiskettePath = controlDisks.value; if (!sDiskettePath) { fdc.unloadDrive(iDrive); return; } var sDisketteName = controlDisks.options[controlDisks.selectedIndex].text; /* * If the special path of "?" is selected, then we want to prompt the user for a URL. Oh, and * make sure we pass an empty string as the 2nd parameter to prompt(), so that IE won't display * "undefined" -- because after all, undefined and "undefined" are EXACTLY the same thing, right? * * TODO: This is literally all I've done to support external disk images. There's probably more * I should do, like dynamically updating "listDisks" to include new entries, and adding new entries * to the save/restore data. */ if (sDiskettePath == "?") { sDiskettePath = window.prompt("Enter the URL of a disk image to load.", ""); if (!sDiskettePath) return; sDisketteName = str.getBaseName(sDiskettePath); fdc.println("Attempting to load " + sDiskettePath + " as \"" + sDisketteName + "\""); } while (fdc.loadDiskette(iDrive, sDisketteName, sDiskettePath, false)) { if (!window.confirm("Click OK to reload the original disk.\n(WARNING: All disk changes will be discarded)")) { return; } /* * So here's the story: loadDiskette() returned true, which it does ONLY if the specified disk is already * mounted, AND the user clicked OK to reload the original disk image. So we must toss any history we have * for the disk, unload it, and then loop back around to loadDiskette(). * * loadDiskette() should NEVER return true the second time, since no disk is loaded. In other words, this * isn't really a loop so much as a one-time retry operation. */ fdc.removeDiskHistory(sDisketteName, sDiskettePath); fdc.unloadDrive(iDrive, false, true); } return; } fdc.notice("Nothing to load"); }; }(this); return true; default: break; } return false; }; /** * initBus(cmp, bus, cpu, dbg) * * @this {FDC} * @param {Computer} cmp * @param {Bus} bus * @param {X86CPU} cpu * @param {Debugger} dbg */ FDC.prototype.initBus = function(cmp, bus, cpu, dbg) { this.bus = bus; this.cpu = cpu; this.dbg = dbg; this.cmp = cmp; this.chipset = cmp.getComponentByType("ChipSet"); /* * If we didn't need auto-mount support, we could defer controller initialization until we received a powerUp() notification, * at which point reset() would call initController(), or restore() would restore the controller; in that case, all we'd need * to do here is call setReady(). */ this.initController(); bus.addPortInputTable(this, FDC.aPortInput); bus.addPortOutputTable(this, FDC.aPortOutput); if (DEBUGGER) { if (dbg) dbg.messageInit(FDC); cpu.addIntNotify(FDC.BIOS.INT_DISKETTE, this, this.intBIOSDiskette); } if (!this.autoMount()) this.setReady(); }; /** * powerUp(data, fRepower) * * @this {FDC} * @param {Object|null} data * @param {boolean} [fRepower] * @return {boolean} true if successful, false if failure */ FDC.prototype.powerUp = function(data, fRepower) { if (!fRepower) { if (!data || !this.restore) { this.reset(); if (this.cmp.fReload) { /* * If the computer's fReload flag is set, we're required to toss all currently * loaded disks and remount all disks specified in the auto-mount configuration. */ this.unloadAllDrives(true); this.autoMount(true); } } else { if (!this.restore(data)) return false; } /* * Populate the HTML controls to match the actual (well, um, specified) number of floppy drives. */ var controlDrives; if ((controlDrives = this.bindings['listDrives'])) { while (controlDrives.firstChild) { controlDrives.removeChild(controlDrives.firstChild); } controlDrives.innerHTML = ""; for (var iDrive = 0; iDrive < this.nDrives; iDrive++) { var controlOption = window.document.createElement("option"); controlOption['value'] = iDrive; /* * TODO: This conversion of drive number to drive letter, starting with A:, is very simplistic * and will NOT match the drive mappings that DOS ultimately uses. We'll need to spiff this up at * some point. */ controlOption.innerHTML = String.fromCharCode(0x41 + iDrive) + ":"; controlDrives.appendChild(controlOption); } if (this.nDrives > 0) { controlDrives.value = "0"; this.displayDiskette(0); } } } return true; }; /** * powerDown(fSave) * * @this {FDC} * @param {boolean} fSave * @return {Object|boolean} */ FDC.prototype.powerDown = function(fSave) { return fSave && this.save? this.save() : true; }; /** * reset() * * NOTE: initController() establishes the maximum possible number of drives, but it's not until * we interrogate the current SW1 settings that we will have an ACTUAL number of drives (nDrives), * at which point we can also update the contents of the "listDrives" HTML control, if any. * * @this {FDC} */ FDC.prototype.reset = function() { /* * NOTE: The controller is also initialized by the constructor, to assist with auto-mount support, * so think about whether we can skip powerUp initialization. */ this.initController(); }; /** * save() * * This implements save support for the FDC component. * * @this {FDC} * @return {Object} */ FDC.prototype.save = function() { var state = new State(this); state.set(0, this.saveController()); return state.data(); }; /** * restore(data) * * This implements restore support for the FDC component. * * @this {FDC} * @param {Object} data * @return {boolean} true if successful, false if failure */ FDC.prototype.restore = function(data) { return this.initController(data[0]); }; /** * initController(data) * * @this {FDC} * @param {Array} [data] * @return {boolean} true if successful, false if failure */ FDC.prototype.initController = function(data) { var i = 0, iDrive; var fSuccess = true; if (data === undefined) { data = [0, 0, FDC.REG_STATUS.RQM, new Array(9), 0, 0, 0, []]; } /* * Selected drive (from regOutput), which can only be selected if its motor is on (see regOutput). */ this.iDrive = data[i++]; i++; // unused slot (if reused, bias by +4, since it was formerly a unit #) /* * Defaults to FDC.REG_STATUS.RQM set (ready for command) and FDC.REG_STATUS.READ_DATA clear (data direction * is from processor to the FDC Data Register). */ this.regStatus = data[i++]; /* * There can be up to 9 command bytes, and 7 result bytes, so 9 data registers are sufficient for communicating * in both directions (hence, the new Array(9) default above). */ this.regDataArray = data[i++]; /* * Determines the next data byte to be received. */ this.regDataIndex = data[i++]; /* * Determines the next data byte to be sent (internally, we use regDataIndex to read data bytes, up to this total). */ this.regDataTotal = data[i++]; this.regOutput = data[i++]; var dataDrives = data[i++]; /* * Initialize the disk history (if available) before initializing the drives, so that any disk deltas can be * applied to disk images that are already loaded. */ var aDiskHistory = data[i++]; if (aDiskHistory != null) this.aDiskHistory = aDiskHistory; if (this.aDrives === undefined) { this.nDrives = 4; // default to the maximum number of drives if (this.chipset) this.nDrives = this.chipset.getSWFloppyDrives(); /* * I would prefer to allocate only nDrives, but as discussed in the handling of the FDC.REG_DATA.CMD.SENSE_INT * command, we're faced with situations where the controller must respond to any drive in the range 0-3, regardless * how many drives are actually installed. We still rely upon nDrives to determine the number of drives displayed * to the user, however. */ this.aDrives = new Array(4); } for (iDrive = 0; iDrive < this.aDrives.length; iDrive++) { var drive = this.aDrives[iDrive]; if (drive === undefined) { /* * The first time each drive is initialized, we query its capacity (based on switches or CMOS) and set * the drive's physical limits accordingly (ie, max tracks, max heads, and max sectors/track). */ drive = this.aDrives[iDrive] = {}; var nKb = (this.chipset? this.chipset.getSWFloppyDriveSize(iDrive) : 0); switch(nKb) { case 160: case 180: drive.nHeads = 1; // required for single-sided drives only (all others default to double-sided) /* falls through */ case 320: case 360: default: // drives that don't have a recognized capacity default to 360 drive.nCylinders = 40; drive.nSectors = 9; // drives capable of writing 8 sectors/track can also write 9 sectors/track break; case 720: drive.nCylinders = 80; drive.nSectors = 9; break; case 1200: drive.nCylinders = 80; drive.nSectors = 15; break; case 1440: drive.nCylinders = 80; drive.nSectors = 18; break; } } if (!this.initDrive(drive, iDrive, dataDrives[iDrive])) { fSuccess = false; } } /* * regInput and regControl (port 0x3F7) were not present on controllers prior to MODEL_5170, which is why * we don't include initializers for them in the default data array; we could eliminate them on older models, * but we don't have access to the model info right now, and there's no real cost to always including them * in the FDC state. * * The bigger compatibility question is whether to always include hooks for them (see aPortInput and aPortOutput). */ this.regInput = data[i++] || 0; // TODO: Determine if we should default to FDC.REG_INPUT.DISK_CHANGE instead of 0 this.regControl = data[i] || FDC.REG_CONTROL.RATE500K; // default to maximum data rate if (DEBUG) this.messageDebugger("FDC initialized for " + this.aDrives.length + " drive(s)"); return fSuccess; }; /** * saveController() * * @this {FDC} * @return {Array} */ FDC.prototype.saveController = function() { var i = 0; var data = []; data[i++] = this.iDrive; data[i++] = 0; data[i++] = this.regStatus; data[i++] = this.regDataArray; data[i++] = this.regDataIndex; data[i++] = this.regDataTotal; data[i++] = this.regOutput; data[i++] = this.saveDrives(); data[i++] = this.saveDeltas(); data[i++] = this.regInput; data[i] = this.regControl; return data; }; /** * initDrive(drive, iDrive, data) * * TODO: Consider a separate Drive class that both FDC and HDC can use, since there's a lot of commonality * between the drive objects created by both controllers. This will clean up overall drive management and allow * us to factor out some common Drive methods (eg, advanceSector()). * * @this {FDC} * @param {Object} drive * @param {number} iDrive * @param {Array|undefined} data * @return {boolean} true if successful, false if failure */ FDC.prototype.initDrive = function(drive, iDrive, data) { var i = 0; var fSuccess = true; drive.iDrive = iDrive; if (data === undefined) { /* * We set a default of two heads (MODEL_5150 PCs originally shipped with single-sided drives, * but the ROM BIOS appears to have always supported both drive types). */ data = [FDC.REG_DATA.RES.RESET, true, 0, 2, 0]; } if (typeof data[1] == "boolean") { /* * Note that when no data is provided (eg, when the controller is being reinitialized), we now take * care to preserve any drive defaults that initController() already obtained for us, falling back to * bare minimums only when all else fails. */ data[1] = [ FDC.DEFAULT_DRIVE_NAME, // a[0] drive.nCylinders || 40, // a[1] drive.nHeads || data[3],// a[2] drive.nSectors || 9, // a[3] drive.cbSector || 512, // a[4] data[1], // a[5] drive.nDiskCylinders, // a[6] drive.nDiskHeads, // a[7] drive.nDiskSectors // a[8] ]; } /* * resCode used to be an FDC global, but in order to insulate FDC state from the operation of various functions * that operate on drive objects (eg, readByte and writeByte), I've made it a per-drive variable. This choice, * similar to my choice for handling PCN, may be contrary to how the actual hardware works, but I prefer this * approach, as long as it doesn't expose any incompatibilities that any software actually cares about. */ drive.resCode = data[i++]; /* * Some additional drive properties/defaults that are largely for the Disk component's benefit. */ var a = data[i++]; drive.name = a[0]; drive.nCylinders = a[1]; // cylinders drive.nHeads = a[2]; // heads/cylinders drive.nSectors = a[3]; // sectors/track drive.cbSector = a[4]; // bytes/sector drive.fRemovable = a[5]; /* * If we have current media parameters, restore them; otherwise, default to the drive's physical parameters. */ if (drive.nDiskCylinders = a[6]) { drive.nDiskHeads = a[7]; drive.nDiskSectors = a[8]; } else { drive.nDiskCylinders = drive.nCylinders; drive.nDiskHeads = drive.nHeads; drive.nDiskSectors = drive.nSectors; } /* * The next group of properties are set by various FDC command sequences. * * We initialize this.iDrive (above) and drive.bHead and drive.bCylinder (below) to zero, but leave the rest undefined, * awaiting their first FDC command. We do this because the initial SENSE_INT command returns a PCN, which will also * be undefined unless we have at least zeroed both the current drive and the "present" cylinder on that drive. * * Alternatively, I could make PCN a global FDC variable. That may be closer to how the actual hardware operates, * but I'm using per-drive variables so that the FDC component can be a good client to both the CPU and other components. * * COMPATIBILITY ALERT: The MODEL_5170 BIOS ("DSKETTE_SETUP") attempts to discern the drive type (double-density vs. * high-capacity) by "slapping" the heads around. Literally (it uses a constant named "TRK_SLAP" equal to 48). * After seeking to "TRK_SLAP", the BIOS performs a series of seeks, looking for the precise point where the heads * return to track 0. * * Here's how it works: the BIOS seeks to track 48 (which is fine on an 80-track 1.2Mb high-capacity drive, but 9 tracks * too far on a 40-track 360Kb double-density drive), then seeks to track 10, and then seeks in single-track increments * up to 10 more times until the SENSE_DRIVE command returns ST3 with the TRACK0 bit set. * * This implies that SEEK isn't really seeking to a specified cylinder, but rather it is calculating a delta from * the previous cylinder to the specified cylinder, and stepping over that number of tracks. Which means that SEEK * is updating a "logical" cylinder number, not the "physical" (actual) cylinder number. Presumably a RECALIBRATE * command will bring the logical and physical values into sync, but once an out-of-bounds cylinder is requested, they * will be out of sync. * * To simulate this, bCylinder is now treated as the "physical" cylinder (since that's how it's ALWAYS been used here), * and bCylinderSeek will now track (pun intended) the "logical" cylinder that's programmed via SEEK commands. */ drive.bHead = data[i++]; drive.bCylinderSeek = data[i++]; // the data[] slot where we used to store drive.nHeads (or -1) drive.bCylinder = data[i++]; if (drive.bCylinderSeek >= 100) { // verify that the saved bCylinderSeek is valid, otherwise sync it with bCylinder drive.bCylinderSeek -= 100; } else { drive.bCylinderSeek -= drive.bCylinder; } drive.bSector = data[i++]; drive.bSectorEnd = data[i++]; // aka EOT drive.nBytes = data[i++]; /* * We no longer reinitialize drive.disk, in order to retain previously mounted diskette across resets. */ /* * The next group of properties are managed by worker functions (eg, doRead()) to maintain state across DMA requests. */ drive.ibSector = data[i++]; // location of the next byte to be accessed in the current sector drive.sector = null; if (!drive.disk) { drive.sDiskettePath = ""; // ensure this is initialized to a default that displayDiskette() can deal with } var deltas = data[i++]; if (deltas === Computer.VERSION_102) { var sDisketteName = data[i++]; var sDiskettePath = data[i]; /* * If loadDiskette() must actually mount a *different* disk image at this late stage (ie, if it returns false), * then we must mark ourselves as "not ready" again, and add another "wait for ready" test in Computer before * finally powering the CPU. Otherwise, go ahead and restore any deltas to the current image. */ if (this.loadDiskette(iDrive, sDisketteName, sDiskettePath, true)) { if (drive.disk) { this.addDiskHistory(sDisketteName, sDiskettePath, drive.disk); } } else { this.setReady(false); } } else if (deltas !== undefined) { /* * If there's any data at all (ie, if this is a restore and not a reset), then it must be in the * pre-v1.02 save/restore format, so we'll restore as best we can, but be aware that if disk.restore() * notices that the currently mounted disk image differs from the disk image that these deltas belong to, * it will return false, and the restore operation will be aborted. */ if (drive.disk && drive.disk.restore(deltas) < 0) { fSuccess = false; } } /* * TODO: If loadDiskette() returned true, then this can happen immediately. Otherwise, loadDiskette() * will have merely "queued up" the load request and drive.disk won't be ready yet, so figure out how/when * we can properly restore drive.sector in that case. */ if (fSuccess && drive.disk && drive.ibSector !== undefined) { drive.sector = drive.disk.seek(drive.bCylinder, drive.bHead, drive.bSector); } return fSuccess; }; /** * saveDrives() * * @this {FDC} * @return {Array} */ FDC.prototype.saveDrives = function() { var i = 0; var data = []; for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) { data[i++] = this.saveDrive(this.aDrives[iDrive]); } return data; }; /** * saveDrive(drive) * * @this {FDC} * @return {Array} */ FDC.prototype.saveDrive = function(drive) { var i = 0; var data = []; data[i++] = drive.resCode; data[i++] = [drive.name, drive.nCylinders, drive.nHeads, drive.nSectors, drive.cbSector, drive.fRemovable, drive.nDiskCylinders, drive.nDiskHeads, drive.nDiskSectors]; data[i++] = drive.bHead; /* * We used to store drive.nHeads in the next slot, but now we store bCylinderSeek, * and we bias it by +100 so that initDrive() can distinguish it from older values. */ data[i++] = drive.bCylinderSeek + 100; data[i++] = drive.bCylinder; data[i++] = drive.bSector; data[i++] = drive.bSectorEnd; data[i++] = drive.nBytes; data[i++] = drive.ibSector; /* * Now we deviate from the 1.01a save format: instead of next storing all the deltas for the * currently mounted disk (if any), we store only the name and path of the currently mounted disk * (if any). Deltas for ALL disks, both currently mounted and previously mounted, are stored later. * * data[i++] = drive.disk? drive.disk.save() : null; * * To indicate this deviation, we store neither a null nor a delta array, but Computer.VERSION_102; * if that value is not present, then the restore code will know it's dealing with a pre-v1.02 state. */ data[i++] = Computer.VERSION_102; data[i++] = drive.sDisketteName; data[i] = drive.sDiskettePath; return data; }; /** * saveDeltas() * * This returns an array of entries, one for each disk image we've ever mounted, including any deltas; ie: * * [name, path, deltas] * * aDiskHistory contains exactly that, except that deltas may not be up-to-date for any currently mounted * disk image(s), so we call updateHistory() for all those disks, and then aDiskHistory is ready to be saved. * * @this {FDC} * @return {Array} */ FDC.prototype.saveDeltas = function() { for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) { var drive = this.aDrives[iDrive]; if (drive.disk) { this.updateDiskHistory(drive.sDisketteName, drive.sDiskettePath, drive.disk); } } return this.aDiskHistory; }; /** * copyDrive(iDrive) * * @this {FDC} * @param {number} iDrive * @return {Object|undefined} drive (which may be undefined if the requested drive does not exist) */ FDC.prototype.copyDrive = function(iDrive) { var driveNew; var driveOld = this.aDrives[iDrive]; if (driveOld !== undefined) { driveNew = {}; for (var p in driveOld) { driveNew[p] = driveOld[p]; } } return driveNew; }; /** * seekDrive(drive, iSector, nSectors) * * The FDC doesn't need this function, since all FDC requests from the CPU are handled by doCmd(). This function * is used by other components (eg, Debugger) to mimic an FDC request, using a drive object obtained from copyDrive(), * to avoid disturbing the internal state of the FDC's drive objects. * * Also note that in an actual FDC request, drive.nBytes is initialized to the size of a single sector; the extent * of the entire transfer is actually determined by a count that has been pre-loaded into the DMA controller. The FDC * isn't even aware of the extent of the transfer, so in the case of a read request, all readByte() can do is return bytes * until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted. * * Since seekDrive() is for use with non-DMA requests, we use nBytes to specify the length of the entire transfer. * * @this {FDC} * @param {Object} drive * @param {number} iSector (a "logical" sector number, relative to the entire disk, NOT a physical sector number) * @param {number} nSectors * @return {boolean} true if successful, false if invalid position request */ FDC.prototype.seekDrive = function(drive, iSector, nSectors) { if (drive.disk) { var aDiskInfo = drive.disk.info(); var nCylinders = aDiskInfo[0]; var nHeads = aDiskInfo[1]; var nSectorsPerTrack = aDiskInfo[2]; var nSectorsPerCylinder = nHeads * nSectorsPerTrack; var nSectorsPerDisk = nCylinders * nSectorsPerCylinder; if (iSector + nSectors <= nSectorsPerDisk) { drive.bCylinder = Math.floor(iSector / nSectorsPerCylinder); iSector %= nSectorsPerCylinder; drive.bHead = Math.floor(iSector / nSectorsPerTrack); drive.bSector = (iSector % nSectorsPerTrack) + 1; drive.nBytes = nSectors * aDiskInfo[3]; /* * NOTE: We don't set bSectorEnd, as an FDC command would, but it's irrelevant, because we don't actually * do anything with bSectorEnd at this point. Perhaps someday, when we faithfully honor/restrict requests * to a single track (or a single cylinder, in the case of multi-track requests). */ drive.resCode = FDC.REG_DATA.RES.NONE; /* * At this point, we've finished simulating what an FDC.REG_DATA.CMD.READ_DATA command would have performed, * up through doRead(). Now it's the caller responsibility to call readByte(), just like the DMA Controller would. */ return true; } } return false; }; /** * autoMount(fRemount) * * @this {FDC} * @param {boolean} [fRemount] is true if we're remounting all auto-mounted diskettes * @return {boolean} true if one or more diskette images are being auto-mounted, false if none */ FDC.prototype.autoMount = function(fRemount) { if (!fRemount) this.cAutoMount = 0; if (this.pAutoMount) { for (var sDrive in this.pAutoMount) { var pDriveConfig = this.pAutoMount[sDrive]; if (pDriveConfig['name'] && pDriveConfig['path']) { /* * WARNING: This conversion of drive letter to drive number, starting with A:, is very simplistic * and is not guaranteed to match the drive mapping that DOS ultimately uses. */ var iDrive = sDrive.charCodeAt(0) - 0x41; if (iDrive >= 0 && iDrive < this.aDrives.length) { if (!this.loadDiskette(iDrive, pDriveConfig['name'], pDriveConfig['path'], true) && fRemount) this.setReady(false); continue; } } this.notice("Unrecognized auto-mount specification for drive " + sDrive); } } return !!this.cAutoMount; }; /** * loadDiskette(iDrive, sDisketteName, sDiskettePath, fAutoMount) * * NOTE: If sDiskettePath is already loaded in the drive, nothing needs to be done. * * @this {FDC} * @param {number} iDrive (pre-validated) * @param {string} sDisketteName * @param {string|null} sDiskettePath * @param {boolean} fAutoMount * @return {boolean} true if diskette (already) loaded, false if queued up (or busy) */ FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAutoMount) { var drive = this.aDrives[iDrive]; if (sDiskettePath && drive.sDiskettePath != sDiskettePath) { this.unloadDrive(iDrive, fAutoMount, true); if (drive.fBusy) { this.notice("Drive " + iDrive + " busy"); return true; } drive.fBusy = true; if (fAutoMount) { drive.fAutoMount = true; this.cAutoMount++; this.messageDebugger("loading diskette '" + sDisketteName + "'"); } var disk = new Disk(this, drive, DiskAPI.MODE.PRELOAD); disk.load(sDisketteName, sDiskettePath, this.doneLoadDiskette); return false; } return true; }; /** * doneLoadDiskette(drive, disk, sDisketteName, sDiskettePath) * * @this {FDC} * @param {Object} drive * @param {Disk} disk is set if the disk was successfully loaded, null if not * @param {string} sDisketteName * @param {string} sDiskettePath */ FDC.prototype.doneLoadDiskette = function onFDCLoadNotify(drive, disk, sDisketteName, sDiskettePath) { var aDiskInfo; drive.fBusy = false; if (disk) { /* * We shouldn't mount the diskette unless the drive is able to handle it; for example, FD360 (40-track) * drives cannot read FD1200 (80-track) diskettes. However, I no longer require that the diskette's * sectors/track fall within the drive's standard maximum, because XDF diskettes use 19 physical sectors/track * on the first cylinder (1 more than the typical 18 sectors/track found on 1.44Mb diskettes) but declare * a larger logical size (23 512-byte sectors/track) to reflect the actual capacity of XDF tracks beyond the * first cylinder (ie, one 8Kb sector, one 2Kb sector, one 1Kb sector, and one 512-byte sector). */ aDiskInfo = disk.info(); if (disk && aDiskInfo[0] > drive.nCylinders || aDiskInfo[1] > drive.nHeads /* || aDiskInfo[2] > drive.nSectors */) { this.notice("Diskette \"" + sDisketteName + "\" too large for drive " + String.fromCharCode(0x41 + drive.iDrive)); disk = null; } } if (disk) { drive.disk = disk; drive.sDisketteName = sDisketteName; drive.sDiskettePath = sDiskettePath; this.addDiskHistory(sDisketteName, sDiskettePath, disk); /* * Clearly, a successful mount implies a disk change, and I suppose that, technically, an *unsuccessful* * mount should imply the same, but what would the real-world analog be? Inserting a piece of cardboard * instead of an actual diskette? In any case, if we can do the user a favor by pretending (as far as the * disk change line is concerned) that an unsuccessful mount never happened, let's do it. * * Successful unmounts are a different story, however; those *do* trigger a change. See unloadDrive(). */ this.regInput |= FDC.REG_INPUT.DISK_CHANGE; /* * With the addition of notify(), users are now "alerted" whenever a diskette has finished loading; * notify() is selective about its output, using print() if a print window is open, alert() otherwise. * * WARNING: This conversion of drive number to drive letter, starting with A:, is very simplistic * and will not match the drive mappings that DOS ultimately uses (ie, for drives beyond B:). */ this.notice("Mounted diskette \"" + sDisketteName + "\" in drive " + String.fromCharCode(0x41 + drive.iDrive), drive.fAutoMount); /* * Update the drive's current media parameters to match the disk's. */ drive.nDiskCylinders = aDiskInfo[0]; drive.nDiskHeads = aDiskInfo[1]; drive.nDiskSectors = aDiskInfo[2]; } if (drive.fAutoMount) { drive.fAutoMount = false; if (!--this.cAutoMount) this.setReady(); } this.displayDiskette(drive.iDrive); }; /** * displayDiskette(iDrive, fUpdateDrive) * * @this {FDC} * @param {number} iDrive (unvalidated) * @param {boolean} [fUpdateDrive] is true to update the drive list to match the specified drive (eg, the auto-mount case) */ FDC.prototype.displayDiskette = function(iDrive, fUpdateDrive) { /* * First things first: validate iDrive. */ if (iDrive >= 0 && iDrive < this.aDrives.length) { var drive = this.aDrives[iDrive]; var controlDisks = this.bindings["listDisks"]; var controlDrives = this.bindings["listDrives"]; /* * Next, make sure controls for both drives and disks exist. */ if (controlDisks && controlDrives) { /* * Next, make sure the drive whose disk we're updating is the currently selected drive. */ var i; var iDriveSelected = parseInt(controlDrives.value, 10); if (!isNaN(iDriveSelected) && iDriveSelected == iDrive) { for (i = 0; i < controlDisks.options.length; i++) { if (controlDisks.options[i].value == drive.sDiskettePath) { if (controlDisks.selectedIndex != i) { controlDisks.selectedIndex = i; } break; } } if (i == controlDisks.options.length) controlDisks.selectedIndex = 0; } if (fUpdateDrive) { for (i = 0; i < controlDrives.options.length; i++) { if (parseInt(controlDrives.options[i].value, 10) == drive.iDrive) { if (controlDrives.selectedIndex != i) { controlDrives.selectedIndex = i; } break; } } } } } }; /** * unloadDrive(iDrive, fAutoUnload, fQuiet) * * @this {FDC} * @param {number} iDrive (pre-validated) * @param {boolean} [fAutoUnload] is true if this unload is being forced as part of an automount and/or restored mount * @param {boolean} [fQuiet] */ FDC.prototype.unloadDrive = function(iDrive, fAutoUnload, fQuiet) { var drive = this.aDrives[iDrive]; if (drive.disk) { /* * Before we toss the disk's information, capture any deltas that may have occurred. */ this.updateDiskHistory(drive.sDisketteName, drive.sDiskettePath, drive.disk); drive.sDisketteName = ""; drive.sDiskettePath = ""; drive.disk = null; this.regInput |= FDC.REG_INPUT.DISK_CHANGE; /* * WARNING: This conversion of drive number to drive letter, starting with A:, is very simplistic * and is not guaranteed to match the drive mapping that DOS ultimately uses. */ if (!fQuiet) { this.notice("Drive " + String.fromCharCode(0x41 + iDrive) + " unloaded", fAutoUnload); } /* * Try to avoid any unnecessary hysteresis regarding the diskette display if this unload is merely * a prelude to another load. */ if (!fAutoUnload && !fQuiet) { this.displayDiskette(iDrive); } } }; /** * unloadAllDrives(fDiscard) * * @this {FDC} * @param {boolean} fDiscard to discard all disk history before unloading */ FDC.prototype.unloadAllDrives = function(fDiscard) { if (fDiscard) { this.aDiskHistory = []; } for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) { this.unloadDrive(iDrive, true); } }; /** * addDiskHistory(sDisketteName, sDiskettePath, disk) * * @this {FDC} * @param {string} sDisketteName * @param {string} sDiskettePath * @param {Disk} disk containing corresponding disk image */ FDC.prototype.addDiskHistory = function(sDisketteName, sDiskettePath, disk) { var i; for (i = 0; i < this.aDiskHistory.length; i++) { if (this.aDiskHistory[i][1] == sDiskettePath) { var nChanges = disk.restore(this.aDiskHistory[i][2]); if (DEBUG) this.messageDebugger("disk '" + sDisketteName + "' restored from history (" + nChanges + " changes)"); return; } } if (DEBUG) this.messageDebugger("disk '" + sDisketteName + "' added to history (" + sDiskettePath + ")"); this.aDiskHistory[i] = [sDisketteName, sDiskettePath, []]; }; /** * removeDiskHistory(sDisketteName, sDiskettePath) * * @this {FDC} * @param {string} sDisketteName * @param {string} sDiskettePath */ FDC.prototype.removeDiskHistory = function(sDisketteName, sDiskettePath) { var i; for (i = 0; i < this.aDiskHistory.length; i++) { if (this.aDiskHistory[i][1] == sDiskettePath) { this.aDiskHistory.splice(i, 1); if (DEBUG) this.messageDebugger("disk '" + sDisketteName + "' removed from history"); return; } } if (DEBUG) this.messageDebugger("unable to remove disk '" + sDisketteName + "' from history (" + sDiskettePath + ")"); }; /** * updateDiskHistory(sDisketteName, sDiskettePath, disk) * * @this {FDC} * @param {string} sDisketteName * @param {string} sDiskettePath * @param {Disk} disk containing corresponding disk image, with possible deltas */ FDC.prototype.updateDiskHistory = function(sDisketteName, sDiskettePath, disk) { var i; for (i = 0; i < this.aDiskHistory.length; i++) { if (this.aDiskHistory[i][1] == sDiskettePath) { this.aDiskHistory[i][2] = disk.save(); if (DEBUG) this.messageDebugger("disk '" + sDisketteName + "' updated in history"); return; } } /* * I used to report this as an error (at least in the DEBUG release), but it's no longer really * an error, because if we're trying to re-mount a clean copy of a disk, we toss its history, then * unload, and then reload/remount. And since unloadDrive's normal behavior is to call updateDiskHistory() * before unloading, the fact that the disk is no longer listed here can't be treated as an error. */ if (DEBUG) this.messageDebugger("unable to update disk '" + sDisketteName + "' in history (" + sDiskettePath + ")"); }; /** * outFDCOutput(port, bOut, addrFrom) * * @this {FDC} * @param {number} port (0x3F2, output only) * @param {number} bOut * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) */ FDC.prototype.outFDCOutput = function(port, bOut, addrFrom) { this.messagePort(port, bOut, addrFrom, "OUTPUT"); if (!(bOut & FDC.REG_OUTPUT.ENABLE)) { this.initController(); /* * initController() resets, among other things, the selected drive (this.iDrive), so if we were * still updating this.iDrive below based on the "drive select" bits in regOutput, we would want * to make sure those bits now match what initController() set. But since we no longer do that * (see below), this is no longer needed either. * * bOut = (bOut & ~FDC.REG_OUTPUT.DS) | this.iDrive; */ } else if (!(this.regOutput & FDC.REG_OUTPUT.ENABLE)) { /* * When FDC.REG_OUTPUT.ENABLE transitions from 0 to 1, generate an interrupt. */ if (this.regOutput & FDC.REG_OUTPUT.INT_ENABLE) { if (this.chipset) this.chipset.setIRR(ChipSet.IRQ.FDC); } } /* * This no longer updates the internally selected drive (this.iDrive) based on regOutput, because (a) there seems * to be no point, as all drive-related commands include their own "drive select" bits, and (b) it breaks the * MODEL_5170 boot code. Here's why: * * Unlike previous models, the MODEL_5170 BIOS probes all installed diskette drives to determine drive type; * ie, FD360 (40-track) or FD1200 (80-track). So if there are two drives, the last selected drive will be drive 1. * Immediately before booting, the BIOS issues an INT 0x13/AH=0 reset, which writes regOutput two times: first * with FDC.REG_OUTPUT.ENABLE clear, and then with it set. However, both times, it ALSO loads the last selected * drive number into regOutput's "drive select" bits. * * If we switched our selected drive to match regOutput, then the ST0 value we returned on an SENSE_INT command * following the regOutput reset operation would indicate drive 1 instead of drive 0. But the BIOS requires * the ST0 result from the SENSE_INT command ALWAYS be 0xC0 (not 0xC1), so the controller must not be propagating * regOutput's "drive select" bits in the way I originally assumed. * * var iDrive = bOut & FDC.REG_OUTPUT.DS; * if (bOut & (FDC.REG_OUTPUT.MOTOR_D0 << iDrive)) this.iDrive = iDrive; */ this.regOutput = bOut; }; /** * inFDCStatus(port, addrFrom) * * @this {FDC} * @param {number} port (0x3F4, input only) * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) * @return {number} simulated port value */ FDC.prototype.inFDCStatus = function(port, addrFrom) { this.messagePort(port, null, addrFrom, "STATUS", this.regStatus); return this.regStatus; }; /** * inFDCData(port, addrFrom) * * @this {FDC} * @param {number} port (0x3F5, input/output) * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) * @return {number} simulated port value */ FDC.prototype.inFDCData = function(port, addrFrom) { var bIn = 0; if (this.regDataIndex < this.regDataTotal) { bIn = this.regDataArray[this.regDataIndex]; } /* * As per the discussion in doCmd(), once the first byte of the Result Phase has been read, the interrupt must be cleared. */ if (this.regOutput & FDC.REG_OUTPUT.INT_ENABLE) { if (this.chipset) this.chipset.clearIRR(ChipSet.IRQ.FDC); } this.messagePort(port, null, addrFrom, "DATA[" + this.regDataIndex + "]", bIn); if (++this.regDataIndex >= this.regDataTotal) { this.regStatus &= ~(FDC.REG_STATUS.READ_DATA | FDC.REG_STATUS.BUSY); this.regDataIndex = this.regDataTotal = 0; } return bIn; }; /** * outFDCData(port, bOut, addrFrom) * * @this {FDC} * @param {number} port (0x3F5, input/output) * @param {number} bOut * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) */ FDC.prototype.outFDCData = function(port, bOut, addrFrom) { this.messagePort(port, bOut, addrFrom, "DATA[" + this.regDataTotal + "]"); if (this.regDataTotal < this.regDataArray.length) { this.regDataArray[this.regDataTotal++] = bOut; } var bCmd = this.regDataArray[0]; var bCmdMasked = bCmd & FDC.REG_DATA.CMD.MASK; if (FDC.aCmdInfo[bCmdMasked] !== undefined) { if (this.regDataTotal >= FDC.aCmdInfo[bCmdMasked].cbReq) { this.doCmd(); } return; } if (DEBUG) { this.messageDebugger("unsupported FDC command: " + str.toHexByte(bCmd)); if (DEBUGGER) this.cpu.haltCPU(); } }; /** * inFDCInput(port, addrFrom) * * @this {FDC} * @param {number} port (0x3F7, input only, MODEL_5170 only) * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) * @return {number} simulated port value */ FDC.prototype.inFDCInput = function(port, addrFrom) { var bIn = this.regInput; /* * TODO: Determine when the DISK_CHANGE bit is *really* cleared (this is just a guess) */ this.regInput &= ~FDC.REG_INPUT.DISK_CHANGE; this.messagePort(port, null, addrFrom, "INPUT", bIn); return bIn; }; /** * outFDCControl(port, bOut, addrFrom) * * @this {FDC} * @param {number} port (0x3F7, output only, MODEL_5170 only) * @param {number} bOut * @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port) */ FDC.prototype.outFDCControl = function(port, bOut, addrFrom) { this.messagePort(port, bOut, addrFrom, "CONTROL"); this.regControl = bOut; }; /** * doCmd() * * @this {FDC} */ FDC.prototype.doCmd = function() { var fIRQ = false; this.regDataIndex = 0; var bCmd = this.popCmd(); var drive, bDrive, bHead, c, h, r, n; /* * NOTE: We currently ignore the FDC.REG_DATA.CMD.SK, FDC.REG_DATA.CMD.MF and FDC.REG_DATA.CMD.MT bits of every command. * The only command bit of possible interest down the road might be the FDC.REG_DATA.CMD.MT (Multi-Track); the rest relate * to storage format details that we cannot emulate as long as our diskette images contain nothing more than sector * data without any formatting data. * * Similarly, we ignore parameters like SRT, HUT, HLT and the like, since our "motors" don't require physical delays; * however, if timing issues become compatibility issues, we'll have to revisit those delays. In any case, the maximum * speed of the simulation will still be limited by various spin-loops in the ROM BIOS that wait prescribed times, so even * with infinitely fast hardware, the simulation will never run as fast as it theoretically could, unless we opt to identify * those spin-loops and either patch them or skip over them. */ var bCmdMasked = bCmd & FDC.REG_DATA.CMD.MASK; switch (bCmdMasked) { case FDC.REG_DATA.CMD.SPECIFY: // 0x03 this.popSRT(); // SRT and HUT (encodings?) this.popHLT(); // HLT and ND (encodings?) this.beginResult(); /* * No results are provided by this command, and fIRQ should remain false */ break; case FDC.REG_DATA.CMD.SENSE_DRIVE: // 0x04 bDrive = this.popCmd(FDC.TERMS.DS); bHead = (bDrive >> 2) & 0x1; this.iDrive = (bDrive & 0x3); drive = this.aDrives[this.iDrive]; this.beginResult(); this.pushST3(drive); break; case FDC.REG_DATA.CMD.WRITE_DATA: // 0x05 case FDC.REG_DATA.CMD.READ_DATA: // 0x06 bDrive = this.popCmd(FDC.TERMS.DS); // Drive Select bHead = (bDrive >> 2) & 0x1; // isolate HD (Head Select) bits this.iDrive = (bDrive & 0x3); // isolate DS (Drive Select, aka Unit Select) bits drive = this.aDrives[this.iDrive]; drive.bHead = bHead; c = drive.bCylinder = this.popCmd(FDC.TERMS.C); // C h = this.popCmd(FDC.TERMS.H); // H /* * Controller docs say that H should always match HD, so I assert that, but what if someone * made a mistake and didn't program them identically -- what would happen? Which should we honor? */ Component.assert(h == bHead); r = drive.bSector = this.popCmd(FDC.TERMS.R); // R n = this.popCmd(FDC.TERMS.N); // N drive.nBytes = 128 << n; // 0 => 128, 1 => 256, 2 => 512, 3 => 1024 drive.bSectorEnd = this.popCmd(FDC.TERMS.EOT); // EOT (final sector number on a cylinder) this.popCmd(FDC.TERMS.GPL); // GPL (spacing between sectors, excluding VCO Sync Field; 3) this.popCmd(FDC.TERMS.DTL); // DTL (when N is 0, DTL stands for the data length to read out or write into the sector) if (bCmdMasked == FDC.REG_DATA.CMD.READ_DATA) this.doRead(drive); else this.doWrite(drive); this.pushResults(drive, bCmd, bHead, c, h, r, n); fIRQ = true; break; case FDC.REG_DATA.CMD.RECALIBRATE: // 0x07 bDrive = this.popCmd(FDC.TERMS.DS); this.iDrive = (bDrive & 0x3); drive = this.aDrives[this.iDrive]; drive.bCylinder = drive.bCylinderSeek = 0; drive.resCode = FDC.REG_DATA.RES.SEEK_END | FDC.REG_DATA.RES.TRACK0; this.beginResult(); // no results provided; this command is typically followed by FDC.REG_DATA.CMD.SENSE_INT fIRQ = true; break; case FDC.REG_DATA.CMD.SENSE_INT: // 0x08 drive = this.aDrives[this.iDrive]; drive.bHead = 0; // this command is documented as ALWAYS returning a head address of 0 in ST0; see pushST0() this.beginResult(); this.pushST0(drive); this.pushResult(drive.bCylinder, FDC.TERMS.PCN); /* * For some strange reason, the "DISK_RESET" function in the MODEL_5170_REV3 BIOS resets the * adapter and then issues FOUR -- that's right, not ONE but FOUR -- SENSE INTERRUPT STATUS commands * in a row, and expects ST0 to contain a different drive number after each command (first 0, then 1, * then 2, and finally 3). What makes this doubly weird is SENSE INTERRUPT STATUS (unlike SENSE * DRIVE STATUS) is a drive-agnostic command. * * Didn't the original PC AT "HFCOMBO" controller limit support to TWO diskette drives max? * And even if the PC AT supported other FDC controllers that DID support up to FOUR diskette drives, * why should "DISK_RESET" hard-code a 4-drive loop? * * Well, whatever. All this head-scratching doesn't change the fact that I apparently have to * "auto-increment" the internal drive number (this.iDrive) after each SENSE INTERRUPT STATUS command. */ this.iDrive = (this.iDrive + 1) & 0x3; /* * No interrupt is generated by this command, so fIRQ should remain false. */ break; case FDC.REG_DATA.CMD.READ_ID: // 0x0A /* * This command is used by "SETUP_DBL" in the MODEL_5170_REV3 BIOS to determine if a double-density * (40-track) diskette has been inserted in a high-density (80-track) drive; ie, whether "double stepping" * is required, since only 40 of the 80 possible "steps" are valid for a double-density diskette. * * To start, we'll focus on making this work in the normal case (80-track diskette in 80-track drive). */ bDrive = this.popCmd(FDC.TERMS.DS); bHead = (bDrive >> 2) & 0x1; this.iDrive = (bDrive & 0x3); drive = this.aDrives[this.iDrive]; c = drive.bCylinder; h = drive.bHead = bHead; r = drive.bSector = 1; n = 0; drive.resCode = FDC.REG_DATA.RES.NONE; if (drive.disk && (drive.sector = drive.disk.seek(drive.bCylinder, drive.bHead, drive.bSector))) { n = drive.sector.length; } else { /* * TODO: Determine the appropriate response code(s) for the possible errors that can occur here. */ drive.resCode = FDC.REG_DATA.RES.NO_DATA | FDC.REG_DATA.RES.INCOMPLETE; } this.pushResults(drive, bCmd, bHead, c, h, r, n); fIRQ = true; break; case FDC.REG_DATA.CMD.FORMAT_TRACK: // 0x0D bDrive = this.popCmd(FDC.TERMS.DS); bHead = (bDrive >> 2) & 0x1; this.iDrive = (bDrive & 0x3); drive = this.aDrives[this.iDrive]; c = drive.bCylinder; h = drive.bHead = bHead; r = 1; n = this.popCmd(FDC.TERMS.N); // N drive.nBytes = 128 << n; // 0 => 128, 1 => 256, 2 => 512, 3 => 1024 (bytes/sector) drive.bSectorEnd = this.popCmd(FDC.TERMS.SC); // SC (sectors/track) this.popCmd(FDC.TERMS.GPL); // GPL (spacing between sectors, excluding VCO Sync Field; 3) drive.bFiller = this.popCmd(FDC.TERMS.D); // D (filler byte) this.doFormat(drive); this.pushResults(drive, bCmd, bHead, c, h, r, n); fIRQ = true; break; case FDC.REG_DATA.CMD.SEEK: // 0x0F bDrive = this.popCmd(FDC.TERMS.DS); bHead = (bDrive >> 2) & 0x1; this.iDrive = (bDrive & 0x3); drive = this.aDrives[this.iDrive]; drive.bHead = bHead; /* * As discussed in initDrive(), we can no longer simply set bCylinder to the specified NCN; * instead, we must calculate the delta between bCylinderSeek and the NCN, and adjust bCylinder * by that amount. Then we simply move the NCN into bCylinderSeek without any range checking. * * Since bCylinder is now expressly defined as the "physical" cylinder number, it must never * be allowed to exceed the physical boundaries of the drive (ie, never lower than 0, and never * greater than or equal to nCylinders). */ c = this.popCmd(FDC.TERMS.NCN); drive.bCylinder += c - drive.bCylinderSeek; if (drive.bCylinder < 0) drive.bCylinder = 0; if (drive.bCylinder >= drive.nCylinders) drive.bCylinder = drive.nCylinders - 1; drive.bCylinderSeek = c; drive.resCode = FDC.REG_DATA.RES.SEEK_END; /* * TODO: To properly support ALL the ST3 result bits (not just TRACK0), we need a resCode * update() function that all FDC commands can use. This code is merely sufficient to get us * through the "DSKETTE_SETUP" gauntlet in the MODEL_5170 BIOS. */ if (drive.bCylinder == 0) { drive.resCode |= FDC.REG_DATA.RES.TRACK0; } this.beginResult(); // like FDC.REG_DATA.CMD.RECALIBRATE, no results are provided fIRQ = true; break; default: if (DEBUG) { this.messageDebugger("FDC operation unsupported (command=0x: " + str.toHexByte(bCmd) + ")"); if (DEBUGGER) this.cpu.haltCPU(); } break; } if (this.regDataTotal > 0) this.regStatus |= (FDC.REG_STATUS.READ_DATA | FDC.REG_STATUS.BUSY); /* * After the Execution Phase (eg, DMA Terminal Count has occurred, or the EOT sector has been read/written), * an interrupt is supposed to occur, signaling the beginning of the Result Phase. Once the first byte of the * result has been read, the interrupt is cleared (see inFDCData). * * TODO: Technically, interrupt request status should be cleared by the FDC.REG_DATA.CMD.SENSE_INT command; in fact, * if that command is issued and no interrupt was pending, then FDC.REG_DATA.RES.INVALID should be returned (via ST0). */ if (this.regOutput & FDC.REG_OUTPUT.INT_ENABLE) { if (drive && !(drive.resCode & FDC.REG_DATA.RES.NOT_READY) && fIRQ) { if (this.chipset) this.chipset.setIRR(ChipSet.IRQ.FDC); } } }; /** * pushResults(drive, bCmd, bHead, c, h, r, n) * * @param {Object} drive * @param {number} bCmd * @param {number} bHead * @param {number} c * @param {number} h * @param {number} r * @param {number} n */ FDC.prototype.pushResults = function(drive, bCmd, bHead, c, h, r, n) { this.beginResult(); this.pushST0(drive); this.pushST1(drive); this.pushST2(drive); /* * NOTE: I used to set the following C/H/R/N results using the values that advanceSector() had "advanced" * them to, which seemed logical but was technically incorrect. For non-multi-track reads, they should match * the programmed C/H/R/N values, except when EOT has been reached, in which case C = C + 1 and R = 1. * * For multi-track, the LSB of H should be complemented whenever EOT has been reached, which I "informally" * detect by testing if the drive's current bCylinder and/or bHead positions advanced to a new cylinder or head, * and apparently, C should never be advanced if H was initially 0. * * I don't do strict EOT comparisons here or elsewhere, because it allows the controller to work with a wider * range of disks (eg, "fake" XDF disk images that contain 23 512-byte sectors/track). */ var i = 0; if (c != drive.bCylinder || h != drive.bHead) { i = r = 1; } if (bCmd & FDC.REG_DATA.CMD.MT) { h ^= i; if (!bHead) i = 0; } c += i; this.pushResult(c, FDC.TERMS.C); // formerly drive.bCylinder this.pushResult(h, FDC.TERMS.H); // formerly drive.bHead this.pushResult(r, FDC.TERMS.R); // formerly drive.bSector this.pushResult(n, FDC.TERMS.N); }; /** * popCmd(name) * * @this {FDC} * @param {string|undefined} [name] * @return {number} */ FDC.prototype.popCmd = function(name) { Component.assert((!this.regDataIndex || name !== undefined) && this.regDataIndex < this.regDataTotal); var bCmd = this.regDataArray[this.regDataIndex]; if (DEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled(FDC.MESSAGE_FDC)) { var bCmdMasked = bCmd & FDC.REG_DATA.CMD.MASK; if (!name && !this.regDataIndex && FDC.aCmdInfo[bCmdMasked]) name = FDC.aCmdInfo[bCmdMasked].name; this.dbg.message("FDC.CMD[" + (name || this.regDataIndex) + "]: 0x" + str.toHexByte(bCmd)); } this.regDataIndex++; return bCmd; }; /** * popHLT() * * NOTE: This byte is actually a combination of HLT (Head Load Time) and ND (Non-DMA Mode) * * @this {FDC} */ FDC.prototype.popHLT = function() { this.popCmd(FDC.TERMS.HLT); // this.nHLT = this.popCmd(FDC.TERMS.HLT); }; /** * popSRT() * * NOTE: This byte is actually a combination of SRT (Step Rate Time) and HUT (Head Unload Time) * * @this {FDC} */ FDC.prototype.popSRT = function() { this.popCmd(FDC.TERMS.SRT); // this.nSRT = this.popCmd(FDC.TERMS.SRT); }; /** * beginResult() * * @this {FDC} */ FDC.prototype.beginResult = function() { this.regDataIndex = this.regDataTotal = 0; }; /** * pushResult(bResult, name) * * @this {FDC} * @param {number} bResult * @param {string|undefined} [name] */ FDC.prototype.pushResult = function(bResult, name) { if (DEBUG) this.messageDebugger("FDC.RES[" + (name || this.regDataTotal) + "]: 0x" + str.toHexByte(bResult), FDC.MESSAGE_FDC); this.regDataArray[this.regDataTotal++] = bResult; }; /** * pushST0(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.pushST0 = function(drive) { this.pushResult(drive.iDrive | (drive.bHead << 2) | (drive.resCode & FDC.REG_DATA.RES.ST0), FDC.TERMS.ST0); }; /** * pushST1(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.pushST1 = function(drive) { this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST1) >>> 8, FDC.TERMS.ST1); }; /** * pushST2(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.pushST2 = function(drive) { this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST2) >>> 16, FDC.TERMS.ST2); }; /** * pushST3(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.pushST3 = function(drive) { this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST3) >>> 24, FDC.TERMS.ST3); }; /** * dmaRead(drive, b, done) * * @this {FDC} * @param {Object} drive * @param {number} b * @param {function(number,boolean)} done */ FDC.prototype.dmaRead = function(drive, b, done) { if (b === undefined || b < 0) { this.readByte(drive, done); return; } /* * The DMA controller should be ASKING for data, not GIVING us data; this suggests an internal DMA miscommunication */ if (DEBUG) this.messageDebugger("dmaRead(): invalid DMA acknowledgement"); done(-1, false); }; /** * dmaWrite(drive, b) * * @this {FDC} * @param {Object} drive * @param {number} b * @return {number} */ FDC.prototype.dmaWrite = function(drive, b) { if (b !== undefined && b >= 0) return this.writeByte(drive, b); /* * The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication */ if (DEBUG) this.messageDebugger("dmaWrite(): invalid DMA acknowledgement"); return -1; }; /** * dmaFormat(drive, b) * * @this {FDC} * @param {Object} drive * @param {number} b * @returns {number} */ FDC.prototype.dmaFormat = function(drive, b) { if (b !== undefined && b >= 0) return this.writeFormat(drive, b); /* * The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication */ if (DEBUG) this.messageDebugger("dmaFormat(): invalid DMA acknowledgement"); return -1; }; /** * doRead(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.doRead = function(drive) { /* * With only NOT_READY and INCOMPLETE set, an empty drive causes DOS to report "General Failure"; * with the addition of NO_DATA, DOS reports "Sector not found". The traditional "Drive not ready" * error message is not triggered by anything we return here, but simply by BIOS commands timing out. */ drive.resCode = FDC.REG_DATA.RES.NOT_READY | FDC.REG_DATA.RES.INCOMPLETE; if (DEBUG) this.messageDebugger("FDC.doRead(" + drive.bCylinder + ":" + drive.bHead + ":" + drive.bSector + ":" + drive.nBytes + ")"); if (drive.disk) { drive.sector = null; drive.resCode = FDC.REG_DATA.RES.NONE; if (this.chipset) { this.chipset.connectDMA(ChipSet.DMA_FDC, this, 'dmaRead', drive); this.chipset.requestDMA(ChipSet.DMA_FDC); } } }; /** * doWrite(drive) * * @this {FDC} * @param {Object} drive */ FDC.prototype.doWrite = function(drive) { drive.resCode = FDC.REG_DATA.RES.NOT_READY | FDC.REG_DATA.RES.INCOMPLETE; if (DEBUG) this.messageDebugger("FDC.doWrite(" + drive.bCylinder + ":" + drive.bHead + ":" + drive.bSector + ":" + drive.nBytes + ")"); if (drive.disk) { if (drive.disk.fWriteProtected) { drive.resCode = FDC.REG_DATA.RES.NOT_WRITABLE | FDC.REG_DATA.RES.INCOMPLETE; return; } drive.sector = null; drive.resCode = FDC.REG_DATA.RES.NONE; if (this.chipset) { this.chipset.connectDMA(ChipSet.DMA_FDC, this, 'dmaWrite', drive); this.chipset.requestDMA(ChipSet.DMA_FDC); } } }; /** * doFormat(drive) * * drive is initialized by doCmd() to the following extent: * * drive.bHead (ignored) * drive.nBytes (bytes/sector) * drive.bSectorEnd (sectors/track) * drive.bFiller (fill byte) * * and we expect the DMA controller to provide C, H, R and N (ie, 4 bytes) for each sector to be formatted. * * @this {FDC} * @param {Object} drive */ FDC.prototype.doFormat = function(drive) { drive.resCode = FDC.REG_DATA.RES.NOT_READY | FDC.REG_DATA.RES.INCOMPLETE; if (drive.disk) { drive.sector = null; drive.resCode = FDC.REG_DATA.RES.NONE; if (this.chipset) { drive.cbFormat = 0; drive.abFormat = new Array(4); drive.bFormatting = true; drive.cSectorsFormatted = 0; this.chipset.connectDMA(ChipSet.DMA_FDC, this, 'dmaFormat', drive); this.chipset.requestDMA(ChipSet.DMA_FDC); drive.bFormatting = false; } } }; /** * readByte(drive) * * The following drive properties must have been setup prior to our first call: * * drive.bHead * drive.bCylinder * drive.bSector * drive.sector (initialized to null) * * On the first readByte() request, since drive.sector will be null, we ask the Disk object to look * up the first sector of the request. We then ask the Disk for bytes from that sector until the sector * is exhausted, and then we look up the next sector and continue the process. * * NOTE: Since the FDC isn't aware of the extent of the transfer, all readByte() can do is return bytes * until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted. * * TODO: Research the requirements, if any, for multi-track I/O and determine what else needs to be done. * * @this {FDC} * @param {Object} drive * @param {function(number,boolean)} done (number is next available byte from drive, or -1 if no more bytes available) */ FDC.prototype.readByte = function(drive, done) { var b = -1; if (!drive.resCode && drive.disk) { do { if (drive.sector) { if ((b = drive.disk.read(drive.sector, drive.ibSector++)) >= 0) break; } /* * Locate the next sector, and then try reading again. */ drive.sector = drive.disk.seek(drive.bCylinder, drive.bHead, drive.bSector); if (!drive.sector) { drive.resCode = FDC.REG_DATA.RES.NO_DATA | FDC.REG_DATA.RES.INCOMPLETE; break; } drive.ibSector = 0; /* * We "pre-advance" bSector et al now, instead of waiting to advance it right before the seek(). * This allows the initial call to readByte() to perform a seek without triggering an unwanted advance. */ this.advanceSector(drive); } while (true); } done(b, false); }; /** * writeByte(drive, b) * * The following drive properties must have been setup prior to our first call: * * drive.bHead * drive.bCylinder * drive.bSector * drive.sector (initialized to null) * * On the first writeByte() request, since drive.sector will be null, we ask the Disk object to look * up the first sector of the request. We then send the Disk bytes for that sector until the sector * is full, and then we look up the next sector and continue the process. * * NOTE: Since the FDC isn't aware of the extent of the transfer, all writeByte() can do is accept bytes * until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted. * * TODO: Research the requirements, if any, for multi-track I/O and determine what else needs to be done. * * @this {FDC} * @param {Object} drive * @param {number} b containing next byte to write * @return {number} (b unchanged; return -1 if command should be terminated) */ FDC.prototype.writeByte = function(drive, b) { if (drive.resCode || !drive.disk) return -1; do { if (drive.sector) { if (drive.disk.write(drive.sector, drive.ibSector++, b)) break; } /* * Locate the next sector, and then try writing again. */ drive.sector = drive.disk.seek(drive.bCylinder, drive.bHead, drive.bSector); if (!drive.sector) { /* * TODO: Determine whether this should be FDC.REG_DATA.RES.CRC_ERROR or FDC.REG_DATA.RES.DATA_FIELD */ drive.resCode = FDC.REG_DATA.RES.CRC_ERROR | FDC.REG_DATA.RES.INCOMPLETE; b = -1; break; } drive.ibSector = 0; /* * We "pre-advance" bSector et al now, instead of waiting to advance it right before the seek(). * This allows the initial call to writeByte() to perform a seek without triggering an unwanted advance. */ this.advanceSector(drive); } while (true); return b; }; /** * advanceSector(drive) * * This increments the sector number; when the sector number reaches drive.nDiskSectors on the current track, we * increment drive.bHead and reset drive.bSector, and when drive.bHead reaches drive.nDiskHeads, we reset drive.bHead * and increment drive.bCylinder. * * @this {FDC} * @param {Object} drive */ FDC.prototype.advanceSector = function(drive) { Component.assert(drive.bCylinder < drive.nDiskCylinders); drive.bSector++; var bSectorStart = 1; if (drive.bSector >= drive.nDiskSectors + bSectorStart) { drive.bSector = bSectorStart; drive.bHead++; if (drive.bHead >= drive.nDiskHeads) { drive.bHead = 0; drive.bCylinder++; } } }; /** * writeFormat(drive, b) * * @this {FDC} * @param {Object} drive * @param {number} b containing a format command byte * @return {number} (b if successful, -1 if command should be terminated) */ FDC.prototype.writeFormat = function(drive, b) { if (drive.resCode) return -1; drive.abFormat[drive.cbFormat++] = b; if (drive.cbFormat == drive.abFormat.length) { drive.bCylinder = drive.abFormat[0]; // C drive.bHead = drive.abFormat[1]; // H drive.bSector = drive.abFormat[2]; // R drive.nBytes = 128 << drive.abFormat[3];// N (0 => 128, 1 => 256, 2 => 512, 3 => 1024) drive.cbFormat = 0; if (DEBUG) this.messageDebugger("writeFormat(head=" + str.toHexByte(drive.bHead) + ",cyl=" + str.toHexByte(drive.bCylinder) + ",sec=" + str.toHexByte(drive.bSector) + ",len=" + str.toHexWord(drive.nBytes) + ")"); for (var i = 0; i < drive.nBytes; i++) { if (this.writeByte(drive, drive.bFiller) < 0) { return -1; } } drive.cSectorsFormatted++; } if (drive.cSectorsFormatted >= drive.bSectorEnd) b = -1; return b; }; /** * intBIOSDiskette(addr) * * NOTE: This function tries to differentiate FDC requests from HDC requests, by whether the INT 0x13 drive number * in DL is < 0x80; however, not all INT 0x13 functions required a drive number in DL, and not all callers supplied one. * * INT 0x13 Quick Reference: * * AH * ---- * 0x00 Reset * 0x01 Get status (from last operation) * 0x02 Read sectors * 0x03 Write sectors * 0x04 Verify sectors * 0x05 Format track * * For Read, Write, Verify and Format commands: * * DL drive number (0-3 allowed, value checked) * DH head number (0-1 allowed, not value checked) * CH track number (0-39 allowed, not value checked [which is good, because high-density diskettes go up to 80 tracks]) * CL sector number (1-8 allowed, not value checked [which is good, because support for 9-sector tracks was later added]) * AL number of sectors (max of 8, not value checked) * ES:BX sector buffer * * @this {FDC} * @param {number} addr * @return {boolean} true to proceed with the INT 0x13 software interrupt, false to skip */ FDC.prototype.intBIOSDiskette = function(addr) { if (DEBUGGER) { var DL = this.cpu.regDX & 0xff; if (this.dbg && this.dbg.messageEnabled(FDC.MESSAGE_FDC | FDC.MESSAGE_INT) && DL < 0x80) { this.dbg.messageInt(FDC.BIOS.INT_DISKETTE, addr); this.cpu.addIntReturn(addr, function(fdc, nCycles) { return function onBIOSDisketteReturn(nLevel) { fdc.dbg.messageIntReturn(FDC.BIOS.INT_DISKETTE, nLevel, fdc.cpu.getCycles() - nCycles); }; }(this, this.cpu.getCycles())); } } return true; }; /** * messageDebugger(sMessage, bitsMessage) * * This is a combination of the Debugger's messageEnabled(MESSAGE_FDC) and message() functions, for convenience. * * @this {FDC} * @param {string} sMessage is any caller-defined message string * @param {number} [bitsMessage] is one or more Debugger MESSAGE_* category flag(s) */ FDC.prototype.messageDebugger = function(sMessage, bitsMessage) { if (DEBUGGER && this.dbg) { if (bitsMessage == null) bitsMessage = FDC.MESSAGE_FDC; if (this.dbg.messageEnabled(bitsMessage)) this.dbg.message(sMessage); } }; /** * messagePort(port, bOut, addrFrom, name, bIn) * * This is an internal version of the Debugger's messagePort() function, for convenience. * * @this {FDC} * @param {number} port * @param {number|null} bOut if an output operation * @param {number|null} [addrFrom] * @param {string|null} [name] of the port, if any * @param {number} [bIn] is the input value, if known, on an input operation */ FDC.prototype.messagePort = function(port, bOut, addrFrom, name, bIn) { if (DEBUGGER && this.dbg) this.dbg.messagePort(this, port, bOut, addrFrom, name, FDC.MESSAGE_FDC, bIn); }; /* * Port input notification table * * TODO: Even though port 0x3F7 was not present on controllers prior to MODEL_5170, I'm taking the easy * way out and always emulating it. So, consider an FDC parameter to disable that feature for stricter compatibility. */ FDC.aPortInput = { 0x3F4: FDC.prototype.inFDCStatus, 0x3F5: FDC.prototype.inFDCData, 0x3F7: FDC.prototype.inFDCInput }; /* * Port output notification table * * TODO: Even though port 0x3F7 was not present on controllers prior to MODEL_5170, I'm taking the easy * way out and always emulating it. So, consider an FDC parameter to disable that feature for stricter compatibility. */ FDC.aPortOutput = { 0x3F2: FDC.prototype.outFDCOutput, 0x3F5: FDC.prototype.outFDCData, 0x3F7: FDC.prototype.outFDCControl }; /** * FDC.init() * * This function operates on every element (e) of class "fdc", and initializes * all the necessary HTML to construct the FDC module(s) as spec'ed. * * Note that each element (e) of class "fdc" is expected to have a "data-value" * attribute containing the same JSON-encoded parameters that the FDC constructor expects. */ FDC.init = function() { var aeFDC = Component.getElementsByClass(window.document, PCJSCLASS, "fdc"); for (var iFDC = 0; iFDC < aeFDC.length; iFDC++) { var eFDC = aeFDC[iFDC]; var parmsFDC = Component.getComponentParms(eFDC); var fdc = new FDC(parmsFDC); Component.bindComponentControls(fdc, eFDC, PCJSCLASS); } }; /* * Initialize every Floppy Drive Controller (FDC) module on the page. */ web.onInit(FDC.init); if (typeof APP_PCJS !== 'undefined') APP_PCJS.FDC = FDC; if (typeof module !== 'undefined') module.exports = FDC;