1899 lines
73 KiB
JavaScript
1899 lines
73 KiB
JavaScript
/**
|
|
* @fileoverview Implements the PCjs Hard Drive Controller (HDC) component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* @suppress {missingProperties}
|
|
* Created 2012-Nov-26
|
|
*
|
|
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
|
|
* at <http://jsmachines.net/> and <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see Computer.sCopyright).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of the
|
|
* PCjs program for purposes of the GNU General Public License, and the author does not claim
|
|
* any copyright as to their contents.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
if (typeof module !== 'undefined') {
|
|
var str = require("../../shared/lib/strlib");
|
|
var web = require("../../shared/lib/weblib");
|
|
var DiskAPI = require("../../shared/lib/diskapi");
|
|
var Component = require("../../shared/lib/component");
|
|
var ChipSet = require("./chipset");
|
|
var Disk = require("./disk");
|
|
var State = require("./state");
|
|
}
|
|
|
|
/**
|
|
* HDC(parmsHDC)
|
|
*
|
|
* The HDC component simulates an STC-506/412 interface to an IBM-compatible fixed disk drive. The first
|
|
* such drive was a 10Mb 5.25-inch drive containing two platters and 4 heads. Data spanned 306 cylinders
|
|
* for a total of 1224 tracks, with 17 sectors/track and 512 bytes/sector.
|
|
*
|
|
* HDC supports the following component-specific properties:
|
|
*
|
|
* drives: an array of driveConfig objects, each containing 'name', 'path', 'size' and 'type' properties
|
|
*
|
|
* If 'path' is empty, a scratch disk image is created; otherwise, we make a note of the path, but we will NOT
|
|
* pre-load it like we do for floppy disk images.
|
|
*
|
|
* My current plan is to read all disk data on-demand, keeping a cache of what we've read, and possibly adding
|
|
* some read-ahead as well. Any portions of the disk image that are written before being read will never be read.
|
|
*
|
|
* TRIVIA: On p.1-179 of the PC XT Technical Reference Manual (revised APR83), it reads:
|
|
*
|
|
* "WARNING: The last cylinder on the fixed disk drive is reserved for diagnostic use.
|
|
* Diagnostic write tests will destroy any data on this cylinder."
|
|
*
|
|
* Does FDISK insure that the last cylinder is reserved? I'm sure we'll eventually find out.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsHDC
|
|
*/
|
|
function HDC(parmsHDC) {
|
|
|
|
Component.call(this, "HDC", parmsHDC, HDC);
|
|
|
|
this['dmaRead'] = this.dmaRead;
|
|
this['dmaWrite'] = this.dmaWrite;
|
|
this['dmaWriteBuffer'] = this.dmaWriteBuffer;
|
|
this['dmaWriteFormat'] = this.dmaWriteFormat;
|
|
|
|
this.aDriveConfigs = [];
|
|
|
|
if (parmsHDC['drives']) {
|
|
try {
|
|
/*
|
|
* The most likely source of any exception will be right here, where we're parsing
|
|
* the JSON-encoded disk data.
|
|
*/
|
|
this.aDriveConfigs = eval("(" + parmsHDC['drives'] + ")");
|
|
/*
|
|
* Nothing more to do with aDriveConfigs now. initController() and autoMount() (if there are
|
|
* any disk image "path" properties to process) will take care of the rest.
|
|
*/
|
|
} catch (e) {
|
|
this.error("HDC drive configuration error: " + e.message + " (" + parmsHDC['drives'] + ")");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The remainder of HDC initialization now takes place in our initBus() handler
|
|
*/
|
|
}
|
|
|
|
Component.subclass(Component, HDC);
|
|
|
|
/*
|
|
* HDC defaults, in case drive parameters weren't specified
|
|
*/
|
|
HDC.DEFAULT_DRIVE_NAME = "Hard Drive";
|
|
HDC.DEFAULT_DRIVE_TYPE = 0x03;
|
|
|
|
/*
|
|
* Each of the following DriveType arrays contain 4 values:
|
|
*
|
|
* [0]: total cylinders
|
|
* [1]: total heads
|
|
* [2]: total sectors/tracks
|
|
* [3]: total bytes/sector
|
|
*
|
|
* verifyDrive() attempts to confirm that these values agree with the programmed drive characteristics.
|
|
*/
|
|
HDC.aDriveTypes = {
|
|
0x00: [306, 2, 17, 512],
|
|
0x01: [375, 8, 17, 512],
|
|
0x02: [306, 6, 17, 512],
|
|
0x03: [306, 4, 17, 512]
|
|
};
|
|
|
|
/*
|
|
* HDC Data Register (0x320, read-write)
|
|
*
|
|
* Writes to this register are discussed below; see HDC Commands.
|
|
*
|
|
* Reads from this register after a command has been executed retrieve a "status byte",
|
|
* which must NOT be confused with the Status Register (see below). This data "status byte"
|
|
* contains only two bits of interest: REG_DATA.STATUS_ERROR and REG_DATA.STATUS_UNIT.
|
|
*/
|
|
HDC.REG_DATA = {};
|
|
HDC.REG_DATA.PORT = 0x320; // port address
|
|
HDC.REG_DATA.STATUS_OK = 0x00; // no error
|
|
HDC.REG_DATA.STATUS_ERROR = 0x02; // error occurred during command execution
|
|
HDC.REG_DATA.STATUS_UNIT = 0x20; // logical unit number of the drive
|
|
|
|
/*
|
|
* HDC Status Register (0x321, read-only)
|
|
*
|
|
* WARNING: The IBM Technical Reference Manual *badly* confuses the REG_DATA "status byte" (above)
|
|
* that the controller sends following an HDC.REG_DATA.CMD operation with the Status Register (below).
|
|
* In fact, it's so badly confused that it completely fails to document any of the Status Register
|
|
* bits below; I'm forced to guess at their meanings from the HDC BIOS listing.
|
|
*/
|
|
HDC.REG_STATUS = {};
|
|
HDC.REG_STATUS.PORT = 0x321; // port address
|
|
HDC.REG_STATUS.NONE = 0x00;
|
|
HDC.REG_STATUS.REQ = 0x01; // HDC BIOS: request bit
|
|
HDC.REG_STATUS.IOMODE = 0x02; // HDC BIOS: mode bit (GUESS: set whenever REG_DATA contains a response?)
|
|
HDC.REG_STATUS.BUS = 0x04; // HDC BIOS: command/data bit (GUESS: set whenever REG_DATA ready for request?)
|
|
HDC.REG_STATUS.BUSY = 0x08; // HDC BIOS: busy bit
|
|
HDC.REG_STATUS.INTERRUPT = 0x20; // HDC BIOS: interrupt bit
|
|
|
|
/*
|
|
* HDC Config Register (0x322, read-only)
|
|
*
|
|
* This register is used to read HDC card switch settings that defined the "Drive Type" for
|
|
* drives 0 and 1. SW[1],SW[2] (for drive 0) and SW[3],SW[4] (for drive 1) are set as follows:
|
|
*
|
|
* ON, ON Drive Type 0 (306 cylinders, 2 heads)
|
|
* ON, OFF Drive Type 1 (375 cylinders, 8 heads)
|
|
* OFF, ON Drive Type 2 (306 cylinders, 6 heads)
|
|
* OFF, OFF Drive Type 3 (306 cylinders, 4 heads)
|
|
*/
|
|
|
|
/*
|
|
* HDC Commands, as issued to REG_DATA
|
|
*
|
|
* Commands are multi-byte sequences sent to REG_DATA, starting with a REG_DATA.CMD byte,
|
|
* and followed by 5 more bytes, for a total of 6 bytes, which collectively are called a
|
|
* Device Control Block (DCB). Not all commands use all 6 bytes, but all 6 bytes must be present;
|
|
* unused bytes are simply ignored.
|
|
*
|
|
* REG_DATA.CMD (3-bit class code, 5-bit operation code)
|
|
* REG_DATA.HEAD (1-bit drive number, 5-bit head number)
|
|
* REG_DATA.CLSEC (upper bits of 10-bit cylinder number, 6-bit sector number)
|
|
* REG_DATA.CH (lower bits of 10-bit cylinder number)
|
|
* REG_DATA.COUNT (8-bit interleave or block count)
|
|
* REG_DATA.CTRL (8-bit control field)
|
|
*
|
|
* One command, HDC.REG_DATA.CMD.INIT_DRIVE, must include 8 additional bytes following the DCB:
|
|
*
|
|
* maximum number of cylinders (high)
|
|
* maximum number of cylinders (low)
|
|
* maximum number of heads
|
|
* start reduced write current cylinder (high)
|
|
* start reduced write current cylinder (low)
|
|
* start write precompensation cylinder (high)
|
|
* start write precompensation cylinder (low)
|
|
* maximum ECC data burst length
|
|
*
|
|
* Note that the 3 word values above are stored in "big-endian" format (high byte followed by low byte),
|
|
* rather than the more typical "little-endian" format (low byte followed by high byte).
|
|
*/
|
|
HDC.REG_DATA.CMD = {};
|
|
HDC.REG_DATA.CMD.TEST_READY = 0x00; // Test Drive Ready
|
|
HDC.REG_DATA.CMD.RECALIBRATE = 0x01; // Recalibrate
|
|
HDC.REG_DATA.CMD.REQUEST_SENSE = 0x03; // Request Sense Status
|
|
HDC.REG_DATA.CMD.FORMAT_DRIVE = 0x04; // Format Drive
|
|
HDC.REG_DATA.CMD.READ_VERIFY = 0x05; // Read Verify
|
|
HDC.REG_DATA.CMD.FORMAT_TRACK = 0x06; // Format Track
|
|
HDC.REG_DATA.CMD.FORMAT_BAD = 0x07; // Format Bad Track
|
|
HDC.REG_DATA.CMD.READ_DATA = 0x08; // Read
|
|
HDC.REG_DATA.CMD.WRITE_DATA = 0x0A; // Write
|
|
HDC.REG_DATA.CMD.SEEK = 0x0B; // Seek
|
|
HDC.REG_DATA.CMD.INIT_DRIVE = 0x0C; // Initialize Drive Characteristics
|
|
HDC.REG_DATA.CMD.READ_ECC_BURST = 0x0D; // Read ECC Burst Error Length
|
|
HDC.REG_DATA.CMD.READ_BUFFER = 0x0E; // Read Data from Sector Buffer
|
|
HDC.REG_DATA.CMD.WRITE_BUFFER = 0x0F; // Write Data to Sector Buffer
|
|
HDC.REG_DATA.CMD.RAM_DIAGNOSTIC = 0xE0; // RAM Diagnostic
|
|
HDC.REG_DATA.CMD.DRV_DIAGNOSTIC = 0xE3; // HDC BIOS: CHK_DRV_CMD
|
|
HDC.REG_DATA.CMD.CTL_DIAGNOSTIC = 0xE4; // HDC BIOS: CNTLR_DIAG_CMD
|
|
HDC.REG_DATA.CMD.READ_LONG = 0xE5; // HDC BIOS: RD_LONG_CMD
|
|
HDC.REG_DATA.CMD.WRITE_LONG = 0xE6; // HDC BIOS: WR_LONG_CMD
|
|
|
|
/*
|
|
* HDC error conditions, as returned in byte 0 of the (4) bytes returned by the Request Sense Status command
|
|
*/
|
|
HDC.REG_DATA.ERR = {};
|
|
HDC.REG_DATA.ERR.NONE = 0x00;
|
|
HDC.REG_DATA.ERR.NO_INDEX = 0x01; // no index signal detected
|
|
HDC.REG_DATA.ERR.SEEK_INCOMPLETE= 0x02; // no seek-complete signal
|
|
HDC.REG_DATA.ERR.WRITE_FAULT = 0x03;
|
|
HDC.REG_DATA.ERR.NOT_READY = 0x04; // after the controller selected the drive, the drive did not respond with a ready signal
|
|
HDC.REG_DATA.ERR.NO_TRACK = 0x06; // after stepping the max number of cylinders, the controller did not receive the track 00 signal from the drive
|
|
HDC.REG_DATA.ERR.STILL_SEEKING = 0x08;
|
|
HDC.REG_DATA.ERR.ECC_ID_ERROR = 0x10;
|
|
HDC.REG_DATA.ERR.ECC_DATA_ERROR = 0x11;
|
|
HDC.REG_DATA.ERR.NO_ADDR_MARK = 0x12;
|
|
HDC.REG_DATA.ERR.NO_SECTOR = 0x14;
|
|
HDC.REG_DATA.ERR.BAD_SEEK = 0x15; // seek error: the cylinder and/or head address did not compare with the expected target address
|
|
HDC.REG_DATA.ERR.ECC_CORRECTABLE= 0x18; // correctable data error
|
|
HDC.REG_DATA.ERR.BAD_TRACK = 0x19;
|
|
HDC.REG_DATA.ERR.BAD_CMD = 0x20;
|
|
HDC.REG_DATA.ERR.BAD_DISK_ADDR = 0x21;
|
|
HDC.REG_DATA.ERR.RAM = 0x30;
|
|
HDC.REG_DATA.ERR.CHECKSUM = 0x31;
|
|
HDC.REG_DATA.ERR.POLYNOMIAL = 0x32;
|
|
HDC.REG_DATA.ERR.MASK = 0x3F;
|
|
HDC.REG_DATA.SENSE_ADDR_VALID = 0x80;
|
|
|
|
/*
|
|
* HDC Command Sequences
|
|
*
|
|
* Unlike the FDC, all the HDC commands have fixed-length command request sequences (well, OK, except for
|
|
* HDC.REG_DATA.CMD.INIT_DRIVE) and fixed-length response sequences (well, OK, except for HDC.REG_DATA.CMD.REQUEST_SENSE),
|
|
* so a table of byte-lengths isn't much use, but having names for all the commands is still handy for debugging.
|
|
*/
|
|
if (DEBUG) {
|
|
HDC.aCmdNames = {
|
|
0x00: "Test Drive Ready",
|
|
0x01: "Recalibrate",
|
|
0x03: "Request Sense Status",
|
|
0x04: "Format Drive",
|
|
0x05: "Read Verify",
|
|
0x06: "Format Track",
|
|
0x07: "Format Bad Track",
|
|
0x08: "Read",
|
|
0x0A: "Write",
|
|
0x0B: "Seek",
|
|
0x0C: "Initialize Drive Characteristics",
|
|
0x0D: "Read ECC Burst Error Length",
|
|
0x0E: "Read Data from Sector Buffer",
|
|
0x0F: "Write Data to Sector Buffer",
|
|
0xE0: "RAM Diagnostic",
|
|
0xE3: "Drive Diagnostic",
|
|
0xE4: "Controller Diagnostic",
|
|
0xE5: "Read Long",
|
|
0xE6: "Write Long"
|
|
};
|
|
}
|
|
|
|
/*
|
|
* HDC BIOS interrupts, functions, and other parameters
|
|
*/
|
|
HDC.BIOS = {};
|
|
HDC.BIOS.DISK_INT = 0x13;
|
|
|
|
HDC.BIOS.DISK_CMD = {};
|
|
HDC.BIOS.DISK_CMD.RESET = 0x00;
|
|
HDC.BIOS.DISK_CMD.GET_STATUS = 0x01;
|
|
HDC.BIOS.DISK_CMD.READ_SECTORS = 0x02;
|
|
HDC.BIOS.DISK_CMD.WRITE_SECTORS = 0x03;
|
|
HDC.BIOS.DISK_CMD.VERIFY_SECTORS = 0x04;
|
|
HDC.BIOS.DISK_CMD.FORMAT_TRACK = 0x05;
|
|
HDC.BIOS.DISK_CMD.FORMAT_BAD = 0x06;
|
|
HDC.BIOS.DISK_CMD.FORMAT_DRIVE = 0x07;
|
|
HDC.BIOS.DISK_CMD.GET_DRIVEPARMS = 0x08;
|
|
HDC.BIOS.DISK_CMD.SET_DRIVEPARMS = 0x09;
|
|
HDC.BIOS.DISK_CMD.READ_LONG = 0x0A;
|
|
HDC.BIOS.DISK_CMD.WRITE_LONG = 0x0B;
|
|
HDC.BIOS.DISK_CMD.SEEK = 0x0C;
|
|
HDC.BIOS.DISK_CMD.ALT_RESET = 0x0D;
|
|
HDC.BIOS.DISK_CMD.READ_BUFFER = 0x0E;
|
|
HDC.BIOS.DISK_CMD.WRITE_BUFFER = 0x0F;
|
|
HDC.BIOS.DISK_CMD.TEST_READY = 0x10;
|
|
HDC.BIOS.DISK_CMD.RECALIBRATE = 0x11;
|
|
HDC.BIOS.DISK_CMD.RAM_DIAGNOSTIC = 0x12;
|
|
HDC.BIOS.DISK_CMD.DRV_DIAGNOSTIC = 0x13;
|
|
HDC.BIOS.DISK_CMD.CTL_DIAGNOSTIC = 0x14;
|
|
|
|
/*
|
|
* When the HDC BIOS overwrites the ROM BIOS INT 0x13 address, it saves the original INT 0x13 address
|
|
* in the INT 0x40 vector.
|
|
*/
|
|
HDC.BIOS.DISKETTE_INT = 0x40;
|
|
|
|
/**
|
|
* setBinding(sHTMLClass, sHTMLType, sBinding, control)
|
|
*
|
|
* @this {HDC}
|
|
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisks")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
HDC.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control) {
|
|
/*
|
|
* This is reserved for future use; for now, hard disk images can be specified during initialization only (no "hot-swapping")
|
|
*/
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {X86CPU} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
HDC.prototype.initBus = function(cmp, bus, cpu, dbg) {
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.cmp = cmp;
|
|
|
|
/*
|
|
* We need access to the ChipSet component, because we need to communicate with
|
|
* the PIC and DMA controller.
|
|
*/
|
|
this.chipset = cmp.getComponentByType("ChipSet");
|
|
|
|
bus.addPortInputTable(this, HDC.aPortInput);
|
|
bus.addPortOutputTable(this, HDC.aPortOutput);
|
|
|
|
if (DEBUGGER) {
|
|
cpu.addInterruptNotify(HDC.BIOS.DISK_INT, this, this.intBIOSDisk);
|
|
cpu.addInterruptNotify(HDC.BIOS.DISKETTE_INT, this, this.intBIOSDiskette);
|
|
}
|
|
|
|
/*
|
|
* The following code used to be performed in the HDC constructor, but now we need to wait for information
|
|
* about the Computer to be available (eg, getMachineID() and getUserID()) before we start loading and/or
|
|
* connecting to disk images.
|
|
*
|
|
* If we didn't need auto-mount support, we could defer controller initialization until we received a powerUp()
|
|
* notification, at which point reset() would call initController(), or restore() would restore the controller;
|
|
* in that case, all we'd need to do here is call setReady().
|
|
*/
|
|
this.initController();
|
|
|
|
if (!this.autoMount()) {
|
|
this.setReady();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
HDC.prototype.powerUp = function(data, fRepower) {
|
|
if (!fRepower) {
|
|
if (!data || !this.restore) {
|
|
this.reset();
|
|
if (this.cmp.fReload) {
|
|
/*
|
|
* If the computer's fReload flag is set, we're required to toss all currently
|
|
* loaded disks and remount all disks specified in the auto-mount configuration.
|
|
*/
|
|
this.autoMount(true);
|
|
}
|
|
} else {
|
|
if (!this.restore(data)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {HDC}
|
|
* @param {boolean} fSave
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean}
|
|
*/
|
|
HDC.prototype.powerDown = function(fSave, fShutdown) {
|
|
return fSave && this.save? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* getMachineID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
HDC.prototype.getMachineID = function() {
|
|
return this.cmp? this.cmp.getMachineID() : "";
|
|
};
|
|
|
|
/**
|
|
* getUserID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
HDC.prototype.getUserID = function() {
|
|
return this.cmp? this.cmp.getUserID() : "";
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {HDC}
|
|
*/
|
|
HDC.prototype.reset = function() {
|
|
/*
|
|
* NOTE: The controller is also initialized by the constructor, to assist with auto-mount support,
|
|
* so think about whether we can skip powerUp initialization.
|
|
*/
|
|
this.initController();
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements save support for the HDC component.
|
|
*
|
|
* @this {HDC}
|
|
* @return {Object}
|
|
*/
|
|
HDC.prototype.save = function() {
|
|
var state = new State(this);
|
|
state.set(0, this.saveController());
|
|
return state.data();
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements restore support for the HDC component.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} data
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
HDC.prototype.restore = function(data) {
|
|
return this.initController(data[0]);
|
|
};
|
|
|
|
/**
|
|
* initController(data)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Array} [data]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
HDC.prototype.initController = function(data) {
|
|
var i = 0;
|
|
var fSuccess = true;
|
|
if (data === undefined) data = [0, HDC.REG_STATUS.NONE, new Array(14), 0, 0];
|
|
this.regConfig = data[i++];
|
|
this.regStatus = data[i++];
|
|
this.regDataArray = data[i++]; // there can be up to 14 command bytes (6 for normal commands, plus 8 more for HDC.REG_DATA.CMD.INIT_DRIVE)
|
|
this.regDataIndex = data[i++]; // used to control the next data byte to be received
|
|
this.regDataTotal = data[i++]; // used to control the next data byte to be sent (internally, we use regDataIndex to read data bytes, up to this total)
|
|
this.regReset = data[i++];
|
|
this.regPulse = data[i++];
|
|
this.regPattern = data[i++];
|
|
|
|
/*
|
|
* Initialize iDriveAllowFail only if it's never been initialized before, otherwise its entire
|
|
* purpose will be defeated. See the related HACK in intBIOSDisk() for more details.
|
|
*/
|
|
var iDriveAllowFail = data[i++];
|
|
if (iDriveAllowFail !== undefined) {
|
|
this.iDriveAllowFail = iDriveAllowFail;
|
|
} else {
|
|
if (this.iDriveAllowFail === undefined) this.iDriveAllowFail = -1;
|
|
}
|
|
|
|
if (this.aDrives === undefined) {
|
|
this.aDrives = new Array(this.aDriveConfigs.length);
|
|
}
|
|
|
|
var dataDrives = data[i];
|
|
if (dataDrives === undefined) dataDrives = [];
|
|
|
|
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
|
|
if (this.aDrives[iDrive] === undefined) {
|
|
this.aDrives[iDrive] = {};
|
|
}
|
|
var drive = this.aDrives[iDrive];
|
|
var driveConfig = this.aDriveConfigs[iDrive];
|
|
if (!this.initDrive(iDrive, drive, driveConfig, dataDrives[iDrive])) {
|
|
fSuccess = false;
|
|
}
|
|
/*
|
|
* The original STC-506/412 controller had two pairs of DIP switches to indicate a drive
|
|
* type (0, 1, 2 or 3) for drives 0 and 1. Those switch settings are recorded in regConfig,
|
|
* now that drive.type has been validated by initDrive().
|
|
*/
|
|
if (iDrive <= 1) {
|
|
this.regConfig |= (drive.type & 0x3) << ((1 - iDrive) << 1);
|
|
}
|
|
}
|
|
if (DEBUG) this.messageDebugger("HDC initialized for " + this.aDrives.length + " drive(s)");
|
|
return fSuccess;
|
|
};
|
|
|
|
/**
|
|
* saveController()
|
|
*
|
|
* @this {HDC}
|
|
* @return {Array}
|
|
*/
|
|
HDC.prototype.saveController = function() {
|
|
var i = 0;
|
|
var data = [];
|
|
data[i++] = this.regConfig;
|
|
data[i++] = this.regStatus;
|
|
data[i++] = this.regDataArray;
|
|
data[i++] = this.regDataIndex;
|
|
data[i++] = this.regDataTotal;
|
|
data[i++] = this.regReset;
|
|
data[i++] = this.regPulse;
|
|
data[i++] = this.regPattern;
|
|
data[i++] = this.iDriveAllowFail;
|
|
data[i] = this.saveDrives();
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* initDrive(iDrive, drive, driveConfig, data)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} iDrive
|
|
* @param {Object} drive
|
|
* @param {Object} driveConfig (contains one or more of the following properties: 'name', 'path', 'size', 'type')
|
|
* @param {Array} [data]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
HDC.prototype.initDrive = function(iDrive, drive, driveConfig, data) {
|
|
var i = 0;
|
|
var fSuccess = true;
|
|
if (data === undefined) data = [HDC.REG_DATA.ERR.NONE, 0, false, new Array(8)];
|
|
|
|
drive.iDrive = iDrive;
|
|
|
|
/*
|
|
* errorCode could be an HDC global, but in order to insulate HDC state from the operation of various functions that operate on drive
|
|
* objects (eg, readByte and writeByte), I've made it a per-drive variable. This choice is probably 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.errorCode = data[i++];
|
|
drive.senseCode = data[i++];
|
|
drive.fRemovable = data[i++];
|
|
drive.abDriveParms = data[i++]; // captures drive parameters programmed via HDC.REG_DATA.CMD.INIT_DRIVE
|
|
drive.abSectorBuffer = data[i++];
|
|
|
|
/*
|
|
* The next group of properties are set by various HDC command sequences.
|
|
*/
|
|
drive.bHead = data[i++];
|
|
drive.nHeads = data[i++];
|
|
drive.wCylinder = data[i++];
|
|
drive.bSector = data[i++];
|
|
drive.bSectorEnd = data[i++]; // aka EOT
|
|
drive.nBytes = data[i++];
|
|
|
|
drive.name = driveConfig['name'];
|
|
if (drive.name === undefined) drive.name = HDC.DEFAULT_DRIVE_NAME;
|
|
drive.path = driveConfig['path'];
|
|
|
|
/*
|
|
* If no 'mode' is specified, we fall back to the original behavior, which is to completely preload
|
|
* any specific disk image, or create an empty (purely local) disk image.
|
|
*/
|
|
drive.mode = driveConfig['mode'] || (drive.path? DiskAPI.MODE.PRELOAD : DiskAPI.MODE.LOCAL);
|
|
|
|
/*
|
|
* On-demand I/O of raw disk images is supported only if there's a valid user ID; fall back to an empty
|
|
* local disk image if there's not.
|
|
*/
|
|
if (drive.mode == DiskAPI.MODE.DEMANDRO || drive.mode == DiskAPI.MODE.DEMANDRW) {
|
|
if (!this.getUserID()) drive.mode = DiskAPI.MODE.LOCAL;
|
|
}
|
|
|
|
drive.type = driveConfig['type'];
|
|
if (drive.type === undefined || HDC.aDriveTypes[drive.type] === undefined) drive.type = HDC.DEFAULT_DRIVE_TYPE;
|
|
var driveType = HDC.aDriveTypes[drive.type];
|
|
drive.nSectors = driveType[2]; // sectors/track
|
|
drive.cbSector = driveType[3]; // bytes/sector
|
|
|
|
/*
|
|
* The next group of properties are set by user requests to load/unload disk images.
|
|
*
|
|
* NOTE: I now avoid reinitializing drive.disk in order to retain any previously mounted disk across resets.
|
|
*/
|
|
if (drive.disk === undefined) {
|
|
drive.disk = null;
|
|
this.notice("Type " + drive.type + " \"" + drive.name + "\" is fixed disk " + iDrive, true);
|
|
}
|
|
|
|
/*
|
|
* With the advent of save/restore, we need to verify every drive at initialization, not just whenever
|
|
* drive characteristics are initialized. Thus, if we've restored a sensible set of drive characteristics,
|
|
* then verifyDrive will create an empty disk if none has been provided, insuring we are ready for
|
|
* disk.restore().
|
|
*/
|
|
this.verifyDrive(drive);
|
|
|
|
/*
|
|
* 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 above sector
|
|
drive.sector = null; // initialized to null by worker, and then set to the next sector satisfying the request
|
|
|
|
if (drive.disk) {
|
|
var deltas = data[i];
|
|
if (deltas !== undefined && drive.disk.restore(deltas) < 0) {
|
|
fSuccess = false;
|
|
}
|
|
if (fSuccess && drive.ibSector !== undefined) {
|
|
drive.sector = drive.disk.seek(drive.wCylinder, drive.bHead, drive.bSector + 1);
|
|
}
|
|
}
|
|
return fSuccess;
|
|
};
|
|
|
|
/**
|
|
* saveDrives()
|
|
*
|
|
* @this {HDC}
|
|
* @return {Array}
|
|
*/
|
|
HDC.prototype.saveDrives = function() {
|
|
var i = 0;
|
|
var data = [];
|
|
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
|
|
data[i++] = this.saveDrive(this.aDrives[iDrive]);
|
|
}
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* saveDrive(drive)
|
|
*
|
|
* @this {HDC}
|
|
* @return {Array}
|
|
*/
|
|
HDC.prototype.saveDrive = function(drive) {
|
|
var i = 0;
|
|
var data = [];
|
|
data[i++] = drive.errorCode;
|
|
data[i++] = drive.senseCode;
|
|
data[i++] = drive.fRemovable;
|
|
data[i++] = drive.abDriveParms;
|
|
data[i++] = drive.abSectorBuffer;
|
|
data[i++] = drive.bHead;
|
|
data[i++] = drive.nHeads;
|
|
data[i++] = drive.wCylinder;
|
|
data[i++] = drive.bSector;
|
|
data[i++] = drive.bSectorEnd;
|
|
data[i++] = drive.nBytes;
|
|
data[i++] = drive.ibSector;
|
|
data[i] = drive.disk? drive.disk.save() : null;
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* copyDrive(iDrive)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} iDrive
|
|
* @return {Object|undefined} (undefined if the requested drive does not exist)
|
|
*/
|
|
HDC.prototype.copyDrive = function(iDrive) {
|
|
var driveNew;
|
|
var driveOld = this.aDrives[iDrive];
|
|
if (driveOld !== undefined) {
|
|
driveNew = {};
|
|
for (var p in driveOld) {
|
|
driveNew[p] = driveOld[p];
|
|
}
|
|
}
|
|
return driveNew;
|
|
};
|
|
|
|
/**
|
|
* verifyDrive(drive, type)
|
|
*
|
|
* If no disk image is attached, create an empty disk with the specified drive characteristics.
|
|
* Normally, we'd rely on the drive characteristics programmed via the HDC.REG_DATA.CMD.INIT_DRIVE
|
|
* command, but if an explicit drive type is specified, then we use the characteristics (geometry)
|
|
* associated with that type.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} [type] to create a disk of the specified type, if no disk exists yet
|
|
*/
|
|
HDC.prototype.verifyDrive = function(drive, type) {
|
|
if (drive) {
|
|
var nHeads = 0, nCylinders = 0;
|
|
if (type == null) {
|
|
/*
|
|
* If the caller wants us to use the programmed drive parameters, we use those,
|
|
* but if there aren't any drive parameters (yet), then use default parameters based
|
|
* on drive.type.
|
|
*
|
|
* We used to do the last step ONLY if there was no drive.path -- otherwise, we'd waste
|
|
* time creating an empty disk if autoMount() was going to load an image from drive.path;
|
|
* but hopefully the Disk component is smarter now.
|
|
*/
|
|
nHeads = drive.abDriveParms[2];
|
|
if (nHeads) {
|
|
nCylinders = (drive.abDriveParms[0] << 8) | drive.abDriveParms[1];
|
|
} else {
|
|
type = drive.type;
|
|
}
|
|
}
|
|
if (type != null && !nHeads) {
|
|
nHeads = HDC.aDriveTypes[type][1];
|
|
nCylinders = HDC.aDriveTypes[type][0];
|
|
}
|
|
if (nHeads) {
|
|
/*
|
|
* The assumption here is that if the 3rd drive parameter byte (abDriveParms[2]) has been set
|
|
* (ie, if nHeads is valid) then the first two bytes (ie, the low and high cylinder byte values)
|
|
* must have been set as well.
|
|
*
|
|
* Do these values agree with those for the given drive type? Even if they don't, all we do is warn.
|
|
*/
|
|
var driveType = HDC.aDriveTypes[drive.type];
|
|
if (driveType) {
|
|
if (nCylinders != driveType[0] && nHeads != driveType[1]) {
|
|
this.notice("Warning: drive parameters (" + nCylinders + "," + nHeads + ") do not match drive type " + drive.type + " (" + driveType[0] + "," + driveType[1] + ")");
|
|
}
|
|
}
|
|
drive.nCylinders = nCylinders;
|
|
drive.nHeads = nHeads;
|
|
if (drive.disk == null) {
|
|
drive.disk = new Disk(this, drive, drive.mode);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* seekDrive(drive, iSector, nSectors)
|
|
*
|
|
* The HDC doesn't need this function, since all HDC requests from the CPU are handled by doCmd(). This function
|
|
* is used by other components (eg, Debugger) to mimic an HDC request, using a drive object obtained from copyDrive(),
|
|
* to avoid disturbing the internal state of the HDC's drive objects.
|
|
*
|
|
* Also note that in an actual HDC 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 HDC
|
|
* isn't aware of the extent of the transfer, so in the case of a read request, all readByte() can do is return bytes
|
|
* until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted.
|
|
*
|
|
* Since seekDrive() is for use with non-DMA requests, we use nBytes to specify the length of the entire transfer.
|
|
*
|
|
* @this {HDC}
|
|
* @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
|
|
*/
|
|
HDC.prototype.seekDrive = function(drive, iSector, nSectors) {
|
|
if (drive.disk) {
|
|
var aDiskInfo = drive.disk.info();
|
|
var nCylinders = aDiskInfo[0];
|
|
/*
|
|
* If nCylinders is zero, we probably have an empty disk image, awaiting initialization (see verifyDrive())
|
|
*/
|
|
if (nCylinders) {
|
|
var nHeads = aDiskInfo[1];
|
|
var nSectorsPerTrack = aDiskInfo[2];
|
|
var nSectorsPerCylinder = nHeads * nSectorsPerTrack;
|
|
var nSectorsPerDisk = nCylinders * nSectorsPerCylinder;
|
|
if (iSector + nSectors <= nSectorsPerDisk) {
|
|
drive.wCylinder = Math.floor(iSector / nSectorsPerCylinder);
|
|
iSector %= nSectorsPerCylinder;
|
|
drive.bHead = Math.floor(iSector / nSectorsPerTrack);
|
|
/*
|
|
* Important difference between the FDC and the HDC: the HDC uses 0-based sector numbers, so unlike
|
|
* FDC.seekDrive(), we must NOT add 1 to bSector below. I could change how sector numbers are stored in
|
|
* hard disk images, but it seems preferable to keep the image format consistent and controller-independent.
|
|
*/
|
|
drive.bSector = (iSector % nSectorsPerTrack);
|
|
drive.nBytes = nSectors * aDiskInfo[3];
|
|
/*
|
|
* NOTE: We don't set nSectorEnd, as an HDC command would, but it's irrelevant, because we don't actually
|
|
* do anything with nSectorEnd 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.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
/*
|
|
* At this point, we've finished simulating what an HDC.REG_DATA.CMD.READ_DATA command would have performed,
|
|
* up through doRead(). Now it's the caller responsibility to call readByte(), like the DMA Controller would.
|
|
*/
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* autoMount(fRemount)
|
|
*
|
|
* @this {HDC}
|
|
* @param {boolean} [fRemount] is true if we're remounting all auto-mounted disks
|
|
* @return {boolean} true if one or more disk images are being auto-mounted, false if none
|
|
*/
|
|
HDC.prototype.autoMount = function(fRemount) {
|
|
if (!fRemount) this.cAutoMount = 0;
|
|
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
|
|
var drive = this.aDrives[iDrive];
|
|
if (drive.name && drive.path) {
|
|
if (!this.loadDisk(iDrive, drive.name, drive.path, true) && fRemount)
|
|
this.setReady(false);
|
|
continue;
|
|
}
|
|
if (fRemount && drive.type !== undefined) {
|
|
drive.disk = null;
|
|
this.verifyDrive(drive, drive.type);
|
|
}
|
|
}
|
|
return !!this.cAutoMount;
|
|
};
|
|
|
|
/**
|
|
* loadDisk(iDrive, sDiskName, sDiskPath, fAutoMount)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} iDrive
|
|
* @param {string} sDiskName
|
|
* @param {string} sDiskPath
|
|
* @param {boolean} fAutoMount
|
|
* @return {boolean} true if disk (already) loaded, false if queued up (or busy)
|
|
*/
|
|
HDC.prototype.loadDisk = function(iDrive, sDiskName, sDiskPath, fAutoMount) {
|
|
var drive = this.aDrives[iDrive];
|
|
if (drive.fBusy) {
|
|
this.notice("Drive " + iDrive + " busy");
|
|
return true;
|
|
}
|
|
drive.fBusy = true;
|
|
if (fAutoMount) {
|
|
drive.fAutoMount = true;
|
|
this.cAutoMount++;
|
|
this.messageDebugger("loading " + sDiskName);
|
|
}
|
|
var disk = drive.disk || new Disk(this, drive, drive.mode);
|
|
disk.load(sDiskName, sDiskPath, this.mountDisk);
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* mountDisk(drive, disk, sDiskName, sDiskPath)
|
|
*
|
|
* This is a callback issued by the Disk component once its own mount() operation has finished.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {Disk} disk is set if the disk was successfully mounted, null if not
|
|
* @param {string} sDiskName
|
|
* @param {string} sDiskPath
|
|
*/
|
|
HDC.prototype.mountDisk = function(drive, disk, sDiskName, sDiskPath) {
|
|
drive.fBusy = false;
|
|
if ((drive.disk = disk)) {
|
|
/*
|
|
* 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, otherwise alert().
|
|
*
|
|
* WARNING: This conversion of drive number to drive letter, starting with "C:" (0x43), is very simplistic
|
|
* and is not guaranteed to match the drive mapping that DOS ultimately uses.
|
|
*/
|
|
this.notice("Mounted disk \"" + sDiskName + "\" in drive " + String.fromCharCode(0x43 + drive.iDrive), drive.fAutoMount);
|
|
}
|
|
if (drive.fAutoMount) {
|
|
drive.fAutoMount = false;
|
|
if (!--this.cAutoMount) this.setReady();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inHDCData(port, addrFrom)
|
|
*
|
|
* NOTE: At the moment, we support only auto-mounts; there is no user interface for selecting hard disk images,
|
|
* let alone unloading them, so there is currently no need for the following function.
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} iDrive
|
|
*
|
|
HDC.prototype.unloadDrive = function(iDrive) {
|
|
this.aDrives[iDrive].disk = null;
|
|
//
|
|
// WARNING: This conversion of drive number to drive letter, starting with "C:" (0x43), is very simplistic
|
|
// and is not guaranteed to match the drive mapping that DOS ultimately uses.
|
|
//
|
|
this.notice("Drive " + String.fromCharCode(0x43 + iDrive) + " unloaded");
|
|
};
|
|
*/
|
|
|
|
/**
|
|
* @this {HDC}
|
|
* @param {number} port (0x320)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
HDC.prototype.inHDCData = function(port, addrFrom) {
|
|
var bIn = 0;
|
|
if (this.regDataIndex < this.regDataTotal) {
|
|
bIn = this.regDataArray[this.regDataIndex];
|
|
}
|
|
if (this.chipset) this.chipset.clearIRR(ChipSet.IRQ.HDC);
|
|
this.regStatus &= ~HDC.REG_STATUS.INTERRUPT;
|
|
|
|
this.messagePort(port, null, addrFrom, "DATA[" + this.regDataIndex + "]", bIn);
|
|
if (++this.regDataIndex >= this.regDataTotal) {
|
|
this.regDataIndex = this.regDataTotal = 0;
|
|
this.regStatus &= ~(HDC.REG_STATUS.IOMODE | HDC.REG_STATUS.BUS | HDC.REG_STATUS.BUSY);
|
|
}
|
|
return bIn;
|
|
};
|
|
|
|
/**
|
|
* outHDCData(port, bOut, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x320)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
|
|
*/
|
|
HDC.prototype.outHDCData = function(port, bOut, addrFrom) {
|
|
this.messagePort(port, bOut, addrFrom, "DATA[" + this.regDataTotal + "]");
|
|
if (this.regDataTotal < this.regDataArray.length) {
|
|
this.regDataArray[this.regDataTotal++] = bOut;
|
|
}
|
|
var bCmd = this.regDataArray[0];
|
|
var cbCmd = (bCmd != HDC.REG_DATA.CMD.INIT_DRIVE? 6 : this.regDataArray.length);
|
|
if (this.regDataTotal == 6) {
|
|
/*
|
|
* REG_STATUS.REQ must be CLEAR following any 6-byte command sequence that the HDC BIOS "COMMAND" function outputs,
|
|
* yet it must also be SET before the HDC BIOS will proceed with the remaining the 8-byte sequence that's part of
|
|
* HDC.REG_DATA.CMD.INIT_DRIVE command. See inHDCStatus() for HACK details.
|
|
*/
|
|
this.regStatus &= ~HDC.REG_STATUS.REQ;
|
|
}
|
|
if (this.regDataTotal >= cbCmd) {
|
|
/*
|
|
* It's essential that REG_STATUS.IOMODE be set here, at least after the final 8-byte HDC.REG_DATA.CMD.INIT_DRIVE sequence.
|
|
*/
|
|
this.regStatus |= HDC.REG_STATUS.IOMODE;
|
|
this.regStatus &= ~HDC.REG_STATUS.REQ;
|
|
this.doCmd();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inHDCStatus(port, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x321)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
HDC.prototype.inHDCStatus = function(port, addrFrom) {
|
|
var b = this.regStatus;
|
|
this.messagePort(port, null, addrFrom, "STATUS", b);
|
|
/*
|
|
* HACK: The HDC BIOS will not finish the HDC.REG_DATA.CMD.INIT_DRIVE sequence unless it sees REG_STATUS.REQ set again, nor will
|
|
* it read any of the REG_DATA bytes returned from a HDC.REG_DATA.CMD.REQUEST_SENSE command unless REG_STATUS.REQ is set again, so
|
|
* we turn it back on if there are unprocessed data bytes.
|
|
*/
|
|
if (this.regDataIndex < this.regDataTotal) {
|
|
this.regStatus |= HDC.REG_STATUS.REQ;
|
|
}
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outHDCReset(port, bOut, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x321)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
|
|
*/
|
|
HDC.prototype.outHDCReset = function(port, bOut, addrFrom) {
|
|
this.messagePort(port, bOut, addrFrom, "RESET");
|
|
/*
|
|
* Not sure what to do with this value, and the value itself may be "don't care", but we'll save it anyway.
|
|
*/
|
|
this.regReset = bOut;
|
|
if (this.chipset) this.chipset.clearIRR(ChipSet.IRQ.HDC);
|
|
this.initController();
|
|
};
|
|
|
|
/**
|
|
* inHDCConfig(port, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x322)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
HDC.prototype.inHDCConfig = function(port, addrFrom) {
|
|
this.messagePort(port, null, addrFrom, "CONFIG", this.regConfig);
|
|
return this.regConfig;
|
|
};
|
|
|
|
/**
|
|
* outHDCPulse(port, bOut, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x322)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
|
|
*/
|
|
HDC.prototype.outHDCPulse = function(port, bOut, addrFrom) {
|
|
this.messagePort(port, bOut, addrFrom, "PULSE");
|
|
/*
|
|
* Not sure what to do with this value, and the value itself may be "don't care", but we'll save it anyway.
|
|
*/
|
|
this.regPulse = bOut;
|
|
/*
|
|
* The HDC BIOS "COMMAND" function (@ C800:0562) waits for these ALL status bits after writing to both regPulse
|
|
* and regPattern, so we must oblige it.
|
|
*/
|
|
/*
|
|
* TODO: Figure out exactly when either REG_STATUS.BUS or REG_STATUS.BUSY supposed to be cleared.
|
|
* The HDC BIOS doesn't care much about them, except for the one location mentioned above. However, MS-DOS 4.0
|
|
* (aka the unreleased "multitasking" version of MS-DOS) cares, so I'm going to start by clearing them at the
|
|
* same point I clear REG_STATUS.IOMODE.
|
|
*/
|
|
this.regStatus = HDC.REG_STATUS.REQ | HDC.REG_STATUS.BUS | HDC.REG_STATUS.BUSY;
|
|
};
|
|
|
|
/**
|
|
* outHDCPattern(port, bOut, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x323)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
|
|
*/
|
|
HDC.prototype.outHDCPattern = function(port, bOut, addrFrom) {
|
|
this.messagePort(port, bOut, addrFrom, "PATTERN");
|
|
this.regPattern = bOut;
|
|
};
|
|
|
|
/**
|
|
* outHDCNoise(port, bOut, addrFrom)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port (0x327, 0x32B or 0x32F)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to write the specified port)
|
|
*/
|
|
HDC.prototype.outHDCNoise = function(port, bOut, addrFrom) {
|
|
this.messagePort(port, bOut, addrFrom, "NOISE");
|
|
};
|
|
|
|
/**
|
|
* intBIOSDisk(addr)
|
|
*
|
|
* NOTE: This function differentiates HDC requests from FDC requests, based on whether the INT 0x13 drive number
|
|
* in DL is >= 0x80.
|
|
*
|
|
* HACK: The HDC BIOS code for both INT 0x13/AH=0x00 and INT 0x13/AH=0x09 calls "INIT_DRV" @ C800:0427, which is
|
|
* hard-coded to issue the HDC.REG_DATA.CMD.INIT_DRIVE command for BOTH drives 0 and 1 (aka drive numbers 0x80 and
|
|
* 0x81), regardless of the drive number specified in DL; this means that the HDC.REG_DATA.CMD.INIT_DRIVE command
|
|
* must always succeed for drive 1 if it also succeeds for drive 0 -- even if there is no drive 1. Bizarre, but OK,
|
|
* whatever.
|
|
*
|
|
* So assuming we a have drive 0, when the power-on diagnostics in "DISK_SETUP" @ C800:0003 call INT 0x13/AH=0x09
|
|
* @ C800:00DB for drive 0, it must succeed. No problem. But when "DISK_SETUP" starts probing for additional drives,
|
|
* it first issues INT 0x13/AH=0x00, followed by INT 0x13/AH=0x11, and finally INT 0x13/AH=0x09. If the first
|
|
* (AH=0x00) or third (AH=0x09) INT 0x13 fails, it quickly moves on (ie, it jumps to "POD_DONE"). But as we just
|
|
* discussed, both those operations call "INIT_DRV", which can't return an error. This means the only function that
|
|
* can return an error in this context is the recalibrate function (AH=0x11). That sucks, because the way the HDC
|
|
* BIOS is written, it will loop for anywhere from 1.5 seconds to 25 seconds (depending on whether the controller
|
|
* is part of the "System Unit" or not; see port 0x213), attempting to recalibrate drive 1 until it finally times out.
|
|
*
|
|
* Normally, you'll only experience the 1.5 second delay, but even so, it's a ridiculous waste of time and a lot of
|
|
* useless INT 0x13 calls. So I monitor INT 0x13/AH=0x00 for DL >= 0x80 and set a special HDC.REG_DATA.CMD.INIT_DRIVE
|
|
* override flag (iDriveAllowFail) that will allow that command to fail, and in theory, make the the HDC BIOS
|
|
* "DISK_SETUP" code much more efficient.
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} addr
|
|
* @return {boolean} true to proceed with the INT 0x13 software interrupt, false to skip
|
|
*/
|
|
HDC.prototype.intBIOSDisk = function(addr) {
|
|
var AH = this.cpu.regAX >> 8;
|
|
var DL = this.cpu.regDX & 0xff;
|
|
if (!AH && DL > 0x80) this.iDriveAllowFail = DL - 0x80;
|
|
if (DEBUGGER) {
|
|
if (this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_HDC) && DL >= 0x80) {
|
|
this.dbg.message("HDC.intBIOSDisk(AX=" + str.toHexWord(this.cpu.regAX) + ",DL=" + str.toHexByte(DL) + ") at " + str.toHexAddr(addr - this.cpu.segCS.base, this.cpu.segCS.sel));
|
|
// this.cpu.haltCPU();
|
|
this.cpu.addInterruptReturn(addr, function (hdc, nCycles) {
|
|
return function onBIOSDiskReturn(nLevel) {
|
|
nCycles = hdc.cpu.getCycles() - nCycles;
|
|
hdc.messageDebugger("HDC.intBIOSDisk(" + nLevel + "): C=" + (hdc.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")");
|
|
// if (DEBUG && nCycles > 10000) hdc.cpu.haltCPU();
|
|
};
|
|
}(this, this.cpu.getCycles()));
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* intBIOSDiskette(addr)
|
|
*
|
|
* When the HDC BIOS overwrites the ROM BIOS INT 0x13 address, it saves the original INT 0x13 address
|
|
* in the INT 0x40 vector. This function intercepts calls to that vector to work around a minor nuisance.
|
|
*
|
|
* The HDC BIOS's plan was simple, albeit slightly flawed: assign fixed disks drive numbers >= 0x80,
|
|
* and whenever someone calls INT 0x13 with a drive number < 0x80, invoke the original INT 0x13 diskette
|
|
* code via INT 0x40 and return via RET 2.
|
|
*
|
|
* Unfortunately, not all original INT 0x13 functions required a drive number in DL (eg, the "reset"
|
|
* function, where AH=0). And the HDC BIOS knew this, which is why, in the case of the "reset" function,
|
|
* the HDC BIOS performs BOTH an INT 0x40 diskette reset AND an HDC reset -- it can't be sure which
|
|
* controller the caller really wants to reset.
|
|
*
|
|
* An unfortunate side-effect of this behavior: when the HDC BIOS is initialized for the first time, it may
|
|
* issue several resets internally, depending on whether there are 0, 1 or 2 hard disks installed, and each
|
|
* of those resets also triggers completely useless diskette resets, each wasting up to two seconds waiting
|
|
* for the FDC to interrupt. The FDC tries to interrupt, but it can't, because at this early stage of
|
|
* ROM BIOS initialization, IRQ_FDC hasn't been unmasked yet.
|
|
*
|
|
* My work-around: have the HDC component hook INT 0x40, and every time an INT 0x40 is issued with AH=0 and
|
|
* IRQ_FDC masked, bypass the INT 0x40 interrupt. This is as close as PCjs has come to patching any BIOS code
|
|
* (something I refuse to do), and even here, I'm not doing it out of necessity, just annoyance.
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} addr
|
|
* @return {boolean} true to proceed with the INT 0x40 software interrupt, false to skip
|
|
*/
|
|
HDC.prototype.intBIOSDiskette = function(addr) {
|
|
var AH = this.cpu.regAX >> 8;
|
|
if ((!AH && this.chipset && this.chipset.checkIMR(ChipSet.IRQ.FDC))) {
|
|
if (DEBUG) this.messageDebugger("HDC.intBIOSDiskette(): skipping useless INT 0x40 diskette reset");
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* doCmd()
|
|
*
|
|
* @this {HDC}
|
|
*/
|
|
HDC.prototype.doCmd = function() {
|
|
var hdc = this;
|
|
this.regDataIndex = 0;
|
|
|
|
var bCmd = this.popCmd();
|
|
var bCmdOrig = bCmd;
|
|
var b1 = this.popCmd();
|
|
var bDrive = b1 & 0x20;
|
|
var iDrive = (bDrive >> 5);
|
|
|
|
var bHead = b1 & 0x1f;
|
|
var b2 = this.popCmd();
|
|
var b3 = this.popCmd();
|
|
var wCylinder = ((b2 << 2) & 0x300) | b3;
|
|
var bSector = b2 & 0x3f;
|
|
var bCount = this.popCmd(); // block count or interleave count, depending on the command
|
|
var bControl = this.popCmd();
|
|
var bParm, bDataStatus;
|
|
|
|
var drive = this.aDrives[iDrive];
|
|
if (drive) {
|
|
drive.wCylinder = wCylinder;
|
|
drive.bHead = bHead;
|
|
drive.bSector = bSector;
|
|
drive.nBytes = bCount * drive.cbSector;
|
|
}
|
|
|
|
/*
|
|
* I tried to save normal command processing from having to deal with invalid drives,
|
|
* but the HDC BIOS initializes both drive 0 AND drive 1 on a HDC.REG_DATA.CMD.INIT_DRIVE command,
|
|
* and apparently that particular command has no problem with non-existent drives.
|
|
*
|
|
* So I've separated the commands into two groups: drive-ambivalent commands should be
|
|
* processed in the first group, and all the rest should be processed in the second group.
|
|
*/
|
|
switch (bCmd) {
|
|
case HDC.REG_DATA.CMD.REQUEST_SENSE: // 0x03
|
|
this.beginResult(drive? drive.errorCode : HDC.REG_DATA.ERR.NOT_READY);
|
|
this.pushResult(b1);
|
|
this.pushResult(b2);
|
|
this.pushResult(b3);
|
|
/*
|
|
* Although not terribly clear from IBM's "Fixed Disk Adapter" documentation,
|
|
* a data "status byte" also follows the 4 "sense bytes". Interestingly, The HDC BIOS
|
|
* checks that data status byte for REG_DATA.STATUS_ERROR, but I have to wonder if it
|
|
* would have ever been set for this command....
|
|
*
|
|
* The whole point of the HDC.REG_DATA.CMD.REQUEST_SENSE command is to obtain details about a
|
|
* previous error, so if HDC.REG_DATA.CMD.REQUEST_SENSE itself reports an error, what would that mean?
|
|
*/
|
|
this.pushResult(HDC.REG_DATA.STATUS_OK | bDrive);
|
|
bCmd = -1; // mark the command as complete
|
|
break;
|
|
case HDC.REG_DATA.CMD.INIT_DRIVE: // 0x0C
|
|
/*
|
|
* Pop off all the extra "Initialize Drive Characteristics" bytes and store them,
|
|
* for the benefit of other functions, like verifyDrive().
|
|
*/
|
|
var i = 0;
|
|
while ((bParm = this.popCmd()) >= 0) {
|
|
if (drive && i < drive.abDriveParms.length) {
|
|
drive.abDriveParms[i++] = bParm;
|
|
}
|
|
}
|
|
if (drive) this.verifyDrive(drive);
|
|
bDataStatus = HDC.REG_DATA.STATUS_OK;
|
|
if (!drive && this.iDriveAllowFail == iDrive) {
|
|
this.iDriveAllowFail = -1;
|
|
if (DEBUG) this.messageDebugger("HDC.doCmd(): fake failure triggered");
|
|
bDataStatus = HDC.REG_DATA.STATUS_ERROR;
|
|
}
|
|
this.beginResult(bDataStatus | bDrive);
|
|
bCmd = -1; // mark the command as complete
|
|
break;
|
|
case HDC.REG_DATA.CMD.RAM_DIAGNOSTIC: // 0xE0
|
|
case HDC.REG_DATA.CMD.CTL_DIAGNOSTIC: // 0xE4
|
|
this.beginResult(HDC.REG_DATA.STATUS_OK | bDrive);
|
|
bCmd = -1; // mark the command as complete
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (bCmd >= 0) {
|
|
if (drive === undefined) {
|
|
bCmd = -1;
|
|
} else {
|
|
/*
|
|
* In preparation for this command, zero out the drive's errorCode and senseCode.
|
|
* Commands that require a disk address should update senseCode with HDC.REG_DATA.SENSE_ADDR_VALID.
|
|
* And of course, any command that encounters an error should set the appropriate error code.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
drive.senseCode = 0;
|
|
}
|
|
switch (bCmd) {
|
|
case HDC.REG_DATA.CMD.TEST_READY: // 0x00
|
|
this.beginResult(HDC.REG_DATA.STATUS_OK | bDrive);
|
|
break;
|
|
case HDC.REG_DATA.CMD.RECALIBRATE: // 0x01
|
|
drive.bControl = bControl;
|
|
if (DEBUG) this.messageDebugger("HDC.doCmd(): drive " + iDrive + " control byte: 0x" + str.toHexByte(bControl));
|
|
this.beginResult(HDC.REG_DATA.STATUS_OK | bDrive);
|
|
break;
|
|
case HDC.REG_DATA.CMD.READ_VERIFY: // 0x05
|
|
/*
|
|
* This is a non-DMA operation, so we simply pretend everything is OK for now; TODO: Revisit.
|
|
*/
|
|
this.beginResult(HDC.REG_DATA.STATUS_OK | bDrive);
|
|
break;
|
|
case HDC.REG_DATA.CMD.READ_DATA: // 0x08
|
|
this.doRead(drive, function(bStatus) {
|
|
hdc.beginResult(bStatus | bDrive);
|
|
});
|
|
break;
|
|
case HDC.REG_DATA.CMD.WRITE_DATA: // 0x0A
|
|
/*
|
|
* QUESTION: The IBM TechRef (p1-188) implies that bCount is used as part of HDC.REG_DATA.CMD.WRITE_DATA command,
|
|
* but it is omitted from the HDC.REG_DATA.CMD.READ_DATA command. Is that correct? Note that, as far as the length
|
|
* of the transfer is concerned, we rely exclusively on the DMA controller being programmed with the
|
|
* appropriate byte count.
|
|
*/
|
|
this.doWrite(drive, function(bStatus) {
|
|
hdc.beginResult(bStatus | bDrive);
|
|
});
|
|
break;
|
|
case HDC.REG_DATA.CMD.WRITE_BUFFER: // 0x0F
|
|
this.doWriteToBuffer(drive, function(bStatus) {
|
|
hdc.beginResult(bStatus | bDrive);
|
|
});
|
|
break;
|
|
default:
|
|
if (DEBUG) this.messageDebugger((bCmd < 0? "HDC.doCmd(): invalid drive" : "unsupported operation") + " (command=0x" + str.toHexByte(bCmdOrig) + ",drive=" + iDrive + ")");
|
|
this.beginResult(HDC.REG_DATA.STATUS_ERROR | bDrive);
|
|
if (DEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_HDC) && bCmd >= 0) this.cpu.haltCPU();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* popCmd()
|
|
*
|
|
* @this {HDC}
|
|
* @return {number}
|
|
*/
|
|
HDC.prototype.popCmd = function() {
|
|
var bCmd = -1;
|
|
var bCmdIndex = this.regDataIndex;
|
|
if (bCmdIndex < this.regDataTotal) {
|
|
bCmd = this.regDataArray[this.regDataIndex++];
|
|
if (DEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled((bCmdIndex > 0? this.dbg.MESSAGE_PORT : 0) | this.dbg.MESSAGE_HDC)) {
|
|
this.dbg.message("HDC.CMD[" + bCmdIndex + "]: 0x" + str.toHexByte(bCmd) + (!bCmdIndex && HDC.aCmdNames[bCmd]? (" (" + HDC.aCmdNames[bCmd] + ")") : ""));
|
|
}
|
|
}
|
|
return bCmd;
|
|
};
|
|
|
|
/**
|
|
* beginResult(bResult)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} [bResult]
|
|
*/
|
|
HDC.prototype.beginResult = function(bResult) {
|
|
this.regDataIndex = this.regDataTotal = 0;
|
|
|
|
if (bResult !== undefined) {
|
|
if (DEBUG) this.messageDebugger("HDC.beginResult(0x" + str.toHexByte(bResult) + ")");
|
|
this.pushResult(bResult);
|
|
}
|
|
/*
|
|
* 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 data "status byte"
|
|
* has been read from REG_DATA, the interrupt is cleared (see inHDCData).
|
|
*/
|
|
if (this.chipset) this.chipset.setIRR(ChipSet.IRQ.HDC);
|
|
this.regStatus |= HDC.REG_STATUS.INTERRUPT;
|
|
};
|
|
|
|
/**
|
|
* pushResult(bResult)
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} bResult
|
|
*/
|
|
HDC.prototype.pushResult = function(bResult) {
|
|
if (DEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled((this.regDataTotal > 0? this.dbg.MESSAGE_PORT : 0) | this.dbg.MESSAGE_HDC)) this.dbg.message("HDC.RES[" + this.regDataTotal + "]: 0x" + str.toHexByte(bResult));
|
|
this.regDataArray[this.regDataTotal++] = bResult;
|
|
};
|
|
|
|
/**
|
|
* dmaRead(drive, b, done)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @param {function(number,boolean)} done
|
|
*/
|
|
HDC.prototype.dmaRead = function(drive, b, done) {
|
|
if (b === undefined || b < 0) {
|
|
this.readByte(drive, done);
|
|
return;
|
|
}
|
|
/*
|
|
* The DMA controller should be ASKING for data, not GIVING us data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.messageDebugger("dmaRead(): invalid DMA acknowledgement");
|
|
done(-1, false);
|
|
};
|
|
|
|
/**
|
|
* dmaWrite(drive, b)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @return {number}
|
|
*/
|
|
HDC.prototype.dmaWrite = function(drive, b) {
|
|
if (b !== undefined && b >= 0)
|
|
return this.writeByte(drive, b);
|
|
/*
|
|
* The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.messageDebugger("dmaWrite(): invalid DMA acknowledgement");
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* dmaWriteBuffer(drive, b)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @return {number}
|
|
*/
|
|
HDC.prototype.dmaWriteBuffer = function(drive, b) {
|
|
if (b !== undefined && b >= 0)
|
|
return this.writeBuffer(drive, b);
|
|
/*
|
|
* The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.messageDebugger("dmaWriteBuffer(): invalid DMA acknowledgement");
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* dmaWriteFormat(drive, b)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b
|
|
* @returns {number}
|
|
*/
|
|
HDC.prototype.dmaWriteFormat = function(drive, b) {
|
|
if (b !== undefined && b >= 0)
|
|
return this.writeFormat(drive, b);
|
|
/*
|
|
* The DMA controller should be GIVING us data, not ASKING for data; this suggests an internal DMA miscommunication
|
|
*/
|
|
if (DEBUG) this.messageDebugger("dmaWritedFormat(): invalid DMA acknowledgement");
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* doRead(drive, done)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {function(number)} done (dataStatus is REG_DATA.STATUS_OK or REG_DATA.STATUS_ERROR; if error, then drive.errorCode should be set as well)
|
|
*/
|
|
HDC.prototype.doRead = function(drive, done) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
|
|
if (DEBUG) this.messageDebugger("HDC.doRead(" + drive.wCylinder + ":" + drive.bHead + ":" + drive.bSector + ")");
|
|
|
|
// if (DEBUG) this.messageDebugger("HDC.doRead(head=" + str.toHexByte(drive.bHead) + ",cyl=" + str.toHexWord(drive.wCylinder) + ",sec=" + str.toHexByte(drive.bSector) + ")");
|
|
|
|
if (drive.disk) {
|
|
drive.sector = null;
|
|
if (this.chipset) {
|
|
/*
|
|
* We need to reverse the original logic, and default to success unless/until an actual error occurs;
|
|
* otherwise dmaRead()/readByte() will bail on us. The original approach used to work because requestDMA()
|
|
* would immediately call us back with fComplete set to true EVEN if the DMA channel was not yet unmasked;
|
|
* now the callback is deferred until the DMA channel has been unmasked and the DMA request has finished.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
this.chipset.connectDMA(ChipSet.DMA_HDC, this, 'dmaRead', drive);
|
|
this.chipset.requestDMA(ChipSet.DMA_HDC, function(fComplete) {
|
|
if (!fComplete) {
|
|
/*
|
|
* If an incomplete request wasn't triggered by an explicit error, then let's make explicit (ie,
|
|
* revert to the default failure code that we originally set above).
|
|
*/
|
|
if (drive.errorCode == HDC.REG_DATA.ERR.NONE) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
};
|
|
|
|
/**
|
|
* doWrite(drive, done)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {function(number)} done (dataStatus is REG_DATA.STATUS_OK or REG_DATA.STATUS_ERROR; if error, then drive.errorCode should be set as well)
|
|
*/
|
|
HDC.prototype.doWrite = function(drive, done) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
|
|
if (DEBUG) this.messageDebugger("HDC.doWrite(" + drive.wCylinder + ":" + drive.bHead + ":" + drive.bSector + ")");
|
|
|
|
// if (DEBUG) this.messageDebugger("HDC.doWrite(head=" + str.toHexByte(drive.bHead) + ",cyl=" + str.toHexWord(drive.wCylinder) + ",sec=" + str.toHexByte(drive.bSector) + ")");
|
|
|
|
if (drive.disk) {
|
|
drive.sector = null;
|
|
if (this.chipset) {
|
|
/*
|
|
* We need to reverse the original logic, and default to success unless/until an actual error occurs;
|
|
* otherwise dmaWrite()/writeByte() will bail on us. The original approach would work because requestDMA()
|
|
* would immediately call us back with fComplete set to true EVEN if the DMA channel was not yet unmasked;
|
|
* now the callback is deferred until the DMA channel has been unmasked and the DMA request has finished.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
this.chipset.connectDMA(ChipSet.DMA_HDC, this, 'dmaWrite', drive);
|
|
this.chipset.requestDMA(ChipSet.DMA_HDC, function(fComplete) {
|
|
if (!fComplete) {
|
|
/*
|
|
* If an incomplete request wasn't triggered by an explicit error, then let's make explicit (ie,
|
|
* revert to the default failure code that we originally set above).
|
|
*/
|
|
if (drive.errorCode == HDC.REG_DATA.ERR.NONE) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
}
|
|
/*
|
|
* Mask any error that's the result of an attempt to write beyond the end of the track (which is
|
|
* something the MS-DOS 4.0M's FORMAT utility seems to like to do).
|
|
*/
|
|
if (drive.errorCode == HDC.REG_DATA.ERR.NO_SECTOR) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
};
|
|
|
|
/**
|
|
* doWriteToBuffer(drive, done)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {function(number)} done (dataStatus is REG_DATA.STATUS_OK or REG_DATA.STATUS_ERROR; if error, then drive.errorCode should be set as well)
|
|
*/
|
|
HDC.prototype.doWriteToBuffer = function(drive, done) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
|
|
if (DEBUG) this.messageDebugger("HDC.doWriteToBuffer()");
|
|
|
|
if (!drive.abSectorBuffer || drive.abSectorBuffer.length != drive.nBytes) {
|
|
drive.abSectorBuffer = new Array(drive.nBytes);
|
|
}
|
|
drive.ibSector = 0;
|
|
if (this.chipset) {
|
|
/*
|
|
* We need to reverse the original logic, and default to success unless/until an actual error occurs;
|
|
* otherwise dmaWriteBuffer() will bail on us. The original approach would work because requestDMA()
|
|
* would immediately call us back with fComplete set to true EVEN if the DMA channel was not yet unmasked;
|
|
* now the callback is deferred until the DMA channel has been unmasked and the DMA request has finished.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
this.chipset.connectDMA(ChipSet.DMA_HDC, this, 'dmaWriteBuffer', drive);
|
|
this.chipset.requestDMA(ChipSet.DMA_HDC, function(fComplete) {
|
|
if (!fComplete) {
|
|
/*
|
|
* If an incomplete request wasn't triggered by an explicit error, then let's make explicit (ie,
|
|
* revert to the default failure code that we originally set above).
|
|
*/
|
|
if (drive.errorCode == HDC.REG_DATA.ERR.NONE) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
});
|
|
return;
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
};
|
|
|
|
/**
|
|
* doFormat(drive, done)
|
|
*
|
|
* The drive variable 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 {HDC}
|
|
* @param {Object} drive
|
|
* @param {function(number)} done (dataStatus is REG_DATA.STATUS_OK or REG_DATA.STATUS_ERROR; if error, then drive.errorCode should be set as well)
|
|
*/
|
|
HDC.prototype.doFormat = function(drive, done) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
|
|
// if (DEBUG) this.messageDebugger("HDC.doFormat()");
|
|
|
|
if (drive.disk) {
|
|
drive.sector = null;
|
|
if (this.chipset) {
|
|
drive.cbFormat = 0;
|
|
drive.abFormat = new Array(4);
|
|
drive.bFormatting = true;
|
|
drive.cSectorsFormatted = 0;
|
|
/*
|
|
* We need to reverse the original logic, and default to success unless/until an actual error occurs;
|
|
* otherwise dmaWriteFormat() will bail on us. The original approach would work because requestDMA()
|
|
* would immediately call us back with fComplete set to true EVEN if the DMA channel was not yet unmasked;
|
|
* now the callback is deferred until the DMA channel has been unmasked and the DMA request has finished.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NONE;
|
|
this.chipset.connectDMA(ChipSet.DMA_HDC, this, 'dmaWriteFormat', drive);
|
|
this.chipset.requestDMA(ChipSet.DMA_HDC, function(fComplete) {
|
|
if (!fComplete) {
|
|
/*
|
|
* If an incomplete request wasn't triggered by an explicit error, then let's make explicit (ie,
|
|
* revert to the default failure code that we originally set above).
|
|
*/
|
|
if (drive.errorCode == HDC.REG_DATA.ERR.NONE) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NOT_READY;
|
|
}
|
|
}
|
|
drive.bFormatting = false;
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
done(drive.errorCode? HDC.REG_DATA.STATUS_ERROR : HDC.REG_DATA.STATUS_OK);
|
|
};
|
|
|
|
/**
|
|
* readByte(drive, done)
|
|
*
|
|
* The following drive variable properties must have been setup prior to our first call:
|
|
*
|
|
* drive.wCylinder
|
|
* drive.bHead
|
|
* drive.bSector
|
|
* drive.sector (initialized to null)
|
|
*
|
|
* On the first readByte() request, since drive.sector will be null, we ask the Disk object to look
|
|
* up the first sector of the request. We then ask the Disk for bytes from that sector until the sector
|
|
* is exhausted, and then we look up the next sector and continue the process.
|
|
*
|
|
* NOTE: Since the HDC isn't aware of the extent of the transfer, all readByte() can do is return bytes
|
|
* until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted.
|
|
*
|
|
* TODO: Research the requirements, if any, for multi-track I/O and determine what if anything needs to be
|
|
* done. At the very least, if it must be supported, there would need to be some head-incrementing somewhere.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {function(number,boolean)} done (number is next available byte from drive, or -1 if no more bytes available)
|
|
*/
|
|
HDC.prototype.readByte = function(drive, done) {
|
|
var b = -1;
|
|
if (drive.errorCode) {
|
|
done(b, false);
|
|
return;
|
|
}
|
|
if (drive.sector) {
|
|
b = drive.disk.read(drive.sector, drive.ibSector++);
|
|
if (b >= 0) {
|
|
done(b, false);
|
|
return;
|
|
}
|
|
}
|
|
/*
|
|
* Locate the next sector, and then try reading again.
|
|
*
|
|
* Important difference between the FDC and the HDC: the HDC uses 0-based sector numbers,
|
|
* hence the "+1" below. I could change how sector numbers are stored in the image, but it
|
|
* seems preferable to keep the image format consistent and controller-independent.
|
|
*/
|
|
drive.disk.seek(drive.wCylinder, drive.bHead, drive.bSector + 1, false, function(sector, fAsync) {
|
|
var b = -1;
|
|
if ((drive.sector = sector)) {
|
|
drive.ibSector = 0;
|
|
drive.bSector++;
|
|
b = drive.disk.read(drive.sector, drive.ibSector++);
|
|
} else {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NO_SECTOR;
|
|
}
|
|
done(b, fAsync);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* writeByte(drive, b)
|
|
*
|
|
* The following drive variable properties must have been setup prior to our first call:
|
|
*
|
|
* drive.wCylinder
|
|
* drive.bHead
|
|
* drive.bSector
|
|
* drive.sector (initialized to null)
|
|
*
|
|
* On the first writeByte() request, since drive.sector will be null, we ask the Disk object to look
|
|
* up the first sector of the request. We then send the Disk bytes for that sector until the sector
|
|
* is full, and then we look up the next sector and continue the process.
|
|
*
|
|
* NOTE: Since the HDC isn't aware of the extent of the transfer, all writeByte() can do is accept bytes
|
|
* until the current track (or, in the case of a multi-track request, the current cylinder) has been exhausted.
|
|
*
|
|
* TODO: Research the requirements, if any, for multi-track I/O and determine what if anything needs to be
|
|
* done. At the very least, if it must be supported, there would need to be some head-incrementing somewhere.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b containing next byte to write
|
|
* @return {number} (b unchanged; return -1 if command should be terminated)
|
|
*/
|
|
HDC.prototype.writeByte = function(drive, b) {
|
|
if (drive.errorCode) 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.
|
|
*
|
|
* Important difference between the FDC and the HDC: the HDC uses 0-based sector numbers,
|
|
* hence the "+1" below. I could change how sector numbers are stored in the image, but it
|
|
* seems preferable to keep the image format consistent and controller-independent.
|
|
*/
|
|
drive.disk.seek(drive.wCylinder, drive.bHead, drive.bSector + 1, true, function(sector, fAsync) {
|
|
drive.sector = sector;
|
|
});
|
|
if (!drive.sector) {
|
|
drive.errorCode = HDC.REG_DATA.ERR.NO_SECTOR;
|
|
b = -1;
|
|
break;
|
|
}
|
|
drive.ibSector = 0;
|
|
drive.bSector++;
|
|
} while (true);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* writeBuffer(drive, b)
|
|
*
|
|
* NOTE: Since the HDC isn't aware of the extent of the transfer, all writeBuffer() can do is accept bytes
|
|
* until the buffer is full.
|
|
*
|
|
* TODO: Support for HDC.REG_DATA.CMD.READ_BUFFER is missing, and support for HDC.REG_DATA.CMD.WRITE_BUFFER may not be complete;
|
|
* tests required.
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b containing next byte to write
|
|
* @return {number} (b unchanged; return -1 if command should be terminated)
|
|
*/
|
|
HDC.prototype.writeBuffer = function(drive, b) {
|
|
if (drive.ibSector < drive.abSectorBuffer.length) {
|
|
drive.abSectorBuffer[drive.ibSector++] = b;
|
|
} else {
|
|
/*
|
|
* TODO: Determine the proper error code to return here.
|
|
*/
|
|
drive.errorCode = HDC.REG_DATA.ERR.NO_SECTOR;
|
|
b = -1;
|
|
}
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* writeFormat(drive, b)
|
|
*
|
|
* @this {HDC}
|
|
* @param {Object} drive
|
|
* @param {number} b containing a format command byte
|
|
* @return {number} (b if successful, -1 if command should be terminated)
|
|
*/
|
|
HDC.prototype.writeFormat = function(drive, b) {
|
|
if (drive.errorCode) return -1;
|
|
drive.abFormat[drive.cbFormat++] = b;
|
|
if (drive.cbFormat == drive.abFormat.length) {
|
|
drive.wCylinder = drive.abFormat[0]; // C
|
|
drive.bHead = drive.abFormat[1]; // H
|
|
drive.bSector = drive.abFormat[2]; // R
|
|
drive.nBytes = 128 << drive.abFormat[3];// N (0 => 128, 1 => 256, 2 => 512, 3 => 1024)
|
|
drive.cbFormat = 0;
|
|
|
|
if (DEBUG) this.messageDebugger("HDC.writeFormat(" + drive.wCylinder + ":" + drive.bHead + ":" + drive.bSector + ":" + drive.nBytes + ")");
|
|
|
|
// if (DEBUG) this.messageDebugger("HDC.writeFormat(head=" + str.toHexByte(drive.bHead) + ",cyl=" + str.toHexWord(drive.wCylinder) + ",sec=" + str.toHexByte(drive.bSector) + ",len=" + str.toHexWord(drive.nBytes) + ")");
|
|
|
|
for (var i = 0; i < drive.nBytes; i++) {
|
|
if (this.writeByte(drive, drive.bFiller) < 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
drive.cSectorsFormatted++;
|
|
}
|
|
if (drive.cSectorsFormatted >= drive.bSectorEnd) b = -1;
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* messageDebugger(sMessage)
|
|
*
|
|
* This is a combination of the Debugger's messageEnabled(MESSAGE_HDC) and message() functions, for convenience.
|
|
*
|
|
* @this {HDC}
|
|
* @param {string} sMessage is any caller-defined message string
|
|
*/
|
|
HDC.prototype.messageDebugger = function(sMessage) {
|
|
if (DEBUGGER && this.dbg) {
|
|
if (this.dbg.messageEnabled(this.dbg.MESSAGE_HDC)) {
|
|
this.dbg.message(sMessage);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* messagePort(port, bOut, addrFrom, name, bIn)
|
|
*
|
|
* This is an internal version of the Debugger's messagePort() function, for convenience.
|
|
*
|
|
* @this {HDC}
|
|
* @param {number} port
|
|
* @param {number|null} bOut if an output operation
|
|
* @param {number|null} [addrFrom]
|
|
* @param {string|null} [name] of the port, if any
|
|
* @param {number} [bIn] is the input value, if known, on an input operation
|
|
*/
|
|
HDC.prototype.messagePort = function(port, bOut, addrFrom, name, bIn) {
|
|
if (DEBUGGER && this.dbg) {
|
|
this.dbg.messagePort(this, port, bOut, addrFrom, name, this.dbg.MESSAGE_HDC, bIn);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Port input notification table
|
|
*/
|
|
HDC.aPortInput = {
|
|
0x320: HDC.prototype.inHDCData,
|
|
0x321: HDC.prototype.inHDCStatus,
|
|
0x322: HDC.prototype.inHDCConfig
|
|
};
|
|
|
|
/*
|
|
* Port output notification table
|
|
*/
|
|
HDC.aPortOutput = {
|
|
0x320: HDC.prototype.outHDCData,
|
|
0x321: HDC.prototype.outHDCReset,
|
|
0x322: HDC.prototype.outHDCPulse,
|
|
0x323: HDC.prototype.outHDCPattern,
|
|
/*
|
|
* The PC XT Fixed Disk BIOS includes some additional "housekeeping" that it performs
|
|
* not only on port 0x323 but also on three additional ports at increments of 4 (see all
|
|
* references to "RESET INT/DMA MASK" in the Fixed Disk BIOS). It's not clear to me if
|
|
* those ports refer to additional HDC controllers, and I haven't seen other references to
|
|
* them, but in any case, they represent a lot of "I/O noise" that we simply squelch here.
|
|
*/
|
|
0x327: HDC.prototype.outHDCNoise,
|
|
0x32B: HDC.prototype.outHDCNoise,
|
|
0x32F: HDC.prototype.outHDCNoise
|
|
};
|
|
|
|
/**
|
|
* HDC.init()
|
|
*
|
|
* This function operates on every element (e) of class "hdc", and initializes
|
|
* all the necessary HTML to construct the HDC module(s) as spec'ed.
|
|
*
|
|
* Note that each element (e) of class "hdc" is expected to have a "data-value"
|
|
* attribute containing the same JSON-encoded parameters that the HDC constructor expects.
|
|
*/
|
|
HDC.init = function() {
|
|
var aeHDC = Component.getElementsByClass(window.document, PCJSCLASS, "hdc");
|
|
for (var iHDC = 0; iHDC < aeHDC.length; iHDC++) {
|
|
var eHDC = aeHDC[iHDC];
|
|
var parmsHDC = Component.getComponentParms(eHDC);
|
|
var hdc = new HDC(parmsHDC);
|
|
Component.bindComponentControls(hdc, eHDC, PCJSCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Hard Drive Controller (HDC) module on the page.
|
|
*/
|
|
web.onInit(HDC.init);
|
|
|
|
if (typeof APP_PCJS !== 'undefined') APP_PCJS.HDC = HDC;
|
|
|
|
if (typeof module !== 'undefined') module.exports = HDC;
|