pcjs/my_modules/pcjs-client/lib/fdc.js
Jeff Parsons 0f06c3f039 The 5170 AT hard disk controller is limping along now, so I'm getting ready for a 1.15.4 release
Some other interesting bugs were fixed along the way:
the Video component incorrectly assumed that masking a negative number with 0x80000000 would yield 0x80000000 (it actually yields -0x80000000, thank you JavaScript);
the ChipSet component provides a new addCMOSMemory() interface that RAM objects can use to bump the CMOS total to match the actual total;
the CPU properly implements INSW and OUTSW instructions now (kind of important for the AT hard disk controller)
2014-10-08 16:15:12 -07:00

2175 lines
88 KiB
JavaScript

/**
* @fileoverview Implements the PCjs Floppy Drive Controller (FDC) component.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* @suppress {missingProperties}
* Created 2012-Aug-09
*
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
* at <http://jsmachines.net/> and <http://pcjs.org/>.
*
* 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 <http://www.gnu.org/licenses/gpl.html>.
*
* 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
*
* 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 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) {
this.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 = [];
/*
* 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();
if (!this.autoMount()) this.setReady();
}
Component.subclass(Component, FDC);
FDC.DEFAULT_DRIVE_NAME = "Floppy Drive";
/*
* 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 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.DRIVE_STATUS = 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.INT_STATUS = 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, cbWrite indicates the total number of bytes in the command request sequence,
* including the first (command) byte; cbRead indicates total number of bytes in the response sequence.
*/
FDC.aCmdSeqs = {
0x03: {cbWrite: 3, cbRead: 0, name: "SPECIFY"},
0x04: {cbWrite: 2, cbRead: 1, name: "DRIVE_STATUS"},
0x05: {cbWrite: 9, cbRead: 7, name: "WRITE_DATA"},
0x06: {cbWrite: 9, cbRead: 7, name: "READ_DATA"},
0x07: {cbWrite: 2, cbRead: 0, name: "RECALIBRATE"},
0x08: {cbWrite: 1, cbRead: 2, name: "INT_STATUS"},
0x0D: {cbWrite: 6, cbRead: 7, name: "FORMAT"},
0x0F: {cbWrite: 3, cbRead: 0, name: "SEEK"}
};
/*
* FDC BIOS interrupts, functions, and other parameters
*/
FDC.BIOS = {};
FDC.BIOS.DISKETTE_INT = 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) {
fdc.error("FDC option error: " + (e.message || e));
}
}
var sDesc = dataValue['desc'];
if (sDesc === undefined) sDesc = "";
var sHRef = dataValue['href'];
if (sHRef !== undefined) sDesc = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sDesc + "</a>";
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");
bus.addPortInputTable(this, FDC.aPortInput);
bus.addPortOutputTable(this, FDC.aPortOutput);
if (DEBUGGER) cpu.addInterruptNotify(FDC.BIOS.DISKETTE_INT, this, this.intBIOSDiskette);
};
/**
* 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;
}
if (this.chipset) {
var iDrive;
this.nDrives = this.chipset.getSWFloppyDrives();
for (iDrive = 0; iDrive < this.nDrives; iDrive++) {
var drive = this.aDrives[iDrive];
drive.bType = this.chipset.getSWFloppyDriveType(iDrive);
if (drive.bType == ChipSet.FDRIVE.DSHD) {
drive.nCylinders = 80;
}
}
/*
* Now that we finally have the SW1 settings, we can populate the HTML control
* to match the actual (well, um, specified) number of floppy drives in the system.
*/
var controlDrives;
if ((controlDrives = this.bindings['listDrives'])) {
while (controlDrives.firstChild) {
controlDrives.removeChild(controlDrives.firstChild);
}
controlDrives.innerHTML = "";
for (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;
var fSuccess = true;
if (data === undefined) {
data = [0, 0, FDC.REG_STATUS.RQM, new Array(9), 0, 0, 0, []];
}
/*
* Selected drive (from reOutput), 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;
/*
* We allocate the maximum number of drives; we won't know the actual number of drives until we're able to query
* the SW1 switch settings.
*/
if (this.aDrives === undefined) {
this.nDrives = 0; // this will be set later to the number of ACTUAL drives
this.aDrives = new Array(4);
}
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
if (this.aDrives[iDrive] === undefined) {
this.aDrives[iDrive] = {};
}
var drive = this.aDrives[iDrive];
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)
*
* @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 use drive.nCylinders as the default, falling back to a 40-track maximum ONLY when the drive
* hasn't been initialized. This preserves whatever maximum the powerUp() function may have obtained
* from the ChipSet component.
*
* TODO: We may need to make a similar accommodation for drive.nHeads and drive.nSectors down the road;
* they currently default to a maximum of 2 heads (see above) and 9 sectors/track (see below).
*/
data[1] = [FDC.DEFAULT_DRIVE_NAME, drive.nCylinders || 40, data[3], 9, 512, data[1]];
}
/*
* 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.
*/
drive.name = data[i][0];
drive.nCylinders = data[i][1]; // cylinders
drive.nHeads = data[i][2]; // heads/cylinders
drive.nSectors = data[i][3]; // sectors/track
drive.cbSector = data[i][4]; // bytes/sector
drive.fRemovable = data[i][5];
i++;
/*
* 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 INT_STATUS 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 DRIVE_STATUS 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.bType = ChipSet.FDRIVE.DSDD; // default; updated later once we have the actual ChipSet object
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++];
/*
* The next group of properties are set by user requests to load/unload diskette images.
*
* NOTE: I now avoid reinitializing drive.disk in order to retain any previously mounted diskette across resets.
*
* drive.disk = null; // when a "disk" is "inserted" into the "drive", this is a Disk object
*/
/*
* 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];
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.mountDiskette);
return false;
}
return true;
};
/**
* mountDiskette(drive, disk, sDisketteName, sDiskettePath)
*
* @this {FDC}
* @param {Object} drive
* @param {Disk} disk is set if the disk was successfully mounted, null if not
* @param {string} sDisketteName
* @param {string} sDiskettePath
*/
FDC.prototype.mountDiskette = function(drive, disk, sDisketteName, sDiskettePath)
{
drive.fBusy = false;
if ((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 disk \"" + sDisketteName + "\" in drive " + String.fromCharCode(0x41 + drive.iDrive), drive.fAutoMount);
}
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, DSDD (40-track) or DSHD (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 INT_STATUS command
* following the regOutput reset operation would indicate drive 1 instead of drive 0. But the BIOS requires
* the ST0 result from the INT_STATUS 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.aCmdSeqs[bCmdMasked] !== undefined) {
if (this.regDataTotal >= FDC.aCmdSeqs[bCmdMasked].cbWrite) {
this.doCmd();
}
return;
}
if (DEBUG) this.messageDebugger("unsupported FDC command: " + str.toHexByte(bCmd));
};
/**
* 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, bHeadSelect, bHead, bCylinder, 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.DRIVE_STATUS: // 0x04 (SENSE DRIVE STATUS)
bDrive = this.popCmd("DS");
bHeadSelect = (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("DS");
bHeadSelect = (bDrive >> 2) & 0x1;
this.iDrive = (bDrive & 0x3);
drive = this.aDrives[this.iDrive];
drive.bHead = bHeadSelect;
drive.bCylinder = this.popCmd("C"); // C
bHead = this.popCmd("H"); // H
Component.assert(bHead == bHeadSelect);
drive.bSector = this.popCmd("R"); // R
n = this.popCmd("N"); // N
drive.nBytes = 128 << n; // 0 => 128, 1 => 256, 2 => 512, 3 => 1024
drive.bSectorEnd = this.popCmd("EOT"); // EOT (final sector number on a cylinder)
this.popCmd("GPL"); // GPL (spacing between sectors, excluding VCO Sync Field; 3)
this.popCmd("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.beginResult();
this.pushST0(drive);
this.pushST1(drive);
this.pushST2(drive);
this.pushResult(drive.bCylinder, "C");
this.pushResult(drive.bHead, "H");
this.pushResult(drive.bSector, "R");
this.pushResult(n, "N");
fIRQ = true;
break;
case FDC.REG_DATA.CMD.RECALIBRATE: // 0x07
bDrive = this.popCmd("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.INT_STATUS
fIRQ = true;
break;
case FDC.REG_DATA.CMD.INT_STATUS: // 0x08 (SENSE INTERRUPT STATUS)
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, "PCN");// no interrupt is generated by this command, so fIRQ should remain false
break;
case FDC.REG_DATA.CMD.FORMAT_TRACK: // 0x0D
bDrive = this.popCmd("DS");
bHeadSelect = (bDrive >> 2) & 0x1;
this.iDrive = (bDrive & 0x3);
drive = this.aDrives[this.iDrive];
drive.bHead = bHeadSelect;
n = this.popCmd("N"); // N
drive.nBytes = 128 << n; // 0 => 128, 1 => 256, 2 => 512, 3 => 1024 (bytes/sector)
drive.bSectorEnd = this.popCmd("SC"); // SC (sectors/track)
this.popCmd("GPL"); // GPL (spacing between sectors, excluding VCO Sync Field; 3)
drive.bFiller = this.popCmd("D"); // D (filler byte)
this.doFormat(drive);
this.beginResult();
this.pushST0(drive);
this.pushST1(drive);
this.pushST2(drive);
this.pushResult(drive.bCylinder, "C");
this.pushResult(drive.bHead, "H");
this.pushResult(drive.bSector, "R");
this.pushResult(n, "N");
fIRQ = true;
break;
case FDC.REG_DATA.CMD.SEEK: // 0x0F
bDrive = this.popCmd("DS");
bHeadSelect = (bDrive >> 2) & 0x1;
this.iDrive = (bDrive & 0x3);
drive = this.aDrives[this.iDrive];
drive.bHead = bHeadSelect;
/*
* 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).
*/
bCylinder = this.popCmd("NCN");
drive.bCylinder += bCylinder - drive.bCylinderSeek;
if (drive.bCylinder < 0) drive.bCylinder = 0;
if (drive.bCylinder >= drive.nCylinders) drive.bCylinder = drive.nCylinders - 1;
drive.bCylinderSeek = bCylinder;
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) + ")");
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.INT_STATUS 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);
}
}
};
/**
* 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((this.regDataIndex > 0? this.dbg.MESSAGE_PORT : 0) | this.dbg.MESSAGE_FDC)) {
var bCmdMasked = bCmd & FDC.REG_DATA.CMD.MASK;
if (!name && !this.regDataIndex && FDC.aCmdSeqs[bCmdMasked]) name = FDC.aCmdSeqs[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("HLT");
// this.nHLT = this.popCmd("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("SRT");
// this.nSRT = this.popCmd("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 && DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_PORT | this.dbg.MESSAGE_FDC)) this.dbg.message("FDC.RES[" + (name || this.regDataTotal) + "]: 0x" + str.toHexByte(bResult));
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), "ST0");
};
/**
* pushST1(drive)
*
* @this {FDC}
* @param {Object} drive
*/
FDC.prototype.pushST1 = function(drive)
{
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST1) >>> 8, "ST1");
};
/**
* pushST2(drive)
*
* @this {FDC}
* @param {Object} drive
*/
FDC.prototype.pushST2 = function(drive)
{
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST2) >>> 16, "ST2");
};
/**
* pushST3(drive)
*
* @this {FDC}
* @param {Object} drive
*/
FDC.prototype.pushST3 = function(drive)
{
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST3) >>> 24, "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 (DEBUG) this.messageDebugger("doFormat()");
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 if anything needs to be
* done. At the very least, if it must be supported, there would need to be some head-incrementing somewhere.
*
* @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;
drive.bSector++;
} 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 if anything needs to be
* done. At the very least, if it must be supported, there would need to be some head-incrementing somewhere.
*
* @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;
drive.bSector++;
} while (true);
return b;
};
/**
* 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 AL = this.cpu.regAX & 0xff;
var AH = this.cpu.regAX >> 8;
var CL = this.cpu.regCX & 0xff;
var CH = this.cpu.regCX >> 8;
var DL = this.cpu.regDX & 0xff;
var DH = this.cpu.regDX >> 8;
if (this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_FDC) && DL < 0x80) {
this.dbg.message("FDC.intBIOS(AH=" + str.toHexByte(AH) + ",D=" + str.toHexByte(DL) + ",C=" + str.toHexByte(CH) + ",H=" + str.toHexByte(DH) + ",S=" + str.toHexByte(CL) + ",N=" + str.toHexByte(AL) + ") at " + str.toHexAddr(addr - this.cpu.segCS.base, this.cpu.segCS.sel));
// this.cpu.haltCPU();
this.cpu.addInterruptReturn(addr, function (fdc, nCycles) {
return function onBIOSDisketteReturn(nLevel) {
nCycles = fdc.cpu.getCycles() - nCycles;
fdc.messageDebugger("FDC.intBIOS(" + nLevel + "): C=" + (fdc.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")");
// if (DEBUG && nCycles > 10000) fdc.cpu.haltCPU();
};
}(this, this.cpu.getCycles()));
}
}
return true;
};
/**
* messageDebugger(sMessage)
*
* 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
*/
FDC.prototype.messageDebugger = function(sMessage)
{
if (DEBUGGER && this.dbg) {
if (this.dbg.messageEnabled(this.dbg.MESSAGE_FDC)) {
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, this.dbg.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;