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