From 4ff357669d83c57f74ecddaab9a37c9f12e59ad5 Mon Sep 17 00:00:00 2001 From: Jeff Parsons Date: Sun, 5 Oct 2014 18:26:29 -0700 Subject: [PATCH] Fixed CMOS date initialization (Model 5170 successfully gets current date/time now) --- my_modules/pcjs-client/lib/chipset.js | 191 ++++++++++++++++++-------- my_modules/pcjs-client/lib/fdc.js | 90 ++++++------ my_modules/pcjs-client/lib/hdc.js | 168 +++++++++++----------- 3 files changed, 255 insertions(+), 194 deletions(-) diff --git a/my_modules/pcjs-client/lib/chipset.js b/my_modules/pcjs-client/lib/chipset.js index 283b9dcae..e50a93b52 100644 --- a/my_modules/pcjs-client/lib/chipset.js +++ b/my_modules/pcjs-client/lib/chipset.js @@ -72,7 +72,7 @@ if (typeof module !== 'undefined') { * SW2[1-4] (bits 3-0) "NNNNxxxx": number of 32Kb blocks of I/O expansion RAM present * * TODO: There are cryptic references to SW2[5] in the original (5150) TechRef, and apparently the 8255A PPI can - * be programmed to return it (which we support), but its purpose is unclear to me (see PPI_B.ENABLE_SW2). + * be programmed to return it (which we support), but its purpose remains unclear to me (see PPI_B.ENABLE_SW2). * * For example, sw1="01110011" indicates that all SW1 DIP switches are ON, except for SW1[1], SW1[5] and SW1[6], * which are OFF. Internally, the order of these bits must reversed (to 11001110) and then inverted (to 00110001) @@ -150,7 +150,7 @@ function ChipSet(parmsChipSet) this.model = (this.model !== undefined? parseInt(this.model, 10) : ChipSet.MODEL_5150); /* - * Given that the ROM BIOS is hard-coded to load boot sectors @0000:7C00, the minimum amount of system RAM + * Given that the ROM BIOS is hard-coded to load boot sectors @ 0000:7C00, the minimum amount of system RAM * required to boot is therefore 32Kb. Whether that's actually enough to run any or all versions of PC-DOS is * a separate question. FYI, with only 16Kb, the ROM BIOS will still try to boot, and fail miserably. */ @@ -178,6 +178,9 @@ function ChipSet(parmsChipSet) this.cDMACs = this.cPICs = 1; if (this.model >= ChipSet.MODEL_5170) { this.cDMACs = this.cPICs = 2; + /* + * Initialize minimal support for the MODEL_5170's HFCOMBO card (see ChipSet.HFCOMBO below for details) + */ this.regsHFCombo = { bCtrl: 0x00, // port 0x1F4 bStatus: 0x7F // port 0x1F7 @@ -657,7 +660,7 @@ ChipSet.KBC.STATUS = { // this.b8042Status (on read from port 0x64) * The ADDR port also controls NMI: write an address with bit 7 clear to enable NMI or set to disable NMI. */ ChipSet.CMOS = {}; -ChipSet.CMOS.ADDR = {}; // this.bCMOSAddr +ChipSet.CMOS.ADDR = {}; // this.bCMOSAddr ChipSet.CMOS.ADDR.PORT = 0x70; ChipSet.CMOS.ADDR.RTC_SEC = 0x00; ChipSet.CMOS.ADDR.RTC_SEC_ALRM = 0x01; @@ -732,8 +735,8 @@ ChipSet.CMOS.DIAG.RESERVED = 0x03; ChipSet.FDRIVE = { // abCMOSData[ChipSet.CMOS.ADDR.FDRIVE] values (drive 0 value in high nibble, drive 1 value in low nibble) NONE: 0, // no drive - DSDD: 1, // double-sided double-density drive (48 TPI, 40-track, 360Kb max) - DSHC: 2 // double-sided high-capacity drive (96 TPI, 80-track, 1.2Mb max) + DSDD: 1, // double-sided double-density drive (48 TPI, 40 tracks, 360Kb max) + DSHD: 2 // double-sided high-density drive (96 TPI, 80 tracks, 1.2Mb max) }; /* @@ -803,31 +806,38 @@ ChipSet.COPROC.PORT_CLEAR = 0xF0; // clear the coprocessor's "busy" state ChipSet.COPROC.PORT_RESET = 0xF1; // reset the coprocessor /* - * Ports used by MODEL_5170 BIOS for "Combo Hard File/Diskette Card" check (@F000:144C) + * Ports used by MODEL_5170 BIOS for "Combo Hard File/Diskette Card" check (@ F000:144C) * * The ChipSet component provides minimal boot-time support for the "IBM Personal Computer * AT Fixed Disk and Diskette Drive Adapter", aka the HFCOMBO card, until we're able to fork * the HDC component into a new HDCombo component to deal with the "Fixed Disk" portion * of the HFCOMBO card. Fortunately, the "Diskette Drive Adapter" portion of the card is - * quite compatible with the existing FDC component, so that component can be used as-is, - * with minor tweaks. + * extremely compatible with the existing FDC component, so that component can be used as-is, + * with only minor tweaks. * * Initially, we intercepted reads for HFCOMBO's STATUS port simply to reduce boot time; - * otherwise, our default "unknown port" response of 0xFF would maximize boot delay. To solve - * that, the STATUS port simply needs to return a byte with bit 7 clear, so that the BIOS - * will then attempt to write/read the CTRL port. + * otherwise, the default "unknown port" response of 0xFF would lengthen boot time. To solve + * that, the STATUS port simply needed to return a byte with bit 7 clear, so that the BIOS + * would then attempt to write/read the CTRL port. * * Next, we initially treated the HFCOMBO's CTRL port as an "unknown port", because again, * we didn't need HDC support and it didn't seem to affect FDC support. But it turns out * that FDC support IS affected, because if the BIOS doesn't set the "DUAL" bit (bit 0) of the * "HFCNTRL" byte at 40:8F, then when it comes time later to report the diskette drive type, - * the "DISK_TYPE" function (@F000:273D) will branch to one of two almost-identical blocks of - * code -- specifically, the block that disallows diskette drive types >= 2 instead of >= 3. + * the "DISK_TYPE" function (@ F000:273D) will branch to one of two almost-identical blocks of + * code -- specifically, the block that disallows diskette drive types >= 2 (ChipSet.FDRIVE.DSDD) + * instead of >= 3 (ChipSet.FDRIVE.DSHD). */ ChipSet.HFCOMBO = {}; ChipSet.HFCOMBO.CTRL = {PORT: 0x1F4}; ChipSet.HFCOMBO.STATUS = {PORT: 0x1F7}; +/* + * ChipSet-related BIOS interrupts, functions, and other parameters + */ +ChipSet.BIOS = {}; +ChipSet.BIOS.RTC = 0x1A; + /** * @this {ChipSet} * @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output") @@ -884,16 +894,11 @@ ChipSet.prototype.initBus = function(cmp, bus, cpu, dbg) this.dbg = dbg; this.cmp = cmp; this.kbd = cmp.getComponentByType("Keyboard"); - if (DEBUGGER && dbg) { - var chipset = this; - dbg.messageInit(ChipSet); - dbg.messageDump(ChipSet.MESSAGE_PIC, function onDumpPIC() { - chipset.dumpPIC(); - }); - dbg.messageDump(ChipSet.MESSAGE_TIMER, function onDumpTimer() { - chipset.dumpTimer(); - }); - } + /* + * This divisor is invariant, so we calculate it as soon as we're able to query the CPU's base speed. + */ + this.nTicksDivisor = Math.round(cpu.getCyclesPerSecond() / ChipSet.TIMER_TICKS_PER_SEC); + bus.addPortInputTable(this, ChipSet.aPortInput); bus.addPortOutputTable(this, ChipSet.aPortOutput); if (this.model < ChipSet.MODEL_5170) { @@ -903,10 +908,25 @@ ChipSet.prototype.initBus = function(cmp, bus, cpu, dbg) bus.addPortInputTable(this, ChipSet.aPortInput5170); bus.addPortOutputTable(this, ChipSet.aPortOutput5170); } - /* - * This divisor is invariant, so we calculate it as soon as we're able to query the CPU's base speed. - */ - this.nTicksDivisor = Math.round(cpu.getCyclesPerSecond() / ChipSet.TIMER_TICKS_PER_SEC); + if (DEBUGGER) { + if (dbg) { + var chipset = this; + dbg.messageInit(ChipSet); + dbg.messageDump(ChipSet.MESSAGE_PIC, function onDumpPIC() + { + chipset.dumpPIC(); + }); + dbg.messageDump(ChipSet.MESSAGE_TIMER, function onDumpTimer() + { + chipset.dumpTimer(); + }); + dbg.messageDump(ChipSet.MESSAGE_CHIPSET, function onDumpCMOS() + { + chipset.dumpCMOS(); + }); + } + cpu.addInterruptNotify(ChipSet.BIOS.RTC, this, this.intBIOSRTC); + } }; /** @@ -1075,8 +1095,10 @@ ChipSet.prototype.initRTCDate = function(sDate) this.abCMOSData[ChipSet.CMOS.ADDR.RTC_WEEK_DAY] = date.getDay() + 1; this.abCMOSData[ChipSet.CMOS.ADDR.RTC_MONTH_DAY] = date.getDate(); this.abCMOSData[ChipSet.CMOS.ADDR.RTC_MONTH] = date.getMonth() + 1; - this.abCMOSData[ChipSet.CMOS.ADDR.RTC_YEAR] = date.getFullYear() % 100; - + var nYear = date.getFullYear(); + this.abCMOSData[ChipSet.CMOS.ADDR.RTC_YEAR] = nYear % 100; + var nCentury = (nYear / 100); + this.abCMOSData[ChipSet.CMOS.ADDR.CENTURY_DATE] = (nCentury % 10) | ((nCentury / 10) << 4); this.nCyclesCMOSLastUpdate = -1; this.abCMOSData[ChipSet.CMOS.ADDR.RTC_STATUSA] = 0x26; // hard-coded default; refer to ChipSet.CMOS.STATUSA.DV and ChipSet.CMOS.STATUSA.RS @@ -1652,7 +1674,7 @@ ChipSet.prototype.getSWFloppyDrives = function(fInit) * * @this {ChipSet} * @param {number} iDrive (0-based) - * @return {number} one of the ChipSet.FDRIVE values (ie, NONE: 0, DSDD: 1, DSHC: 2) + * @return {number} one of the ChipSet.FDRIVE values (ie, NONE: 0, DSDD: 1, DSHD: 2) */ ChipSet.prototype.getSWFloppyDriveType = function(iDrive) { @@ -1660,7 +1682,7 @@ ChipSet.prototype.getSWFloppyDriveType = function(iDrive) * TODO: For MODEL_5170, we default all floppy drive types to High Capacity, but more control would be nice. */ if (iDrive < this.getSWFloppyDrives()) { - return (this.model < ChipSet.MODEL_5170? ChipSet.FDRIVE.DSDD : ChipSet.FDRIVE.DSHC); + return (this.model < ChipSet.MODEL_5170? ChipSet.FDRIVE.DSDD : ChipSet.FDRIVE.DSHD); } return ChipSet.FDRIVE.NONE; }; @@ -1849,6 +1871,27 @@ ChipSet.prototype.dumpTimer = function() } }; +/** + * dumpCMOS() + * + * @this {ChipSet} + */ +ChipSet.prototype.dumpCMOS = function() +{ + if (DEBUGGER) { + var sDump = ""; + for (var p in ChipSet.CMOS.ADDR) { + var iCMOS = ChipSet.CMOS.ADDR[p]; + if (iCMOS >= 0 && iCMOS < ChipSet.CMOS.ADDR.MASK) { + var b = (iCMOS <= ChipSet.CMOS.ADDR.RTC_STATUSD? this.getRTCByte(iCMOS) : this.abCMOSData[iCMOS]); + if (sDump) sDump += '\n'; + sDump += str.pad(p + "(" + str.toHexByte(iCMOS) + "):", 19) + str.toHexByte(b); + } + } + this.dbg.message(sDump); + } +}; + /** * inDMAChannelAddr(iDMAC, iChannel, port, addrFrom) * @@ -1948,13 +1991,13 @@ ChipSet.prototype.outDMAChannelCount = function(iDMAC, iChannel, port, bOut, add * Bits 4–7 are set whenever their corresponding channel is requesting service." * * TRIVIA: This hook wasn't installed when I was testing with the MODEL_5150 ROM BIOS, and it - * didn't matter, but the MODEL_5160 ROM BIOS checks it several times, including @F000:E156, where + * didn't matter, but the MODEL_5160 ROM BIOS checks it several times, including @ F000:E156, where * it verifies that TIMER1 didn't request service on channel 0. */ ChipSet.prototype.inDMAStatus = function(iDMAC, port, addrFrom) { /* - * HACK: Unlike the MODEL_5150, the MODEL_5160 ROM BIOS checks DMA channel 0 for TC (@F000:E4DF) + * HACK: Unlike the MODEL_5150, the MODEL_5160 ROM BIOS checks DMA channel 0 for TC (@ F000:E4DF) * after running a number of unrelated tests, since enough time would have passed for channel 0 to * have reached TC at least once. So I simply OR in a hard-coded TC bit for channel 0 every time * status is read. @@ -2855,9 +2898,9 @@ ChipSet.prototype.outTimer = function(iTimer, bOut, addrFrom) } /* - * HACK to detect lower-than-normal initial timer counts and reduce the length of CPU bursts using + * HACK to detect lower-than-normal initial timer counts and reduce the length of CPU bursts, using * cpu.setBurstDivisor(). Alternatively, the CPU could ask us for a cycle limit, via getTimerCycleLimit(), - * prior to starting a new burst, but for now, this hack actually performs better (see "BASICA DONKEY.BAS"). + * prior to starting a new burst, but this seems to perform better (see "BASICA DONKEY.BAS"). */ if (iTimer == ChipSet.TIMER0.INDEX) { var countInit = this.getTimerInit(ChipSet.TIMER0.INDEX); @@ -2874,21 +2917,12 @@ ChipSet.prototype.outTimer = function(iTimer, bOut, addrFrom) /* * HACK to satisfy the quick h/w interrupt turn-around expected by the ROM BIOS when it sets TIMER0 to a * low test count (0x16); since we typically don't update any of the timers until after we've finished a - * burst of CPU cycles, we originally solved that particular problem by forcing a h/w interrupt to be - * simulated immediately, by reducing the starting cycle count for TIMER0 to an earlier point in time and - * then immediately calling updateTimer(). + * burst of CPU cycles, we reduce the current burst cycle count, so that the burst will end at roughly the + * same time a timer interrupt is expected. Note that in some cases, if the number of cycles remaining + * in the current burst is less than the target, this will have the effect of *lengthening* the current + * burst instead of shortening it, but stepCPU() should be OK with that. * - * if (bOut == 0x16) { - * timer.nStartCycles -= 4 * bOut; // used a multiplier of 4 since there were always 4 cycles per tick - * this.updateTimer(iTimer); - * } - * - * However, a cleaner solution is to reduce the current burst cycle count instead, so that a timer interrupt - * will be simulated at the appropriate time, rather than immediately. Note that in some cases, if the number - * of cycles remaining in the current burst is less than the target, this will have the effect of *lengthening* - * the current burst instead of shortening it, but stepCPU() should be OK with that. - * - * Also notice how this complements the setBurstDivisor() HACK above: while that code is concerned with how + * Notice how this complements the setBurstDivisor() HACK above: while that code is concerned with how * to deal with low timer counts prior to starting new bursts, here we're concerned with low timer counts * (in particular, single-byte LSB counts) programmed in the middle of a burst. * @@ -2950,14 +2984,14 @@ ChipSet.prototype.outTimerCtrl = function(port, bOut, addrFrom) this.setTimerMode(iTimer, bcd, mode, rw); /* - * The 5150 ROM BIOS code @F000:E285 ("TEST.7") would fail after a warm boot (eg, after a CTRL-ALT-DEL) because + * The 5150 ROM BIOS code @ F000:E285 ("TEST.7") would fail after a warm boot (eg, after a CTRL-ALT-DEL) because * it assumed that no TIMER0 interrupt would occur between the point it unmasked the TIMER0 interrupt and the * point it started reprogramming TIMER0. * - * Similarly, the 5160 ROM BIOS @F000:E35D ("8253 TIMER CHECKOUT") would fail after initializing the EGA BIOS, + * Similarly, the 5160 ROM BIOS @ F000:E35D ("8253 TIMER CHECKOUT") would fail after initializing the EGA BIOS, * because the EGA BIOS uses TIMER0 during its diagnostics; as in the previous example, by the time the 8253 * test code runs later, there's now a pending TIMER0 interrupt, which triggers an interrupt as soon as IRQ0 is - * unmasked @F000:E364. + * unmasked @ F000:E364. * * After looking at this problem at bit more closely the second time around (while debugging the EGA BIOS), * it turns out I missed an important 8253 feature: whenever a new MODE0 control word OR a new MODE0 count @@ -2969,7 +3003,7 @@ ChipSet.prototype.outTimerCtrl = function(port, bOut, addrFrom) if (iTimer == ChipSet.TIMER0.INDEX) this.clearIRR(ChipSet.IRQ.TIMER0); /* - * Another TIMER0 HACK: The "CASSETTE DATA WRAP TEST" @F000:E51E occasionally reports an error when the second of + * Another TIMER0 HACK: The "CASSETTE DATA WRAP TEST" @ F000:E51E occasionally reports an error when the second of * two TIMER0 counts it latches is greater than the first. You would think the ROM BIOS would expect this, since * TIMER0 can reload its count at any time. Is the ROM BIOS assuming that TIMER0 was initialized sufficiently * recently that this should never happen? I'm not sure, but for now, let's try resetting TIMER0's count immediately @@ -3203,7 +3237,7 @@ ChipSet.prototype.updateTimer = function(iTimer, fCycleReset) } /* * Early implementation of this mode was minimal because when using this mode, the ROM BIOS simply wanted - * to see the count changing; it wasn't looking for interrupts. See ROM BIOS "TEST.03" code @F000:E0DE, + * to see the count changing; it wasn't looking for interrupts. See ROM BIOS "TEST.03" code @ F000:E0DE, * where TIMER1 is programmed for MODE2, LSB (the same settings, incidentally, used immediately afterward * for TIMER1 in conjunction with DMA channel 0 memory refreshes). * @@ -3383,7 +3417,7 @@ ChipSet.prototype.updatePPIB = function(bOut) this.bPPIB = bOut; if (fNewSpeaker != fOldSpeaker) { /* - * Originally, this code didn't catch the "ERROR_BEEP" case @F000:EC34, which first turns both PPI_B.CLK_TIMER2 (0x01) + * Originally, this code didn't catch the "ERROR_BEEP" case @ F000:EC34, which first turns both PPI_B.CLK_TIMER2 (0x01) * and PPI_B.SPK_TIMER2 (0x02) off, then turns on only PPI_B.SPK_TIMER2 (0x02), then restores the original port value. * * So, when the ROM BIOS keyboard buffer got full, we didn't issue a BEEP alert. I've fixed that by limiting the test @@ -3950,6 +3984,53 @@ ChipSet.prototype.inHFCStatus = function(port, addrFrom) return b; }; +/** + * intBIOSRTC(addr) + * + * INT 0x1A Quick Reference: + * + * AH + * ---- + * 0x00 Get current clock count in CX:DX + * 0x01 Set current clock count from CX:DX + * 0x02 Get real-time clock using BCD (CH=hours, CL=minutes, DH=seconds) + * 0x03 Set real-time clock using BCD (CH=hours, CL=minutes, DH=seconds, DL=1 if Daylight Savings Time option) + * 0x04 Get real-time date using BCD (CH=century, CL=year, DH=month, DL=day) + * 0x05 Set real-time date using BCD (CH=century, CL=year, DH=month, DL=day) + * 0x06 Set alarm using BCD (CH=hours, CL=minutes, DH=seconds) + * 0x07 Reset alarm + * + * @this {ChipSet} + * @param {number} addr + * @return {boolean} true to proceed with the INT 0x1A software interrupt, false to skip + */ +ChipSet.prototype.intBIOSRTC = function(addr) +{ + if (DEBUGGER) { + var AH = this.cpu.regAX >> 8; + if (this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_CHIPSET)) { + this.dbg.message("ChipSet.intBIOSRTC(AH=" + str.toHexByte(AH) + ") at " + str.toHexAddr(addr - this.cpu.segCS.base, this.cpu.segCS.sel)); + this.cpu.addInterruptReturn(addr, function(chipset, nCycles) { + return function onBIOSRTCReturn(nLevel) { + nCycles = chipset.cpu.getCycles() - nCycles; + var sResult = "C=" + (chipset.cpu.getCF()? 1 : 0); + var CL = chipset.cpu.regDX & 0xff; + var CH = chipset.cpu.regDX >> 8; + var DL = chipset.cpu.regDX & 0xff; + var DH = chipset.cpu.regDX >> 8; + if (AH == 0x02 || AH == 0x03) { + sResult += " CH(hour)=" + str.toHexWord(CH) + " CL(min)=" + str.toHexByte(CL) + " DH(sec)=" + str.toHexByte(DH); + } else if (AH == 0x04 || AH == 0x05) { + sResult += " CX(year)=" + str.toHexWord(chipset.cpu.regCX) + " DH(month)=" + str.toHexByte(DH) + " DL(day)=" + str.toHexByte(DL); + } + chipset.messageDebugger("ChipSet.intBIOSRTC(" + nLevel + "): " + sResult + " (cycles=" + nCycles + ")"); + }; + }(this, this.cpu.getCycles())); + } + } + return true; +}; + /** * parseSwitches(s, def) * diff --git a/my_modules/pcjs-client/lib/fdc.js b/my_modules/pcjs-client/lib/fdc.js index c7ae152d6..668515dbe 100644 --- a/my_modules/pcjs-client/lib/fdc.js +++ b/my_modules/pcjs-client/lib/fdc.js @@ -74,15 +74,15 @@ if (typeof module !== 'undefined') { * * 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) + * MT Multitrack 1 selects multitrack operation (both HD0 and HD1 will be read or written) * * N Number the number of data bytes written in a sector * - * NCN New Cylinder the new cylinder number for a seek operation + * NCN New Cylinder the new cylinder number for a Seek operation * * ND Non-Data Mode indicates an operation in the non-data mode * - * PCN Present Cylinder Number the cylinder number at the completion of a Sense interrupt status command + * 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 @@ -183,19 +183,13 @@ function FDC(parmsFDC) { Component.subclass(Component, FDC); -/* - * FDC BIOS interrupts, functions, and other parameters - */ -FDC.BIOS = {}; -FDC.BIOS.DISKETTE_INT = 0x13; - FDC.DEFAULT_DRIVE_NAME = "Floppy Drive"; /* * FDC Digital Output Register (DOR) (0x3F2, write-only) * - * NOTE: Reportedly, a drive's MOTOR bit had to be ON before the the drive could be selected, so outFDCOutput() - * verifies that. Also, motor start time for early model drives was 500ms, but we make no attempt to simulate that. + * 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 @@ -358,6 +352,12 @@ FDC.aCmdSeqs = { 0x0F: {cbWrite: 3, cbRead: 0, name: "SEEK"} }; +/* + * FDC BIOS interrupts, functions, and other parameters + */ +FDC.BIOS = {}; +FDC.BIOS.DISKETTE_INT = 0x13; + /** * setBinding(sHTMLClass, sHTMLType, sBinding, control) * @@ -538,7 +538,7 @@ FDC.prototype.powerUp = function(data, fRepower) for (iDrive = 0; iDrive < this.nDrives; iDrive++) { var drive = this.aDrives[iDrive]; drive.bType = this.chipset.getSWFloppyDriveType(iDrive); - if (drive.bType == ChipSet.FDRIVE.DSHC) { + if (drive.bType == ChipSet.FDRIVE.DSHD) { drive.nCylinders = 80; } } @@ -1363,14 +1363,14 @@ FDC.prototype.outFDCOutput = function(port, bOut, addrFrom) * MODEL_5170 boot code. Here's why: * * Unlike previous models, the MODEL_5170 BIOS probes all installed diskette drives to determine drive type; - * ie, DSDD (40-track) or DSHC (80-track). So if there are two drives, the last selected drive will be drive 1. - * Immediately before booting, the BIOS issues an INT 0x3/AH=0 reset, which writes regOutput two times: first + * ie, DSDD (40-track) or DSHD (80-track). So if there are two drives, the last selected drive will be drive 1. + * Immediately before booting, the BIOS issues an INT 0x13/AH=0 reset, which writes regOutput two times: first * with FDC.REG_OUTPUT.ENABLE clear, and then with it set. However, both times, it ALSO loads the last selected - * drive # into regOutput's "drive select" bits. + * drive number into regOutput's "drive select" bits. * * If we switched our selected drive to match regOutput, then the ST0 value we returned on an INT_STATUS command * following the regOutput reset operation would indicate drive 1 instead of drive 0. But the BIOS requires - * the ST0 result from the INT_STATUS command ALWAYS be 0xC0, not 0xC1, so the controller must not be propagating + * the ST0 result from the INT_STATUS command ALWAYS be 0xC0 (not 0xC1), so the controller must not be propagating * regOutput's "drive select" bits in the way I originally assumed. */ // var iDrive = bOut & FDC.REG_OUTPUT.DS; @@ -1483,26 +1483,28 @@ FDC.prototype.outFDCControl = function(port, bOut, addrFrom) /** * 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. + * 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 + * 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 + * 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 @@ -1522,7 +1524,9 @@ FDC.prototype.intBIOSDiskette = function(addr) // this.cpu.haltCPU(); this.cpu.addInterruptReturn(addr, function (fdc, nCycles) { return function onBIOSDisketteReturn(nLevel) { - fdc.intBIOSDisketteReturn(nCycles, nLevel); + nCycles = fdc.cpu.getCycles() - nCycles; + fdc.messageDebugger("FDC.intBIOS(" + nLevel + "): C=" + (fdc.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")"); + // if (DEBUG && nCycles > 10000) fdc.cpu.haltCPU(); }; }(this, this.cpu.getCycles())); } @@ -1530,22 +1534,6 @@ FDC.prototype.intBIOSDiskette = function(addr) return true; }; -/** - * intBIOSDisketteReturn(nCycles, nLevel) - * - * @this {FDC} - * @param {number} nCycles - * @param {number} nLevel - */ -FDC.prototype.intBIOSDisketteReturn = function(nCycles, nLevel) -{ - if (DEBUGGER) { - nCycles = this.cpu.getCycles() - nCycles; - this.messageDebugger("FDC.intBIOSReturn(" + nLevel + "): C=" + (this.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")"); - // if (DEBUG && nCycles > 10000) this.cpu.haltCPU(); - } -}; - /** * doCmd() * @@ -1622,11 +1610,12 @@ FDC.prototype.doCmd = function() 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 are provided; this command is typically followed by FDC.REG_DATA.CMD.INT_STATUS + this.beginResult(); // no results provided; this command is typically followed by FDC.REG_DATA.CMD.INT_STATUS fIRQ = true; break; case FDC.REG_DATA.CMD.INT_STATUS: // 0x08 (SENSE INTERRUPT STATUS) drive = this.aDrives[this.iDrive]; + drive.bHead = 0; // this command is documented as ALWAYS returning a head address of 0 in ST0; see pushST0() this.beginResult(); this.pushST0(drive); this.pushResult(drive.bCylinder, "PCN");// no interrupt is generated by this command, so fIRQ should remain false @@ -1784,7 +1773,7 @@ FDC.prototype.pushResult = function(bResult, name) */ FDC.prototype.pushST0 = function(drive) { - this.pushResult(drive.iDrive | drive.bHead | (drive.resCode & FDC.REG_DATA.RES.ST0), "ST0"); + this.pushResult(drive.iDrive | (drive.bHead << 2) | (drive.resCode & FDC.REG_DATA.RES.ST0), "ST0"); }; /** @@ -1889,7 +1878,8 @@ 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". + * 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; diff --git a/my_modules/pcjs-client/lib/hdc.js b/my_modules/pcjs-client/lib/hdc.js index eb45e1bf4..e5c9ce839 100644 --- a/my_modules/pcjs-client/lib/hdc.js +++ b/my_modules/pcjs-client/lib/hdc.js @@ -105,57 +105,6 @@ function HDC(parmsHDC) { Component.subclass(Component, HDC); -/* - * HDC BIOS interrupts, functions, and other parameters - */ -HDC.BIOS = {}; -HDC.BIOS.DISK_INT = 0x13; - -HDC.BIOS.DISK_CMD = {}; -HDC.BIOS.DISK_CMD.RESET = 0x00; -HDC.BIOS.DISK_CMD.GET_STATUS = 0x01; -HDC.BIOS.DISK_CMD.READ_SECTORS = 0x02; -HDC.BIOS.DISK_CMD.WRITE_SECTORS = 0x03; -HDC.BIOS.DISK_CMD.VERIFY_SECTORS = 0x04; -HDC.BIOS.DISK_CMD.FORMAT_TRACK = 0x05; -HDC.BIOS.DISK_CMD.FORMAT_BAD = 0x06; -HDC.BIOS.DISK_CMD.FORMAT_DRIVE = 0x07; -HDC.BIOS.DISK_CMD.GET_DRIVEPARMS = 0x08; -HDC.BIOS.DISK_CMD.SET_DRIVEPARMS = 0x09; -HDC.BIOS.DISK_CMD.READ_LONG = 0x0A; -HDC.BIOS.DISK_CMD.WRITE_LONG = 0x0B; -HDC.BIOS.DISK_CMD.SEEK = 0x0C; -HDC.BIOS.DISK_CMD.ALT_RESET = 0x0D; -HDC.BIOS.DISK_CMD.READ_BUFFER = 0x0E; -HDC.BIOS.DISK_CMD.WRITE_BUFFER = 0x0F; -HDC.BIOS.DISK_CMD.TEST_READY = 0x10; -HDC.BIOS.DISK_CMD.RECALIBRATE = 0x11; -HDC.BIOS.DISK_CMD.RAM_DIAGNOSTIC = 0x12; -HDC.BIOS.DISK_CMD.DRV_DIAGNOSTIC = 0x13; -HDC.BIOS.DISK_CMD.CTL_DIAGNOSTIC = 0x14; - -/* - * When the HDC BIOS overwrites the ROM BIOS INT 0x13 address, it saves the original INT 0x13 address - * in the INT 0x40 vector. The HDC BIOS's plan was simple, albeit slightly flawed: assign fixed disks - * drive numbers >= 0x80, and whenever someone calls INT 0x13 with a drive number < 0x80, invoke the - * original INT 0x13 diskette code via INT 0x40 and return via RET 2. - * - * Unfortunately, not all original INT 0x13 functions required a drive number in DL (eg, the "reset" - * function, where AH=0). And the HDC BIOS knew this, which is why, in the case of the "reset" function, - * the HDC BIOS performs BOTH an INT 0x40 diskette reset AND an HDC reset -- it can't be sure which - * controller the caller really wants to reset. - * - * An unfortunate side-effect of this behavior: when the HDC BIOS is initialized for the first time, it may - * issue several resets internally, depending on whether there are 0, 1 or 2 hard disks installed, and each - * of those resets also triggers completely useless diskette resets, each wasting up to two seconds waiting - * for the FDC to interrupt. The FDC tries to interrupt, but it can't, because at this early stage of - * ROM BIOS initialization, IRQ_FDC hasn't been unmasked yet. - * - * My work-around: have the HDC component hook INT 0x40, and every time an INT 0x40 is issued with AH=0 and - * IRQ_FDC masked, eat the INT 0x40 interrupt. - */ -HDC.BIOS.DISKETTE_INT = 0x40; - /* * HDC defaults, in case drive parameters weren't specified */ @@ -330,6 +279,41 @@ if (DEBUG) { }; } +/* + * HDC BIOS interrupts, functions, and other parameters + */ +HDC.BIOS = {}; +HDC.BIOS.DISK_INT = 0x13; + +HDC.BIOS.DISK_CMD = {}; +HDC.BIOS.DISK_CMD.RESET = 0x00; +HDC.BIOS.DISK_CMD.GET_STATUS = 0x01; +HDC.BIOS.DISK_CMD.READ_SECTORS = 0x02; +HDC.BIOS.DISK_CMD.WRITE_SECTORS = 0x03; +HDC.BIOS.DISK_CMD.VERIFY_SECTORS = 0x04; +HDC.BIOS.DISK_CMD.FORMAT_TRACK = 0x05; +HDC.BIOS.DISK_CMD.FORMAT_BAD = 0x06; +HDC.BIOS.DISK_CMD.FORMAT_DRIVE = 0x07; +HDC.BIOS.DISK_CMD.GET_DRIVEPARMS = 0x08; +HDC.BIOS.DISK_CMD.SET_DRIVEPARMS = 0x09; +HDC.BIOS.DISK_CMD.READ_LONG = 0x0A; +HDC.BIOS.DISK_CMD.WRITE_LONG = 0x0B; +HDC.BIOS.DISK_CMD.SEEK = 0x0C; +HDC.BIOS.DISK_CMD.ALT_RESET = 0x0D; +HDC.BIOS.DISK_CMD.READ_BUFFER = 0x0E; +HDC.BIOS.DISK_CMD.WRITE_BUFFER = 0x0F; +HDC.BIOS.DISK_CMD.TEST_READY = 0x10; +HDC.BIOS.DISK_CMD.RECALIBRATE = 0x11; +HDC.BIOS.DISK_CMD.RAM_DIAGNOSTIC = 0x12; +HDC.BIOS.DISK_CMD.DRV_DIAGNOSTIC = 0x13; +HDC.BIOS.DISK_CMD.CTL_DIAGNOSTIC = 0x14; + +/* + * When the HDC BIOS overwrites the ROM BIOS INT 0x13 address, it saves the original INT 0x13 address + * in the INT 0x40 vector. + */ +HDC.BIOS.DISKETTE_INT = 0x40; + /** * setBinding(sHTMLClass, sHTMLType, sBinding, control) * @@ -1068,7 +1052,7 @@ HDC.prototype.outHDCPulse = function(port, bOut, addrFrom) { */ this.regPulse = bOut; /* - * The HDC BIOS "COMMAND" function (@C800:0562) waits for these ALL status bits after writing to both regPulse + * The HDC BIOS "COMMAND" function (@ C800:0562) waits for these ALL status bits after writing to both regPulse * and regPattern, so we must oblige it. */ /* @@ -1108,25 +1092,28 @@ HDC.prototype.outHDCNoise = function(port, bOut, addrFrom) { /** * intBIOSDisk(addr) * - * NOTE: This function tries to differentiate HDC requests from FDC requests, by whether the INT 0x13 drive number in DL is >= 0x80 + * NOTE: This function differentiates HDC requests from FDC requests, based on whether the INT 0x13 drive number + * in DL is >= 0x80. * - * HACK: The HDC BIOS code for both INT 0x13/AH=0x00 and INT 0x13/AH=0x09 calls "INIT_DRV" @C800:0427, which is hard-coded - * to issue the HDC.REG_DATA.CMD.INIT_DRIVE command for BOTH drives 0 and 1 (aka drive numbers 0x80 and 0x81), regardless of - * the drive number specified in DL; this means that the HDC.REG_DATA.CMD.INIT_DRIVE command must always succeed for drive 1 - * if it also succeeds for drive 0 -- even if there is no drive 1. Bizarre, but OK, whatever. + * HACK: The HDC BIOS code for both INT 0x13/AH=0x00 and INT 0x13/AH=0x09 calls "INIT_DRV" @ C800:0427, which is + * hard-coded to issue the HDC.REG_DATA.CMD.INIT_DRIVE command for BOTH drives 0 and 1 (aka drive numbers 0x80 and + * 0x81), regardless of the drive number specified in DL; this means that the HDC.REG_DATA.CMD.INIT_DRIVE command + * must always succeed for drive 1 if it also succeeds for drive 0 -- even if there is no drive 1. Bizarre, but OK, + * whatever. * - * So assuming we a have drive 0, when the power-on diagnostics in "DISK_SETUP" @C800:0003 call INT 0x13/AH=0x09 @C800:00DB - * for drive 0, it must succeed. No problem. But when "DISK_SETUP" starts probing for additional drives, it first issues - * INT 0x13/AH=0x00, followed by INT 0x13/AH=0x11, and finally INT 0x13/AH=0x09. If the first (AH=0x00) or third (AH=0x09) - * INT 0x13 fails, it quickly moves on (ie, it jumps to "POD_DONE"). But as we just discussed, both those operations call "INIT_DRV", - * which can't return an error. This means the only function that can return an error in this context is the recalibrate function - * (AH=0x11). That sucks, because the way the HDC BIOS is written, it will loop for anywhere from 1.5 seconds to 25 seconds - * (depending on whether the controller is part of the "System Unit" or not; see port 0x213), attempting to recalibrate drive 1 - * until it finally times out. + * So assuming we a have drive 0, when the power-on diagnostics in "DISK_SETUP" @ C800:0003 call INT 0x13/AH=0x09 + * @ C800:00DB for drive 0, it must succeed. No problem. But when "DISK_SETUP" starts probing for additional drives, + * it first issues INT 0x13/AH=0x00, followed by INT 0x13/AH=0x11, and finally INT 0x13/AH=0x09. If the first + * (AH=0x00) or third (AH=0x09) INT 0x13 fails, it quickly moves on (ie, it jumps to "POD_DONE"). But as we just + * discussed, both those operations call "INIT_DRV", which can't return an error. This means the only function that + * can return an error in this context is the recalibrate function (AH=0x11). That sucks, because the way the HDC + * BIOS is written, it will loop for anywhere from 1.5 seconds to 25 seconds (depending on whether the controller + * is part of the "System Unit" or not; see port 0x213), attempting to recalibrate drive 1 until it finally times out. * - * Normally, you'll only experience the 1.5 second delay, but even so, it's a ridiculous waste of time and a lot of useless - * INT 0x13 calls. So I monitor INT 0x13/AH=0x00 for DL >= 0x80 and set a special HDC.REG_DATA.CMD.INIT_DRIVE override flag - * (iDriveAllowFail) that will allow that command to fail, and in theory, make the the HDC BIOS "DISK_SETUP" code much more efficient. + * Normally, you'll only experience the 1.5 second delay, but even so, it's a ridiculous waste of time and a lot of + * useless INT 0x13 calls. So I monitor INT 0x13/AH=0x00 for DL >= 0x80 and set a special HDC.REG_DATA.CMD.INIT_DRIVE + * override flag (iDriveAllowFail) that will allow that command to fail, and in theory, make the the HDC BIOS + * "DISK_SETUP" code much more efficient. * * @this {HDC} * @param {number} addr @@ -1135,16 +1122,16 @@ HDC.prototype.outHDCNoise = function(port, bOut, addrFrom) { HDC.prototype.intBIOSDisk = function(addr) { var AH = this.cpu.regAX >> 8; var DL = this.cpu.regDX & 0xff; - if (!AH && DL > 0x80) { - this.iDriveAllowFail = DL - 0x80; - } + if (!AH && DL > 0x80) this.iDriveAllowFail = DL - 0x80; if (DEBUGGER) { if (this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_HDC) && DL >= 0x80) { this.dbg.message("HDC.intBIOSDisk(AX=" + str.toHexWord(this.cpu.regAX) + ",DL=" + str.toHexByte(DL) + ") at " + str.toHexAddr(addr - this.cpu.segCS.base, this.cpu.segCS.sel)); // this.cpu.haltCPU(); this.cpu.addInterruptReturn(addr, function (hdc, nCycles) { return function onBIOSDiskReturn(nLevel) { - hdc.intBIOSDiskReturn(nCycles, nLevel); + nCycles = hdc.cpu.getCycles() - nCycles; + hdc.messageDebugger("HDC.intBIOSDisk(" + nLevel + "): C=" + (hdc.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")"); + // if (DEBUG && nCycles > 10000) hdc.cpu.haltCPU(); }; }(this, this.cpu.getCycles())); } @@ -1152,27 +1139,30 @@ HDC.prototype.intBIOSDisk = function(addr) { return true; }; -/** - * intBIOSDiskReturn(nCycles, nLevel) - * - * @this {HDC} - * @param {number} nCycles - * @param {number} nLevel - */ -HDC.prototype.intBIOSDiskReturn = function(nCycles, nLevel) { - if (DEBUGGER) { - nCycles = this.cpu.getCycles() - nCycles; - this.messageDebugger("HDC.intBIOSDiskReturn(" + nLevel + "): C=" + (this.cpu.getCF()? 1 : 0) + " (cycles=" + nCycles + ")"); - // if (DEBUG && nCycles > 10000) this.cpu.haltCPU(); - } -}; - /** * intBIOSDiskette(addr) * - * Every time an INT 0x40 is issued with AH=0 and IRQ_FDC masked, eat the INT 0x40 interrupt. + * When the HDC BIOS overwrites the ROM BIOS INT 0x13 address, it saves the original INT 0x13 address + * in the INT 0x40 vector. This function intercepts calls to that vector to work around a minor nuisance. + * + * The HDC BIOS's plan was simple, albeit slightly flawed: assign fixed disks drive numbers >= 0x80, + * and whenever someone calls INT 0x13 with a drive number < 0x80, invoke the original INT 0x13 diskette + * code via INT 0x40 and return via RET 2. * - * For more details on why this is necessary, see the definition of HDC.BIOS.DISKETTE_INT (above) + * Unfortunately, not all original INT 0x13 functions required a drive number in DL (eg, the "reset" + * function, where AH=0). And the HDC BIOS knew this, which is why, in the case of the "reset" function, + * the HDC BIOS performs BOTH an INT 0x40 diskette reset AND an HDC reset -- it can't be sure which + * controller the caller really wants to reset. + * + * An unfortunate side-effect of this behavior: when the HDC BIOS is initialized for the first time, it may + * issue several resets internally, depending on whether there are 0, 1 or 2 hard disks installed, and each + * of those resets also triggers completely useless diskette resets, each wasting up to two seconds waiting + * for the FDC to interrupt. The FDC tries to interrupt, but it can't, because at this early stage of + * ROM BIOS initialization, IRQ_FDC hasn't been unmasked yet. + * + * My work-around: have the HDC component hook INT 0x40, and every time an INT 0x40 is issued with AH=0 and + * IRQ_FDC masked, bypass the INT 0x40 interrupt. This is as close as PCjs has come to patching any BIOS code + * (something I refuse to do), and even here, I'm not doing it out of necessity, just annoyance. * * @this {HDC} * @param {number} addr