using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Contralto.Memory; using System.IO; using Contralto.Logging; namespace Contralto.IO { public class DiskController : IClockable { public DiskController(AltoSystem system) { _system = system; Reset(); // Load the pack _pack = new DiabloPack(DiabloDiskType.Diablo31); // TODO: this does not belong here. FileStream fs = new FileStream("Disk\\tdisk4.dsk", FileMode.Open, FileAccess.Read); _pack.Load(fs); fs.Close(); } /// /// TODO: this is messy; the read and write sides of KDATA are distinct hardware. /// According to docs, on a Write, eventually it appears on the Read side during an actual write to the disk /// but not right away. For now, this never happens (since we don't yet support writing). /// public ushort KDATA { get { return _kDataRead; } set { _kDataWrite = value; } } public ushort KADR { get { return _kAdr; } set { _kAdr = value; _recNo = 0; // "In addition, it causes the head address bit to be loaded from KDATA[13]." int newHead = (_kDataWrite & 0x4) >> 2; Log.Write(LogComponent.DiskController, "At sector time {0}:", _elapsedSectorTime); if (newHead != _head) { // If we switch heads, we need to reload the sector _head = newHead; LoadSector(); } // "0 normally, 1 if the command is to terminate immediately after the correct cylinder // position is reached (before any data is transferred)." _dataXfer = (_kAdr & 0x2) != 0x2; Log.Write(LogComponent.DiskController, "KADR set to {0} (Header {1}, Label {2}, Data {3}, Xfer {4}, Drive {5})", Conversion.ToOctal(_kAdr), Conversion.ToOctal((_kAdr & 0xc0) >> 6), Conversion.ToOctal((_kAdr & 0x30) >> 4), Conversion.ToOctal((_kAdr & 0xc) >> 2), _dataXfer, _kAdr & 0x1); Log.Write(LogComponent.DiskController, " -Disk Address ({0}) is C/H/S {1}/{2}/{3}, Drive {4} Restore {5}", Conversion.ToOctal(_kDataWrite), (_kDataWrite & 0x0ff8) >> 3, _head, (_kDataWrite & 0xf000) >> 12, (_kDataWrite & 0x2) >> 1, (_kDataWrite & 0x1)); Log.Write(LogComponent.DiskController, " -Selected disk is {0}", ((_kDataWrite & 0x2) >> 1) ^ (_kAdr & 0x1)); } } public ushort KCOM { get { return _kCom; } set { _kCom = value; // Read control bits (pg. 47 of hw manual) _xferOff = (_kCom & 0x10) == 0x10; _wdInhib = (_kCom & 0x08) == 0x08; _bClkSource = (_kCom & 0x04) == 0x04; _wffo = (_kCom & 0x02) == 0x02; _sendAdr = (_kCom & 0x01) == 0x01; _diskBitCounterEnable = _wffo; // Update WDINIT state based on _wdInhib. if (_wdInhib) { _wdInit = true; } } } /// /// Used by the DiskTask code to check the WDINIT signal for dispatch. /// public bool WDINIT { get { return _wdInit; } } public ushort KSTAT { get { // Bits 4-7 of KSTAT are always 1s. return (ushort)(_kStat | (0x0f00)); } set { _kStat = value; } } public ushort RECNO { get { return _recMap[_recNo]; } } public bool DataXfer { get { return _dataXfer; } } /// /// This is a hack to see how the microcode expects INIT to work /// public bool RecordInit { get { return _sectorWordTime < 10; } } public int Cylinder { get { return _cylinder; } } public int SeekCylinder { get { return _destCylinder; } } public int Head { get { return _head; } } public int Sector { get { return _sector; } } public int Drive { get { return 0; } } public double ClocksUntilNextSector { get { return _sectorClocks - _elapsedSectorTime; } } public bool Ready { get { // TODO: verify if this is correct. // Not ready if we're in the middle of a seek. return (_kStat & 0x0040) == 0; } } public void Reset() { ClearStatus(); _recNo = 0; _elapsedSectorTime = 0.0; _cylinder = _destCylinder = 0; _sector = 0; _head = 0; _kStat = 0; _kDataRead = 0; _kDataWrite = 0; _sendAdr = false; _wdInhib = true; _xferOff = true; _wdInit = false; _diskBitCounterEnable = false; _sectorWordIndex = 0; _sectorWordTime = 0; InitSector(); // Wakeup the sector task first thing _system.CPU.WakeupTask(CPU.TaskType.DiskSector); } public void Clock() { _elapsedSectorTime++; // TODO: only signal sector changes if disk is loaded, etc. if (_elapsedSectorTime > _sectorClocks ) { // // Next sector; save fractional part of elapsed time (to more accurately keep track of time), move to next sector // and wake up sector task. // _elapsedSectorTime -= _sectorClocks; _sector = (_sector + 1) % 12; _kStat = (ushort)((_kStat & 0x0fff) | (_sector << 12)); // TODO: seclate semantics. Looks like if the sector task was BLOCKed when a new sector is signaled // then the seclate flag is set. // Reset internal state machine for sector data _sectorWordIndex = 0; _sectorWordTime = 0.0; _kDataRead = 0; // Load new sector in LoadSector(); // Only wake up if not actively seeking. if ((_kStat & 0x0040) == 0) { Log.Write(LogType.Verbose, LogComponent.DiskController, "Waking up sector task for C/H/S {0}/{1}/{2}", _cylinder, _head, _sector); _system.CPU.WakeupTask(CPU.TaskType.DiskSector); } } // If seek is in progress, move closer to the desired cylinder... // TODO: move bitfields to enums / constants, this is getting silly. if ((_kStat & 0x0040) != 0) { _elapsedSeekTime++; if (_elapsedSeekTime > _seekClocks) { _elapsedSeekTime -= _seekClocks; if (_cylinder < _destCylinder) { _cylinder++; } else if (_cylinder > _destCylinder) { _cylinder--; } Log.Write(LogComponent.DiskController, "Seek progress: cylinder {0} reached.", _cylinder); // Are we *there* yet? if (_cylinder == _destCylinder) { // clear Seek bit _kStat &= 0xffbf; Log.Write(LogComponent.DiskController, "Seek to {0} completed.", _cylinder); } } } // // Spin the disk platter and read in words as applicable. // SpinDisk(); // // Update the WDINIT signal; this is based on WDALLOW (!_wdInhib) which sets WDINIT (this is done // in KCOM way above). // WDINIT is reset when BLOCK (a BLOCK F1 is being executed) and WDTSKACT (the disk word task is running) are 1. // if (_system.CPU.CurrentTask.Priority == (int)CPU.TaskType.DiskWord && _system.CPU.CurrentTask.BLOCK) { _wdInit = false; } } public void ClearStatus() { // "...clears KSTAT[13]." (chksum error flag) _kStat &= 0xfffb; } public void IncrementRecord() { // "Advances the shift registers holding the KADR register so that they present the number and read/write/check status of the // next record to the hardware." // "RECORD" in this context indicates the sector field corresponding to the 2 bit "action" field in the KADR register // (i.e. one of Header, Label, or Data.) // INCRECNO shifts the data over two bits to select from Header->Label->Data. _kAdr = (ushort)(_kAdr << 2); _recNo++; if (_recNo > 3) { // sanity check for now throw new InvalidOperationException("Unexpected INCRECORD past rec 3."); } } public void Strobe() { // // "Initiates a disk seek operation. The KDATA register must have been loaded previously, // and the SENDADR bit of the KCOMM register previously set to 1." // // sanity check: see if SENDADR bit is set, if not we'll signal an error (since I'm trusting that // the official Xerox uCode is doing the right thing, this will help ferret out emulation issues. // eventually this can be removed.) if (!_sendAdr) { throw new InvalidOperationException("STROBE while SENDADR bit of KCOM not 1. Unexpected."); } Log.Write(LogComponent.DiskController, "STROBE: Seek initialized."); _destCylinder = (_kDataWrite & 0x0ff8) >> 3; // set "seek fail" bit based on selected cylinder (if out of bounds) and do not // commence a seek if so. if (_destCylinder > 202) { _kStat |= 0x0080; Log.Write(LogComponent.DiskController, "Seek failed, specified cylinder {0} is out of range.", _destCylinder); } else { // Otherwise, start a seek. // Clear the fail bit. _kStat &= 0xff7f; // Set seek bit _kStat |= 0x0040; // And figure out how long this will take. _seekClocks = CalculateSeekTime(); _elapsedSeekTime = 0.0; Log.Write(LogComponent.DiskController, "Seek to {0} from {1} commencing. Will take {2} clocks.", _destCylinder, _cylinder, _seekClocks); } } private double CalculateSeekTime() { // How many cylinders are we moving? int dt = Math.Abs(_destCylinder - _cylinder); // // From the Hardware Manual, pg 43: // "Seek time (approx.): 15 + 8.6 * sqrt(dt) (msec) // double seekTimeMsec = 15.0 + 8.6 * Math.Sqrt(dt); return seekTimeMsec / AltoSystem.ClockInterval; } /// /// "Rotates" the emulated disk platter one clock's worth. /// private void SpinDisk() { // // Roughly: If transfer is enabled: // Select data word based on elapsed time in this sector. // On a new word, wake up the disk word task if not inhibited. // // If transfer is not enabled BUT the disk word task is enabled, // we will still wake up the disk word task if the appropriate clock // source is selected. // // We simulate the movement of a sector under the heads by dividing // the sector into word-sized timeslices. Not all of these slices // will actually contain valid data -- some are empty, used by the microcode // for lead-in or inter-record delays, but the slices are still used to // keep things in line time-wise; the real hardware uses a crystal-controlled clock // to generate these slices during these periods (and the clock comes from the // disk itself when actual data is present). For our purposes, the two clocks // are one and the same. // // Move the disk forward one clock _sectorWordTime++; // If we have reached a new word timeslice, do something appropriate. if (_sectorWordTime > _wordDuration) { // Save the fractional portion of the timeslice for the next slice _sectorWordTime -= _wordDuration; // // Pick out the word that just passed under the head. This may not be // actual data (it could be the pre-header delay, inter-record gaps or sync words) // and we may not actually end up doing anything with it, but we may // need it to decide whether to do anything at all. // ushort diskWord = _sectorData[_sectorWordIndex].Data; bool bWakeup = false; // // If the word task is enabled AND the write ("crystal") clock is enabled // then we will wake up the word task now. // if (!_wdInhib && !_bClkSource) { bWakeup = true; } // // If the clock is enabled OR the WFFO bit is set (go ahead and run the bit clock) // then we will wake up the word task and read in the data if transfers are not // inhibited. TODO: this should only happen on reads. // if (_wffo || _diskBitCounterEnable) { if (!_xferOff) { Log.Write(LogType.Verbose, LogComponent.DiskWordTask, "Sector {0} Word {1} read into KDATA", _sector, Conversion.ToOctal(diskWord)); _kDataRead = diskWord; } if (!_wdInhib) { bWakeup = true; } } // // If the WFFO bit is cleared (wait for the sync word to be read) // then we check the word for a "1" (the sync word) to enable // the clock. This occurs late in the cycle so that the NEXT word // (not the sync word) is actually read. TODO: this should only happen on reads. // if (!_wffo && diskWord == 1) { _diskBitCounterEnable = true; } if (bWakeup) { Log.Write(LogType.Verbose, LogComponent.DiskWordTask, "Word task awoken for word {0}.", _sectorWordIndex); _system.CPU.WakeupTask(CPU.TaskType.DiskWord); } // Last, move to the next word. _sectorWordIndex++; } } private void LoadSector() { // // Pull data off disk and pack it into our faked-up sector. // Note that this data is packed in in REVERSE ORDER because that's // how it gets written out and it's how the Alto expects it to be read back in. // DiabloDiskSector sector = _pack.GetSector(_cylinder, _head, _sector); // debugging /* if (_cylinder >= 32) { Console.WriteLine("loading in C/H/S {0}/{1}/{2}", _cylinder, _head, _sector); for(int i=0;i