2764 lines
118 KiB
JavaScript
2764 lines
118 KiB
JavaScript
/**
|
|
* @fileoverview Implements the PCx86 Floppy Drive Controller (FDC) component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @copyright © Jeff Parsons 2012-2017
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
|
|
* and to display that copyright notice when the software starts running; see COPYRIGHT in
|
|
* <http://pcjs.org/modules/shared/lib/defines.js>.
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
if (NODE) {
|
|
var Str = require("../../shared/lib/strlib");
|
|
var Web = require("../../shared/lib/weblib");
|
|
var DiskAPI = require("../../shared/lib/diskapi");
|
|
var Component = require("../../shared/lib/component");
|
|
var State = require("../../shared/lib/state");
|
|
var PCX86 = require("./defines");
|
|
var Messages = require("./messages");
|
|
var ChipSet = require("./chipset");
|
|
var Disk = require("./disk");
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
|
|
/**
|
|
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
|
|
* which would force us to declare all class properties in the constructor, as well as prevent
|
|
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
|
|
*
|
|
* @unrestricted
|
|
*/
|
|
class FDC extends Component {
|
|
/**
|
|
* FDC(parmsFDC)
|
|
*
|
|
* The FDC component simulates a NEC µPD765A or Intel 8272A compatible floppy disk controller, and has one
|
|
* component-specific property:
|
|
*
|
|
* autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties
|
|
*
|
|
* sortBy: "name" to sort disks by name, "path" to sort by path, or "none" to leave as-is (default is "name")
|
|
*
|
|
* 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.
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} parmsFDC
|
|
*/
|
|
constructor(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).
|
|
*/
|
|
super("FDC", parmsFDC, Messages.FDC);
|
|
|
|
this['dmaRead'] = FDC.prototype.doDMARead;
|
|
this['dmaWrite'] = FDC.prototype.doDMAWrite;
|
|
this['dmaFormat'] = FDC.prototype.doDMAFormat;
|
|
|
|
/*
|
|
* We record any 'autoMount' object now, but we no longer parse it until initBus(), because the Computer's
|
|
* getMachineParm() service may have an override for us.
|
|
*/
|
|
this.configMount = parmsFDC['autoMount'] || null;
|
|
|
|
/*
|
|
* This establishes "name" as the default; if we decide we'd prefer "none" to be the default (ie, the order
|
|
* to use when no sortBy value is specified), we can just drop the '|| "name"', because an undefined value is
|
|
* just as falsey as null.
|
|
*
|
|
* The code that actually performs the sorting (in setBinding()) first checks that sortBy is not falsey, and
|
|
* then assumes that the non-falsey value must be either "path" or "name", and since it explicitly checks for
|
|
* "path" first, any non-sensical value will be treated as "name" (which is fine, since that's our current default).
|
|
*/
|
|
this.sortBy = parmsFDC['sortBy'] || "name";
|
|
if (this.sortBy == "none") this.sortBy = 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 = [];
|
|
|
|
/*
|
|
* Support for local disk images is currently limited to desktop browsers with FileReader support;
|
|
* when this flag is set, setBinding() allows local disk bindings and informs initBus() to update the
|
|
* "listDisks" binding accordingly.
|
|
*/
|
|
this.fLocalDisks = (!Web.isMobile() && window && 'FileReader' in window);
|
|
|
|
/*
|
|
* 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().
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {FDC}
|
|
* @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)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
setBinding(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var fdc = this;
|
|
|
|
switch (sBinding) {
|
|
|
|
case "listDisks":
|
|
this.bindings[sBinding] = control;
|
|
|
|
/*
|
|
* Since binding is a one-time initialization operation, it's also the perfect time to
|
|
* perform whatever sorting (if any) is indicated by the FDC component's "sortBy" property.
|
|
*
|
|
* And since setBinding() is called before initBus(), that means any "special" disk entries
|
|
* will be added after the sorting, so we won't be "burying" those entries somewhere in the
|
|
* middle.
|
|
*/
|
|
if (this.sortBy) {
|
|
var i, aOptions = [];
|
|
/*
|
|
* NOTE: All this monkeying around with copying the elements from control.options to aOptions
|
|
* and then back again is necessary because control.options isn't a *real* Array (at least not
|
|
* in all browsers); consequently, it may have no sort() method. It has a length property,
|
|
* along with numeric properties 0 to length-1, but it's still probably just an Object, not
|
|
* an Array.
|
|
*
|
|
* Also note that changing the order of the control's options would ordinarily mean that the
|
|
* control's selectedIndex may now be incorrect, but in our case, it doesn't matter, because
|
|
* we have a special function, displayDiskette(), that will be called at LEAST once during
|
|
* initialization, ensuring that selectedIndex is set correctly.
|
|
*/
|
|
for (i = 0; i < control.options.length; i++) {
|
|
aOptions.push(control.options[i]);
|
|
}
|
|
aOptions.sort(function(a, b) {
|
|
/*
|
|
* I've switched to localeCompare() because it offers case-insensitivity by default;
|
|
* I'm still a little concerned that we could somehow end up with list elements whose text
|
|
* and/or value properties are undefined (because calling a method on an undefined variable
|
|
* will throw an exception), but maybe I'm being overly paranoid....
|
|
*/
|
|
if (fdc.sortBy != "path") {
|
|
return a.text.localeCompare(b.text);
|
|
} else {
|
|
return a.value.localeCompare(b.value);
|
|
}
|
|
});
|
|
for (i = 0; i < aOptions.length; i++) {
|
|
try {
|
|
/*
|
|
* TODO: Determine why this line blows up in IE8; are the properties of an options object not settable in IE8?
|
|
*/
|
|
control.options[i] = aOptions[i];
|
|
} catch(e) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
control.onchange = function onChangeListDisks(event) {
|
|
var controlDesc = fdc.bindings["descDisk"];
|
|
var controlOption = control.options[control.selectedIndex];
|
|
if (controlDesc && controlOption) {
|
|
var dataValue = {};
|
|
var sValue = controlOption.getAttribute("data-value");
|
|
if (sValue) {
|
|
try {
|
|
dataValue = eval("(" + sValue + ")");
|
|
} catch (e) {
|
|
Component.error("FDC option error: " + e.message);
|
|
}
|
|
}
|
|
var sHTML = dataValue['desc'];
|
|
if (sHTML === undefined) sHTML = "";
|
|
var sHRef = dataValue['href'];
|
|
if (sHRef !== undefined) sHTML = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sHTML + "</a>";
|
|
controlDesc.innerHTML = sHTML;
|
|
}
|
|
};
|
|
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 onChangeListDrives(event) {
|
|
var iDrive = Str.parseInt(control.value, 10);
|
|
if (iDrive != null) fdc.displayDiskette(iDrive);
|
|
};
|
|
return true;
|
|
|
|
case "loadDisk":
|
|
this.bindings[sBinding] = control;
|
|
|
|
control.onclick = function onClickLoadDrive(event) {
|
|
var controlDisks = fdc.bindings["listDisks"];
|
|
if (controlDisks) {
|
|
var sDisketteName = controlDisks.options[controlDisks.selectedIndex].text;
|
|
var sDiskettePath = controlDisks.value;
|
|
fdc.loadSelectedDrive(sDisketteName, sDiskettePath);
|
|
}
|
|
};
|
|
return true;
|
|
|
|
case "saveDisk":
|
|
/*
|
|
* Yes, technically, this feature does not require "Local disk support" (which is really a reference
|
|
* to FileReader support), but since fLocalDisks is also false for all mobile devices, and since there
|
|
* is an "orthogonality" to disabling both features in tandem, let's just let it slide, OK?
|
|
*/
|
|
if (!this.fLocalDisks) {
|
|
if (DEBUG) this.log("Local disk support not available");
|
|
/*
|
|
* We could also simply hide the control; eg:
|
|
*
|
|
* control.style.display = "none";
|
|
*
|
|
* but removing the control altogether seems better.
|
|
*/
|
|
control.parentNode.removeChild(/** @type {Node} */ (control));
|
|
return false;
|
|
}
|
|
|
|
this.bindings[sBinding] = control;
|
|
|
|
control.onclick = function onClickSaveDrive(event) {
|
|
var controlDrives = fdc.bindings["listDrives"];
|
|
if (controlDrives && controlDrives.options && fdc.aDrives) {
|
|
var iDriveSelected = Str.parseInt(controlDrives.value, 10) || 0;
|
|
var drive = fdc.aDrives[iDriveSelected];
|
|
if (drive) {
|
|
/*
|
|
* Note the similarity (and hence factoring opportunity) between this code and the HDC's
|
|
* "saveHD*" binding.
|
|
*/
|
|
var disk = drive.disk;
|
|
if (disk) {
|
|
if (DEBUG) fdc.println("saving diskette " + disk.sDiskPath + "...");
|
|
var sAlert = Web.downloadFile(disk.encodeAsBase64(), "octet-stream", true, disk.sDiskFile.replace(".json", ".img"));
|
|
Component.alertUser(sAlert);
|
|
} else {
|
|
fdc.notice("No diskette loaded in drive.");
|
|
}
|
|
} else {
|
|
fdc.notice("No diskette drive selected.");
|
|
}
|
|
}
|
|
};
|
|
return true;
|
|
|
|
case "mountDisk":
|
|
if (!this.fLocalDisks) {
|
|
if (DEBUG) this.log("Local disk support not available");
|
|
/*
|
|
* We could also simply hide the control; eg:
|
|
*
|
|
* control.style.display = "none";
|
|
*
|
|
* but removing the control altogether seems better.
|
|
*/
|
|
control.parentNode.removeChild(/** @type {Node} */ (control));
|
|
return false;
|
|
}
|
|
|
|
this.bindings[sBinding] = control;
|
|
|
|
/*
|
|
* Enable "Mount" button only if a file is actually selected
|
|
*/
|
|
control.addEventListener('change', function() {
|
|
var fieldset = control.children[0];
|
|
var files = fieldset.children[0].files;
|
|
var submit = fieldset.children[1];
|
|
submit.disabled = !files.length;
|
|
});
|
|
|
|
control.onsubmit = function(event) {
|
|
var file = event.currentTarget[1].files[0];
|
|
if (file) {
|
|
var sDiskettePath = file.name;
|
|
var sDisketteName = Str.getBaseName(sDiskettePath, true);
|
|
fdc.loadSelectedDrive(sDisketteName, sDiskettePath, file);
|
|
}
|
|
/*
|
|
* Prevent reloading of web page after form submission
|
|
*/
|
|
return false;
|
|
};
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {X86CPU} cpu
|
|
* @param {DebuggerX86} dbg
|
|
*/
|
|
initBus(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.cmp = cmp;
|
|
|
|
this.chipset = cmp.getMachineComponent("ChipSet");
|
|
|
|
this.configMount = this.cmp.getMachineParm('autoMount') || this.configMount;
|
|
|
|
if (this.configMount) {
|
|
if (typeof this.configMount == "string") {
|
|
try {
|
|
/*
|
|
* The most likely source of any exception will be right here, where we're parsing
|
|
* the JSON-encoded diskette data.
|
|
*/
|
|
this.configMount = eval("(" + this.configMount + ")");
|
|
} catch (e) {
|
|
Component.error("FDC auto-mount error: " + e.message + " (" + this.configMount + ")");
|
|
this.configMount = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
|
|
this.addDiskette("None", "", true);
|
|
if (this.fLocalDisks) this.addDiskette("Local Disk", "?");
|
|
this.addDiskette("Remote Disk", "??");
|
|
|
|
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
|
|
*/
|
|
powerUp(data, fRepower)
|
|
{
|
|
if (!fRepower) {
|
|
if (!data) {
|
|
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.value = "";
|
|
for (var iDrive = 0; iDrive < this.nDrives; iDrive++) {
|
|
var controlOption = 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.text = String.fromCharCode(0x41 + iDrive) + ":";
|
|
controlDrives.appendChild(controlOption);
|
|
}
|
|
if (this.nDrives > 0) {
|
|
controlDrives.value = "0";
|
|
this.displayDiskette(0);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {FDC}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
powerDown(fSave, fShutdown)
|
|
{
|
|
return fSave? 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}
|
|
*/
|
|
reset()
|
|
{
|
|
/*
|
|
* 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}
|
|
*/
|
|
save()
|
|
{
|
|
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
|
|
*/
|
|
restore(data)
|
|
{
|
|
return this.initController(data[0]);
|
|
}
|
|
|
|
/**
|
|
* initController(data)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Array} [data]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
initController(data)
|
|
{
|
|
var i = 0, iDrive;
|
|
var fSuccess = true;
|
|
|
|
if (!data) {
|
|
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.getDIPFloppyDrives();
|
|
/*
|
|
* 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.getDIPFloppyDriveSize(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:
|
|
/* falls through */
|
|
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.messageEnabled()) {
|
|
this.printMessage("FDC initialized for " + this.aDrives.length + " drive(s)");
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
/**
|
|
* saveController()
|
|
*
|
|
* @this {FDC}
|
|
* @return {Array}
|
|
*/
|
|
saveController()
|
|
{
|
|
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
|
|
*/
|
|
initDrive(drive, iDrive, data)
|
|
{
|
|
var i = 0;
|
|
var fSuccess = true;
|
|
|
|
drive.iDrive = iDrive;
|
|
drive.fBusy = drive.fLocal = false;
|
|
|
|
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, readData and writeData), 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 -- "litrally" (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 == 102) deltas = false; // v1.02 backward-compatibility
|
|
|
|
if (typeof deltas == "boolean") {
|
|
var fLocal = deltas;
|
|
var sDisketteName = data[i++];
|
|
var sDiskettePath = data[i];
|
|
/*
|
|
* If we're restoring a local disk image, then the entire disk contents should be captured in aDiskHistory,
|
|
* so all we have to do is mount a blank diskette and let disk.restore() do the rest; ie, there's nothing to
|
|
* "load" (it's a purely synchronous operation).
|
|
*
|
|
* Otherwise, we must call loadDrive(); in the common case, loadDrive() will have already "auto-mounted"
|
|
* the diskette, so it will return true, and then we restore any deltas to the current image.
|
|
*
|
|
* However, if loadDrive() returns false, then it has initiated the load for a *different* disk image,
|
|
* so we must mark ourselves as "not ready" again, and add another "wait for ready" test in Computer before
|
|
* finally powering the CPU.
|
|
*/
|
|
if (fLocal) {
|
|
this.mountDrive(iDrive, sDisketteName, sDiskettePath);
|
|
}
|
|
else if (this.loadDrive(iDrive, sDisketteName, sDiskettePath, true)) {
|
|
if (drive.disk) {
|
|
if (sDiskettePath) {
|
|
this.addDiskHistory(sDisketteName, sDiskettePath, drive.disk);
|
|
} else {
|
|
if (MAXDEBUG) Component.warning("Disk '" + (drive.disk.sDiskName || sDisketteName) + "' not recorded properly in drive " + iDrive);
|
|
}
|
|
}
|
|
} 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 loadDrive() returned true, then this can happen immediately. Otherwise, loadDrive()
|
|
* 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}
|
|
*/
|
|
saveDrives()
|
|
{
|
|
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}
|
|
* @param {Object} drive
|
|
* @return {Array}
|
|
*/
|
|
saveDrive(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 a boolean (fLocal);
|
|
* if that boolean is not present, then the restore code will know it's dealing with a pre-v1.02 state.
|
|
*/
|
|
data[i++] = drive.fLocal;
|
|
data[i++] = drive.sDisketteName;
|
|
data[i] = drive.sDiskettePath;
|
|
if (DEBUG && !drive.sDiskettePath && drive.disk && drive.disk.sDiskPath) {
|
|
Component.warning("Disk '" + drive.disk.sDiskName + "' not saved properly in drive " + drive.iDrive);
|
|
}
|
|
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}
|
|
*/
|
|
saveDeltas()
|
|
{
|
|
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)
|
|
*/
|
|
copyDrive(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 readData() 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
|
|
*/
|
|
seekDrive(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 readData(), 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
|
|
*/
|
|
autoMount(fRemount)
|
|
{
|
|
if (!fRemount) this.cAutoMount = 0;
|
|
if (this.configMount) {
|
|
for (var sDrive in this.configMount) {
|
|
var configDrive = this.configMount[sDrive];
|
|
var sDiskettePath = configDrive['path'] || "";
|
|
var sDisketteName = configDrive['name'] || this.findDiskette(sDiskettePath);
|
|
if (sDiskettePath && sDisketteName) {
|
|
/*
|
|
* 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.loadDrive(iDrive, sDisketteName, sDiskettePath, true) && fRemount) {
|
|
this.setReady(false);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
this.notice("Incorrect auto-mount settings for drive " + sDrive + " (" + JSON.stringify(configDrive) + ")");
|
|
}
|
|
}
|
|
return !!this.cAutoMount;
|
|
}
|
|
|
|
/**
|
|
* loadSelectedDrive(sDisketteName, sDiskettePath, file)
|
|
*
|
|
* @this {FDC}
|
|
* @param {string} sDisketteName
|
|
* @param {string} sDiskettePath
|
|
* @param {File} [file] is set if there's an associated File object
|
|
*/
|
|
loadSelectedDrive(sDisketteName, sDiskettePath, file)
|
|
{
|
|
var iDrive;
|
|
var controlDrives = this.bindings["listDrives"];
|
|
if (controlDrives && !isNaN(iDrive = Str.parseInt(controlDrives.value, 10)) && iDrive >= 0 && iDrive < this.aDrives.length) {
|
|
|
|
if (!sDiskettePath) {
|
|
this.unloadDrive(iDrive);
|
|
return;
|
|
}
|
|
|
|
if (sDiskettePath == "?") {
|
|
this.notice('Use "Choose File" and "Mount" to select and load a local disk.');
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* 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 remote 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 remote disk image.", "") || "";
|
|
if (!sDiskettePath) return;
|
|
sDisketteName = Str.getBaseName(sDiskettePath);
|
|
if (DEBUG) this.println("Attempting to load " + sDiskettePath + " as \"" + sDisketteName + "\"");
|
|
}
|
|
|
|
if (DEBUG) this.println("loading disk " + sDiskettePath + "...");
|
|
|
|
while (this.loadDrive(iDrive, sDisketteName, sDiskettePath, false, file) < 0) {
|
|
if (!window.confirm("Click OK to reload the original disk.\n(WARNING: All disk changes will be discarded)")) {
|
|
return;
|
|
}
|
|
/*
|
|
* So here's the story: loadDrive() 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 loadDrive().
|
|
*
|
|
* loadDrive() 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.
|
|
*/
|
|
this.removeDiskHistory(sDisketteName, sDiskettePath);
|
|
this.unloadDrive(iDrive, false, true);
|
|
}
|
|
return;
|
|
}
|
|
this.notice("Unable to load the selected drive");
|
|
}
|
|
|
|
/**
|
|
* mountDrive(iDrive, sDisketteName, sDiskettePath)
|
|
*
|
|
* @this {FDC}
|
|
* @param {number} iDrive
|
|
* @param {string} sDisketteName
|
|
* @param {string} sDiskettePath
|
|
*/
|
|
mountDrive(iDrive, sDisketteName, sDiskettePath)
|
|
{
|
|
var drive = this.aDrives[iDrive];
|
|
this.unloadDrive(iDrive, true, true);
|
|
drive.fLocal = true;
|
|
var disk = new Disk(this, drive, DiskAPI.MODE.PRELOAD);
|
|
this.doneLoadDrive(drive, disk, sDisketteName, sDiskettePath, true);
|
|
}
|
|
|
|
/**
|
|
* loadDrive(iDrive, sDisketteName, sDiskettePath, fAutoMount, file)
|
|
*
|
|
* NOTE: If sDiskettePath is already loaded in the drive, nothing needs to be done.
|
|
*
|
|
* @this {FDC}
|
|
* @param {number} iDrive
|
|
* @param {string} sDisketteName
|
|
* @param {string} sDiskettePath
|
|
* @param {boolean} [fAutoMount]
|
|
* @param {File} [file] is set if there's an associated File object
|
|
* @return {number} 1 if diskette loaded, 0 if queued up (or busy), -1 if already loaded
|
|
*/
|
|
loadDrive(iDrive, sDisketteName, sDiskettePath, fAutoMount, file)
|
|
{
|
|
var drive = this.aDrives[iDrive];
|
|
if (sDiskettePath) {
|
|
/*
|
|
* TODO: This code contains two hacks that should eventually be eliminated: first, machines
|
|
* with saved states may attempt to load disks using old paths, so we replace the old path
|
|
* with the new, and second, they be using lower-case disk image names, whereas we now use
|
|
* UPPER-CASE names for disk images, so we lower-case both before comparing. The only problem
|
|
* with removing these hacks is that we can never be sure when all saved states in the wild
|
|
* have been updated....
|
|
*/
|
|
sDiskettePath = sDiskettePath.replace("/disks/pc/", "/disks/pcx86/");
|
|
if (drive.sDiskettePath.toLowerCase() != sDiskettePath.toLowerCase()) {
|
|
this.unloadDrive(iDrive, fAutoMount, true);
|
|
if (drive.fBusy) {
|
|
this.notice("Drive " + iDrive + " busy");
|
|
return 0;
|
|
}
|
|
drive.fBusy = true;
|
|
if (fAutoMount) {
|
|
drive.fAutoMount = true;
|
|
this.cAutoMount++;
|
|
if (this.messageEnabled()) this.printMessage("loading diskette '" + sDisketteName + "'");
|
|
}
|
|
drive.fLocal = !!file;
|
|
var disk = new Disk(this, drive, DiskAPI.MODE.PRELOAD);
|
|
if (!disk.load(sDisketteName, sDiskettePath, file, this.doneLoadDrive)) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* doneLoadDrive(drive, disk, sDisketteName, sDiskettePath, fAutoMount)
|
|
*
|
|
* @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
|
|
* @param {boolean} [fAutoMount]
|
|
*/
|
|
doneLoadDrive(drive, disk, sDisketteName, sDiskettePath, fAutoMount)
|
|
{
|
|
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;
|
|
|
|
/*
|
|
* Adding local disk image names to the disk list seems like a nice idea, but it's too confusing,
|
|
* because then it looks like the "Mount" button should be able to (re)load them, and that can NEVER
|
|
* happen, for security reasons; local disk images can ONLY be loaded via the "Mount" button after
|
|
* the user has selected them via the "Choose File" button.
|
|
*
|
|
* this.addDiskette(sDisketteName, sDiskettePath);
|
|
*
|
|
* So we're going to take a different approach: when displayDiskette() is asked to display the name
|
|
* of a local disk image, it will map all such disks to "Local Disk", and any attempt to "Mount" such
|
|
* a disk, will essentially result in a "Disk not found" error.
|
|
*/
|
|
|
|
this.addDiskHistory(sDisketteName, sDiskettePath, disk);
|
|
|
|
/*
|
|
* For a local disk (ie, one loaded via mountDrive()), the disk.restore() performed by addDiskHistory()
|
|
* may have altered the disk geometry, so refresh the disk info.
|
|
*/
|
|
aDiskInfo = disk.info();
|
|
|
|
/*
|
|
* 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 || 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];
|
|
|
|
/*
|
|
* Since you usually want the Computer to have focus again after loading a new diskette, let's try automatically
|
|
* updating the focus after a successful load.
|
|
*/
|
|
if (this.cmp) this.cmp.updateFocus();
|
|
}
|
|
else {
|
|
drive.fLocal = false;
|
|
}
|
|
|
|
if (drive.fAutoMount) {
|
|
drive.fAutoMount = false;
|
|
if (!--this.cAutoMount) this.setReady();
|
|
}
|
|
|
|
this.displayDiskette(drive.iDrive);
|
|
}
|
|
|
|
/**
|
|
* addDiskette(sName, sPath, fTop)
|
|
*
|
|
* @this {FDC}
|
|
* @param {string} sName
|
|
* @param {string} sPath
|
|
* @param {boolean} [fTop] (default is bottom)
|
|
*/
|
|
addDiskette(sName, sPath, fTop)
|
|
{
|
|
var controlDisks = this.bindings["listDisks"];
|
|
if (controlDisks && controlDisks.options) {
|
|
for (var i = 0; i < controlDisks.options.length; i++) {
|
|
if (controlDisks.options[i].value == sPath) return;
|
|
}
|
|
var controlOption = document.createElement("option");
|
|
controlOption.text = sName;
|
|
controlOption.value = sPath;
|
|
if (fTop && controlDisks.childNodes[0]) {
|
|
controlDisks.insertBefore(controlOption, controlDisks.childNodes[0]);
|
|
} else {
|
|
controlDisks.appendChild(controlOption);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* findDiskette(sPath)
|
|
*
|
|
* This is used to deal with mount requests (eg, autoMount) that supply a path without a name;
|
|
* if we can find the path in the "listDisks" control, then we return the associated disk name.
|
|
*
|
|
* @this {FDC}
|
|
* @param {string} sPath
|
|
* @return {string|null}
|
|
*/
|
|
findDiskette(sPath)
|
|
{
|
|
var controlDisks = this.bindings["listDisks"];
|
|
if (controlDisks && controlDisks.options) {
|
|
for (var i = 0; i < controlDisks.options.length; i++) {
|
|
var control = controlDisks.options[i];
|
|
if (control.value == sPath) return control.text;
|
|
}
|
|
}
|
|
return Str.getBaseName(sPath, true);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
displayDiskette(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 && controlDisks.options && controlDrives.options) {
|
|
/*
|
|
* Next, make sure the drive whose disk we're updating is the currently selected drive.
|
|
*/
|
|
var i;
|
|
var iDriveSelected = Str.parseInt(controlDrives.value, 10);
|
|
var sTargetPath = (drive.fLocal? "?" : drive.sDiskettePath);
|
|
if (!isNaN(iDriveSelected) && iDriveSelected == iDrive) {
|
|
for (i = 0; i < controlDisks.options.length; i++) {
|
|
if (controlDisks.options[i].value == sTargetPath) {
|
|
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 (Str.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]
|
|
*/
|
|
unloadDrive(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;
|
|
drive.fLocal = false;
|
|
|
|
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
|
|
*/
|
|
unloadAllDrives(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
|
|
*/
|
|
addDiskHistory(sDisketteName, sDiskettePath, disk)
|
|
{
|
|
var i;
|
|
this.assert(!!sDiskettePath);
|
|
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.messageEnabled()) {
|
|
this.printMessage("disk '" + sDisketteName + "' restored from history (" + nChanges + " changes)");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("disk '" + sDisketteName + "' added to history (nothing to restore)");
|
|
}
|
|
this.aDiskHistory[i] = [sDisketteName, sDiskettePath, []];
|
|
}
|
|
|
|
/**
|
|
* removeDiskHistory(sDisketteName, sDiskettePath)
|
|
*
|
|
* @this {FDC}
|
|
* @param {string} sDisketteName
|
|
* @param {string} sDiskettePath
|
|
*/
|
|
removeDiskHistory(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.messageEnabled()) {
|
|
this.printMessage("disk '" + sDisketteName + "' removed from history");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("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
|
|
*/
|
|
updateDiskHistory(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.messageEnabled()) {
|
|
this.printMessage("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.messageEnabled()) {
|
|
this.printMessage("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)
|
|
*/
|
|
outFDCOutput(port, bOut, addrFrom)
|
|
{
|
|
this.printMessageIO(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;
|
|
}
|
|
|
|
/**
|
|
* inFDCDiagnostic(port, addrFrom)
|
|
*
|
|
* It turns out that any 5170 configuration without an HDC component that attempts to use either the REV2 or REV3
|
|
* PC AT ROM BIOS will fail with error "601-Diskette Error", unless we also provide this "D/S/P DIAGNOSTIC REGISTER".
|
|
* The original 5170 REV1 BIOS didn't have this requirement.
|
|
*
|
|
* I'm unable to find any documentation on this so-called "D/S/P DIAGNOSTIC REGISTER" (port 0x3F1) or the "D/S/P CARD"
|
|
* to which the ROM BIOS refers. But it seems clear that if we don't provide the expected response from the DIAGNOSTIC
|
|
* REGISTER, and there's no HDC to respond to the MULTIPLE DATA RATE CAPABLE test that follows, then an error is
|
|
* inevitable. Clearly, there is a very intimate relationship between the FDC and HDC portions of this card.
|
|
*
|
|
* Here's the relevant code from the REV3 PC AT ROM BIOS (TEST2.ASM):
|
|
*
|
|
* ;----- CHECK FOR MULTIPLE DATA RATE CAPABILITY
|
|
*
|
|
* J_OK:
|
|
* MOV DX,03F1H ; D/S/P DIAGNOSTIC REGISTER
|
|
* IN AL,DX ; READ D/S/P TYPE CODE
|
|
* AND AL,11111000B ; KEEP ONLY UNIQUE CODE FOR D/S/P
|
|
* CMP AL,01010000B ; D/S/P CARD - MULTIPLE DATA RATE?
|
|
* JZ J_OK3 ; IF SO JUMP
|
|
*
|
|
* MOV DX,05F7H ; FIXED DISK DIAGNOSTIC REGISTER
|
|
* IN AL,DX ; READ FIXED DISK TYPE CODE
|
|
* AND AL,11110000B ; KEEP ONLY UNIQUE CODE FOR F/D
|
|
* CMP AL,10100000B ; FIXED DISK ADAPTER ?
|
|
* JZ J_FAIL ; MUST BE COMBO ELSE ERROR
|
|
*
|
|
* MOV BL,0FH ; OUTER LOOP COUNT WAIT FOR BUSY OFF
|
|
* SUB CX,CX
|
|
* MOV DX,01F7H ; HARD FILE STATUS PORT
|
|
* J_OK1:
|
|
* IN AL,DX ; GET THE STATUS
|
|
* TEST AL,080H ; IS THE CONTROLLER BUSY?
|
|
* JZ J_OK2 ; CONTINUE IF NOT
|
|
* LOOP J_OK1 ; TRY AGAIN
|
|
* DEC BL ; DECREMENT OUTER LOOP
|
|
* JNZ J_OK1 ; TRY AGAIN IF NOT ZERO
|
|
* AND AL,0CH ; BITS 2 & 3 = 0 IF MULTI DATA CAPABLE
|
|
* JZ J_OK3 ; GO IF YES
|
|
* JMP SHORT J_FAIL ; NO MULTIPLE DATA RATE CAPABILITY
|
|
* J_OK2:
|
|
* MOV DX,1F4H ; VERIFY MULTIPLE DATA RATE CAPABLE
|
|
* MOV AL,055H ; WRITE TO THE CYLINDER BYTE
|
|
* OUT DX,AL
|
|
* JMP $+2 ; I/O DELAY
|
|
* IN AL,DX ; CHECK DATA WRITTEN = DATA READ
|
|
* CMP AL,055H
|
|
* JNZ J_FAIL ; GO IF NOT
|
|
* MOV AL,0AAH ; WRITE ANOTHER PATTERN
|
|
* OUT DX,AL
|
|
* JMP $+2 ; I/O DELAY
|
|
* IN AL,DX
|
|
* CMP AL,0AAH ; IS DATA PATTERN THE SAME?
|
|
* JZ J_OK3 ; GO IF SO
|
|
*
|
|
* J_FAIL:
|
|
* OR @MFG_ERR_FLAG+1,DSK_FAIL; <><><><><><><><><><><><><>
|
|
* ; <><> DISKETTE FAILED <><>
|
|
* MOV SI,OFFSET E601 ; GET ADDRESS OF MESSAGE
|
|
* CALL E_MSG ; GO PRINT ERROR MESSAGE
|
|
* JMP SHORT F15C ; SKIP SETUP IF ERROR
|
|
*
|
|
* J_OK3:
|
|
* OR @LASTRATE,DUAL ; TURN ON DSP/COMBO FLAG
|
|
*
|
|
* @this {FDC}
|
|
* @param {number} port (0x3F1, input only)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
inFDCDiagnostic(port, addrFrom)
|
|
{
|
|
var b = 0x50; // we simply return the expected pattern (01010000B); see code excerpt above
|
|
this.printMessageIO(port, null, addrFrom, "DIAG", b);
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
inFDCStatus(port, addrFrom)
|
|
{
|
|
this.printMessageIO(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
|
|
*/
|
|
inFDCData(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);
|
|
}
|
|
if (this.messageEnabled()) {
|
|
this.printMessageIO(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)
|
|
*/
|
|
outFDCData(port, bOut, addrFrom)
|
|
{
|
|
if (this.messageEnabled()) {
|
|
this.printMessageIO(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.messageEnabled()) {
|
|
this.printMessage("unsupported FDC command: " + Str.toHexByte(bCmd));
|
|
if (MAXDEBUG) this.dbg.stopCPU();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
inFDCInput(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.printMessageIO(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)
|
|
*/
|
|
outFDCControl(port, bOut, addrFrom)
|
|
{
|
|
this.printMessageIO(port, bOut, addrFrom, "CONTROL");
|
|
this.regControl = bOut;
|
|
}
|
|
|
|
/**
|
|
* doCmd()
|
|
*
|
|
* @this {FDC}
|
|
*/
|
|
doCmd()
|
|
{
|
|
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?
|
|
*/
|
|
this.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'] >> 8);
|
|
} else {
|
|
/*
|
|
* TODO: Determine the appropriate response code(s) for the possible errors that can occur here.
|
|
*/
|
|
drive.resCode = FDC.REG_DATA.RES.NOT_READY | 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) {
|
|
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.messageEnabled()) {
|
|
this.printMessage("unsupported FDC operation: " + Str.toHexByte(bCmd));
|
|
if (MAXDEBUG) this.dbg.stopCPU();
|
|
}
|
|
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
|
|
*/
|
|
pushResults(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}
|
|
*/
|
|
popCmd(name)
|
|
{
|
|
this.assert((!this.regDataIndex || name !== undefined) && this.regDataIndex < this.regDataTotal);
|
|
var bCmd = this.regDataArray[this.regDataIndex];
|
|
if (DEBUG && this.messageEnabled(Messages.PORT | Messages.FDC)) {
|
|
var bCmdMasked = bCmd & FDC.REG_DATA.CMD.MASK;
|
|
if (!name && !this.regDataIndex && FDC.aCmdInfo[bCmdMasked]) name = FDC.aCmdInfo[bCmdMasked].name;
|
|
this.printMessage(this.idComponent + ".popCmd(" + (name || this.regDataIndex) + "): " + Str.toHexByte(bCmd), true);
|
|
}
|
|
this.regDataIndex++;
|
|
return bCmd;
|
|
}
|
|
|
|
/**
|
|
* popHLT()
|
|
*
|
|
* NOTE: This byte is actually a combination of HLT (Head Load Time) and ND (Non-DMA Mode)
|
|
*
|
|
* @this {FDC}
|
|
*/
|
|
popHLT()
|
|
{
|
|
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}
|
|
*/
|
|
popSRT()
|
|
{
|
|
this.popCmd(FDC.TERMS.SRT);
|
|
// this.nSRT = this.popCmd(FDC.TERMS.SRT);
|
|
}
|
|
|
|
/**
|
|
* beginResult()
|
|
*
|
|
* @this {FDC}
|
|
*/
|
|
beginResult()
|
|
{
|
|
this.regDataIndex = this.regDataTotal = 0;
|
|
}
|
|
|
|
/**
|
|
* pushResult(bResult, name)
|
|
*
|
|
* @this {FDC}
|
|
* @param {number} bResult
|
|
* @param {string|undefined} [name]
|
|
*/
|
|
pushResult(bResult, name)
|
|
{
|
|
if (DEBUG && this.messageEnabled(Messages.PORT | Messages.FDC)) {
|
|
this.printMessage(this.idComponent + ".pushResult(" + (name || this.regDataTotal) + "): " + Str.toHexByte(bResult), true);
|
|
}
|
|
this.assert(!(bResult & ~0xff));
|
|
this.regDataArray[this.regDataTotal++] = bResult;
|
|
}
|
|
|
|
/**
|
|
* pushST0(drive)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
*/
|
|
pushST0(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
|
|
*/
|
|
pushST1(drive)
|
|
{
|
|
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST1) >>> 8, FDC.TERMS.ST1);
|
|
}
|
|
|
|
/**
|
|
* pushST2(drive)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
*/
|
|
pushST2(drive)
|
|
{
|
|
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST2) >>> 16, FDC.TERMS.ST2);
|
|
}
|
|
|
|
/**
|
|
* pushST3(drive)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
*/
|
|
pushST3(drive)
|
|
{
|
|
this.pushResult((drive.resCode & FDC.REG_DATA.RES.ST3) >>> 24, FDC.TERMS.ST3);
|
|
}
|
|
|
|
/**
|
|
* doDMARead(drive, b, done)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @param {function(number,boolean)} done
|
|
*/
|
|
doDMARead(drive, b, done)
|
|
{
|
|
if (b === undefined || b < 0) {
|
|
this.readData(drive, done);
|
|
return;
|
|
}
|
|
/*
|
|
* The DMA controller should be ASKING for data, not GIVING us data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.printMessage(this.idComponent + ".doDMARead(): invalid DMA acknowledgement");
|
|
done(-1, false);
|
|
}
|
|
|
|
/**
|
|
* doDMAWrite(drive, b)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @return {number}
|
|
*/
|
|
doDMAWrite(drive, b)
|
|
{
|
|
if (b !== undefined && b >= 0)
|
|
return this.writeData(drive, b);
|
|
/*
|
|
* The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.printMessage(this.idComponent + ".doDMAWrite(): invalid DMA acknowledgement");
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* doDMAFormat(drive, b)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @returns {number}
|
|
*/
|
|
doDMAFormat(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.printMessage(this.idComponent + ".doDMAFormat(): invalid DMA acknowledgement");
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* doRead(drive)
|
|
*
|
|
* @this {FDC}
|
|
* @param {Object} drive
|
|
*/
|
|
doRead(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 (drive.disk) {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage(this.idComponent + ".doRead(CHS=" + drive.bCylinder + ':' + drive.bHead + ':' + drive.bSector + ",PBA=" + (drive.bCylinder * (drive.disk.nHeads * drive.disk.nSectors) + drive.bHead * drive.disk.nSectors + drive.bSector-1) + ')');
|
|
}
|
|
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
|
|
*/
|
|
doWrite(drive)
|
|
{
|
|
drive.resCode = FDC.REG_DATA.RES.NOT_READY | FDC.REG_DATA.RES.INCOMPLETE;
|
|
|
|
if (drive.disk) {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage(this.idComponent + ".doWrite(CHS=" + drive.bCylinder + ':' + drive.bHead + ':' + drive.bSector + ",PBA=" + (drive.bCylinder * (drive.disk.nHeads * drive.disk.nSectors) + drive.bHead * drive.disk.nSectors + drive.bSector-1) + ')');
|
|
}
|
|
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
|
|
*/
|
|
doFormat(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* readData(drive, done)
|
|
*
|
|
* 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 readData() 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 readData() 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,Object,number)} done (number is next available byte from drive, or -1 if no more bytes available)
|
|
*/
|
|
readData(drive, done)
|
|
{
|
|
var b = -1;
|
|
var obj = null, off = 0; // these variables are purely for BACKTRACK purposes
|
|
|
|
if (!drive.resCode && drive.disk) {
|
|
do {
|
|
if (drive.sector) {
|
|
off = drive.ibSector;
|
|
if ((b = drive.disk.read(drive.sector, drive.ibSector++)) >= 0) {
|
|
obj = drive.sector;
|
|
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 readData() to perform a seek without triggering an unwanted advance.
|
|
*/
|
|
this.advanceSector(drive);
|
|
} while (true);
|
|
}
|
|
done(b, false, obj, off);
|
|
}
|
|
|
|
/**
|
|
* writeData(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 writeData() 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 writeData() 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)
|
|
*/
|
|
writeData(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 writeData() 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
|
|
*/
|
|
advanceSector(drive)
|
|
{
|
|
this.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)
|
|
*/
|
|
writeFormat(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.messageEnabled()) {
|
|
this.printMessage(this.idComponent + ".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.writeData(drive, drive.bFiller) < 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
drive.cSectorsFormatted++;
|
|
}
|
|
if (drive.cSectorsFormatted >= drive.bSectorEnd) b = -1;
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* FDC.init()
|
|
*
|
|
* This function operates on every HTML element of class "fdc", extracting the
|
|
* JSON-encoded parameters for the FDC constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a FDC component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
static init()
|
|
{
|
|
var aeFDC = Component.getElementsByClass(document, PCX86.APPCLASS, "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, PCX86.APPCLASS);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = {
|
|
PORT: 0x3F2,
|
|
DS: 0x03, // drive select bits
|
|
DS0: 0x00,
|
|
DS1: 0x01,
|
|
DS2: 0x02, // reserved on the MODEL_5170
|
|
DS3: 0x03, // reserved on the MODEL_5170
|
|
ENABLE: 0x04, // clearing this bit resets the FDC
|
|
INT_ENABLE: 0x08, // enables both FDC and DMA (Channel 2) interrupt requests (IRQ 6)
|
|
MOTOR_D0: 0x10,
|
|
MOTOR_D1: 0x20,
|
|
MOTOR_D2: 0x40, // reserved on the MODEL_5170
|
|
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 = {
|
|
PORT: 0x3F4,
|
|
BUSY_A: 0x01,
|
|
BUSY_B: 0x02,
|
|
BUSY_C: 0x04, // reserved on the MODEL_5170
|
|
BUSY_D: 0x08, // reserved on the MODEL_5170
|
|
BUSY: 0x10, // a read or write command is in progress
|
|
NON_DMA: 0x20, // FDC is in non-DMA mode
|
|
READ_DATA: 0x40, // transfer is from FDC Data Register to processor (if clear, then transfer is from processor to the FDC Data Register)
|
|
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 = {
|
|
PORT: 0x3F5,
|
|
/*
|
|
* 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.
|
|
*/
|
|
CMD: {
|
|
READ_TRACK: 0x02,
|
|
SPECIFY: 0x03,
|
|
SENSE_DRIVE: 0x04,
|
|
WRITE_DATA: 0x05,
|
|
READ_DATA: 0x06,
|
|
RECALIBRATE: 0x07,
|
|
SENSE_INT: 0x08, // this command is used to clear the FDC interrupt following the clearing/setting of FDC.REG_OUTPUT.ENABLE
|
|
WRITE_DEL_DATA: 0x09,
|
|
READ_ID: 0x0A,
|
|
READ_DEL_DATA: 0x0C,
|
|
FORMAT_TRACK: 0x0D,
|
|
SEEK: 0x0F,
|
|
SCAN_EQUAL: 0x11,
|
|
SCAN_LO_EQUAL: 0x19,
|
|
SCAN_HI_EQUAL: 0x1D,
|
|
MASK: 0x1F,
|
|
SK: 0x20, // SK (Skip Deleted Data Address Mark)
|
|
MF: 0x40, // MF (Modified Frequency Modulation)
|
|
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; also, "77 step pulses" sounds suspiciously like a typo (it's not 79?)
|
|
*/
|
|
RES: {
|
|
NONE: 0x00000000, // ST0 (IC): Normal termination of command (NT)
|
|
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
|
|
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
|
|
SEEK_END: 0x00000020, // ST0 (SE): When the FDC completes the Seek command, this flag is set to 1 (high)
|
|
INCOMPLETE: 0x00000040, // ST0 (IC): Abnormal termination of command (AT); execution of command was started, but was not successfully completed
|
|
RESET: 0x000000C0, // ST0 (IC): Abnormal termination because during command execution the ready signal from the drive changed state
|
|
INVALID: 0x00000080, // ST0 (IC): Invalid command issue (IC); command which was issued was never started
|
|
ST0: 0x000000FF,
|
|
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
|
|
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
|
|
NO_DATA: 0x00000400, // ST1 (ND): FDC cannot find specified sector (or specified ID if READ_ID command)
|
|
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
|
|
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
|
|
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
|
|
ST1: 0x0000FF00,
|
|
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
|
|
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
|
|
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
|
|
SCAN_EQUAL: 0x00080000, // ST2 (SH): During execution of the Scan command, if the condition of "equal" is satisfied, this flag is set
|
|
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
|
|
DATA_FIELD: 0x00200000, // ST2 (DD): If the FDC detects a CRC error in the data, then this flag is set
|
|
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
|
|
ST2: 0x00FF0000,
|
|
DRIVE: 0x03000000, // ST3 (Ux): Status of the "Drive Select" signals from the diskette drive
|
|
HEAD: 0x04000000, // ST3 (HD): Status of the "Side Select" signal from the diskette drive
|
|
TWOSIDE: 0x08000000, // ST3 (TS): Status of the "Two Side" signal from the diskette drive
|
|
TRACK0: 0x10000000, // ST3 (T0): Status of the "Track 0" signal from the diskette drive
|
|
READY: 0x20000000, // ST3 (RY): Status of the "Ready" signal from the diskette drive
|
|
WRITEPROT: 0x40000000, // ST3 (WP): Status of the "Write Protect" signal from the diskette drive
|
|
FAULT: 0x80000000|0, // ST3 (FT): Status of the "Fault" signal from the diskette drive
|
|
ST3: 0xFF000000|0
|
|
}
|
|
};
|
|
|
|
/*
|
|
* FDC "Fixed Disk" Register (0x3F6, write-only)
|
|
*
|
|
* Since this register's functions are all specific to the Hard Drive 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 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 = {
|
|
PORT: 0x3F7,
|
|
DS0: 0x01, // Drive Select 0
|
|
DS1: 0x02, // Drive Select 1
|
|
HS0: 0x04, // Head Select 0
|
|
HS1: 0x08, // Head Select 1
|
|
HS2: 0x10, // Head Select 2
|
|
HS3: 0x20, // Head Select 3
|
|
WRITE_GATE: 0x40, // Write Gate
|
|
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 = {
|
|
PORT: 0x3F7,
|
|
RATE500K: 0x00, // 500,000 bps
|
|
RATE300K: 0x02, // 300,000 bps
|
|
RATE250K: 0x01, // 250,000 bps
|
|
RATEUNUSED: 0x03
|
|
};
|
|
|
|
/*
|
|
* 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}
|
|
};
|
|
|
|
/*
|
|
* 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 = {
|
|
0x3F1: FDC.prototype.inFDCDiagnostic,
|
|
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
|
|
};
|
|
|
|
/*
|
|
* Initialize every Floppy Drive Controller (FDC) module on the page.
|
|
*/
|
|
Web.onInit(FDC.init);
|
|
|
|
if (NODE) module.exports = FDC;
|