From 3b29addb985c1ee2cb6aed5b231c79bf9e930b49 Mon Sep 17 00:00:00 2001 From: Josh Dersch Date: Fri, 23 Oct 2015 16:07:07 -0700 Subject: [PATCH] Disk sector reads now work correctly, and checksums are correctly calculated. Still feeding dummy sector data. --- Contralto/CPU/Tasks/DiskTask.cs | 9 +- Contralto/CPU/Tasks/Task.cs | 5 + Contralto/IO/DiskController.cs | 460 +++++++++++++++++--------------- Contralto/Memory/Memory.cs | 1 + 4 files changed, 262 insertions(+), 213 deletions(-) diff --git a/Contralto/CPU/Tasks/DiskTask.cs b/Contralto/CPU/Tasks/DiskTask.cs index 7f39f8d..e65d6e3 100644 --- a/Contralto/CPU/Tasks/DiskTask.cs +++ b/Contralto/CPU/Tasks/DiskTask.cs @@ -45,8 +45,9 @@ namespace Contralto.CPU return _cpu._system.DiskController.KSTAT; case DiskBusSource.ReadKDATA: - Console.WriteLine("kdata read"); - return _cpu._system.DiskController.KDATA; + ushort kdata = _cpu._system.DiskController.KDATA; + Console.WriteLine("kdata read {0}", OctalHelpers.ToOctal(kdata)); + return kdata; default: throw new InvalidOperationException(String.Format("Unhandled bus source {0}", bs)); @@ -87,8 +88,10 @@ namespace Contralto.CPU // "KSTAT[12-15] are loaded from BUS[12-15]. (Actually BUS[13] is ORed onto // KSTAT[13].)" - // OR in BUS[12-15] after masking in KSTAT[13] so it is ORed in properly. + // OR in BUS[12-15] after masking in KSTAT[13] so it is ORed in properly. _cpu._system.DiskController.KSTAT = (ushort)(((_cpu._system.DiskController.KSTAT & 0xfff4)) | (_busData & 0xf)); + + Console.WriteLine("KSTAT loaded with {0}, is now {1}", (_busData & 0xf), _cpu._system.DiskController.KSTAT); break; case DiskF1.STROBE: diff --git a/Contralto/CPU/Tasks/Task.cs b/Contralto/CPU/Tasks/Task.cs index 5911fee..e9451fd 100644 --- a/Contralto/CPU/Tasks/Task.cs +++ b/Contralto/CPU/Tasks/Task.cs @@ -337,6 +337,11 @@ namespace Contralto.CPU if (loadR) { _cpu._r[_rSelect] = Shifter.DoOperation(_cpu._l, _cpu._t); + + if(_rSelect == 26) + { + Console.WriteLine("cksum is now {0}", OctalHelpers.ToOctal(_cpu._r[_rSelect])); + } } // Do writeback to selected R register from M diff --git a/Contralto/IO/DiskController.cs b/Contralto/IO/DiskController.cs index 87cf758..5119aef 100644 --- a/Contralto/IO/DiskController.cs +++ b/Contralto/IO/DiskController.cs @@ -17,6 +17,8 @@ namespace Contralto.IO // Wakeup the sector task first thing _system.CPU.WakeupTask(CPU.TaskType.DiskSector); + + } public ushort KDATA @@ -61,13 +63,15 @@ namespace Contralto.IO Console.WriteLine( "sst {0}, xferOff {1}, wdInhib {2}, bClkSource {3}, wffo {4}, sendAdr {5}", - _elapsedSectorStateTime, + _sectorWordTime, _xferOff, _wdInhib, _bClkSource, _wffo, _sendAdr); + _diskBitCounterEnable = _wffo; + // Update WDINIT state based on _wdInhib. if (_wdInhib) { @@ -87,14 +91,12 @@ namespace Contralto.IO public ushort KSTAT { get - { - Console.WriteLine("kstat read {0}", _kStat); + { return _kStat; } set { - _kStat = value; - Console.WriteLine("kstat write {0}", _kStat); + _kStat = value; } } @@ -113,7 +115,7 @@ namespace Contralto.IO /// public bool RecordInit { - get { return _elapsedSectorStateTime < 10; } + get { return _sectorWordTime < 10; } } public int Cylinder @@ -160,11 +162,15 @@ namespace Contralto.IO _xferOff = true; _wdInit = false; + + _diskBitCounterEnable = false; + + InitSector(); } public void Clock() { - _elapsedSectorTime++; + _elapsedSectorTime++; // TODO: only signal sector changes if disk is loaded, etc. if (_elapsedSectorTime > _sectorClocks) @@ -183,12 +189,14 @@ namespace Contralto.IO // then the seclate flag is set. // Reset internal state machine for sector data - _sectorState = SectorState.HeaderReadDelay; _sectorWordIndex = 0; - _elapsedSectorStateTime = 0.0; + _sectorWordTime = 0.0; Console.WriteLine("New sector ({0}), switching to HeaderReadDelay state.", _sector); _kData = 13; + // Load new sector in + LoadSector(); + _system.CPU.WakeupTask(CPU.TaskType.DiskSector); } @@ -200,7 +208,7 @@ namespace Contralto.IO _elapsedSeekTime++; if (_elapsedSeekTime > _seekClocks) { - _elapsedSectorTime -= _seekClocks; + _elapsedSeekTime -= _seekClocks; if (_cylinder < _destCylinder) { @@ -221,184 +229,9 @@ namespace Contralto.IO } // - // Select data word based on elapsed time in this sector, if data transfer is not inhibited. - // On a new word, wake up the disk word task if not inhibited - // TODO: the exact mechanics of this are still kind of mysterious. - // Things to examine the schematics / uCode for: - // - Use of WFFO bit -- is this related to the "sync word" that the docs mention? - // - how does WFFO work -- I assume the "1 bit" mentioned in the docs indicates the MFM bit - // and indicates the start of the next data word (or is at least used to wait for the next - // data word) - // - How does the delaying work - // The microcode word-copying code works basically as: - // On wakeup: - // - read word, store into memory. - // - block task (remove wakeup) - // - task switch (let something else run) - // - repeat until all words read - // that is, the microcode expects to be woken up on a per-word basis, and it only reads in one word - // per wakeup. + // Spin the disk platter and read in words as applicable. // - - - if (!_wdInhib) - { - - _elapsedSectorStateTime++; - - switch (_sectorState) - { - case SectorState.HeaderReadDelay: - if (_sectorWordIndex > 19) - { - _sectorState = SectorState.Header; - _sectorWordIndex = 0; - Console.WriteLine("Switching to HeaderPreamble state."); - _kData = 1; - } - else if (_elapsedSectorStateTime > _wordDuration) - { - _elapsedSectorStateTime -= _wordDuration; - - _sectorWordIndex++; - - _kData = 0xfefe; // unused, just for debugging - - _system.CPU.WakeupTask(CPU.TaskType.DiskWord); - Console.WriteLine("delay wakeup"); - - } - break; - - case SectorState.HeaderPreamble: - if (_sectorWordIndex > 32) - { - _sectorState = SectorState.Header; - _sectorWordIndex = 0; - Console.WriteLine("Switching to Header state."); - _kData = 2; - } - else if (_elapsedSectorStateTime > _wordDuration) - { - _elapsedSectorStateTime -= _wordDuration; - - _sectorWordIndex++; - - _kData = 0xfeff; // unused, just for debugging - _system.CPU.WakeupTask(CPU.TaskType.DiskWord); - Console.WriteLine("preamble wakeup"); - } - break; - - case SectorState.Header: - if (_sectorWordIndex > 2) // two words + sync - { - //_elapsedSectorStateTime -= 2.0 * _wordDuration; - _sectorState = SectorState.HeaderInterrecord; - _sectorWordIndex = 0; - Console.WriteLine("Switching to HeaderGap state."); - _kData = 3; - } - else if (_elapsedSectorStateTime > _wordDuration) - { - _elapsedSectorStateTime -= _wordDuration; - - // Put next word into KDATA if not inhibited from doing so. - if (!_xferOff) - { - _kData = 0xdead; // placeholder - Console.WriteLine(" Header word {0} is {1}", _sectorWordIndex, OctalHelpers.ToOctal(_kData)); - } - _sectorWordIndex++; - - if (!_wdInhib) - { - Console.WriteLine("header wakeup"); - _system.CPU.WakeupTask(CPU.TaskType.DiskWord); - } - } - break; - - case SectorState.HeaderInterrecord: - if (_elapsedSectorStateTime > _interRecordDelay) - { - _elapsedSectorStateTime -= _interRecordDelay; - _sectorState = SectorState.Label; - Console.WriteLine("Switching to Label state."); - _kData = 4; - } - break; - - case SectorState.Label: - if (_sectorWordIndex > 8) // eight words + sync - { - //_elapsedSectorStateTime -= 8.0 * _wordDuration; - _sectorState = SectorState.LabelInterrecord; - _sectorWordIndex = 0; - Console.WriteLine("Switching to LabelGap state."); - _kData = 5; - } - else if (_elapsedSectorStateTime > _wordDuration) - { - _elapsedSectorStateTime -= _wordDuration; - // Put next word into KDATA if not inhibited from doing so. - if (!_xferOff) - { - _kData = 0xbeef; // placeholder - Console.WriteLine(" Label word {0} is {1}", _sectorWordIndex, OctalHelpers.ToOctal(_kData)); - } - _sectorWordIndex++; - - if (!_wdInhib) - { - _system.CPU.WakeupTask(CPU.TaskType.DiskWord); - } - } - break; - - case SectorState.LabelInterrecord: - if (_elapsedSectorStateTime > _interRecordDelay) - { - _elapsedSectorStateTime -= _interRecordDelay; - _sectorState = SectorState.Data; - Console.WriteLine("Switching to Data state."); - _kData = 6; - } - break; - - case SectorState.Data: - if (_sectorWordIndex > 256) // 256 words + sync - { - //_elapsedSectorStateTime -= 256.0 * _wordDuration; - _sectorState = SectorState.Postamble; - _sectorWordIndex = 0; - Console.WriteLine("Switching to Leadout state."); - _kData = 7; - } - else if (_elapsedSectorStateTime > _wordDuration) - { - _elapsedSectorStateTime -= _wordDuration; - // Put next word into KDATA if not inhibited from doing so. - if (!_xferOff) - { - _kData = 0xda1a; // placeholder - Console.WriteLine(" Sector word {0} is {1}", _sectorWordIndex, OctalHelpers.ToOctal(_kData)); - } - _sectorWordIndex++; - - if (!_wdInhib) - { - _system.CPU.WakeupTask(CPU.TaskType.DiskWord); - } - } - break; - - case SectorState.Postamble: - // Just stay here forever. We will get reset at the start of the next sector. - break; - - } - } + SpinDisk(); // // Update the WDINIT signal; this is based on WDALLOW (!_wdInhib) which sets WDINIT (this is done @@ -488,6 +321,193 @@ namespace Contralto.IO 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; + + Console.WriteLine("Sector Word {0}:{1}", _sectorWordIndex, OctalHelpers.ToOctal(diskWord)); + + 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) + { + Console.WriteLine("Disk Word task wakeup due to word clock."); + 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) + { + Console.WriteLine("KDATA loaded."); + _kData = diskWord; + } + + if (!_wdInhib) + { + Console.WriteLine("Disk Word task wakeup due to word read."); + 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) + { + Console.WriteLine("Sync word hit; starting bit clock for next word"); + _diskBitCounterEnable = true; + } + + if (bWakeup) + { + _system.CPU.WakeupTask(CPU.TaskType.DiskWord); + } + + // Last, move to the next word. + _sectorWordIndex++; + } + } + + private void LoadSector() + { + // Fill in sector with test data; eventually actually load real disk data! + + // Header (2 words data, 1 word cksum) + for (int i = _headerOffset + 1; i < _headerOffset + 3; i++) + { + // actual data to be loaded from disk / cksum calculated + _sectorData[i] = new DataCell(0xbeef, CellType.Data); + } + + _sectorData[_headerOffset + 3].Data = CalculateChecksum(_sectorData, _headerOffset + 1, 2); + + // Label (8 words data, 1 word cksum) + for (int i = _labelOffset + 1; i < _labelOffset + 9; i++) + { + // actual data to be loaded from disk / cksum calculated + _sectorData[i] = new DataCell(0xdead, CellType.Data); + } + + _sectorData[_labelOffset + 9].Data = CalculateChecksum(_sectorData, _labelOffset + 1, 8); + + // sector data (256 words data, 1 word cksum) + for (int i = _dataOffset + 1; i < _dataOffset + 257; i++) + { + // actual data to be loaded from disk / cksum calculated + _sectorData[i] = new DataCell((ushort)(0x7000 + i), CellType.Data); + } + + _sectorData[_dataOffset + 257].Data = CalculateChecksum(_sectorData, _dataOffset + 1, 256); + + } + + private void InitSector() + { + // Fill in sector with default data (basically, fill in non-data areas). + + + // + // header delay, 22 words + for (int i=0; i < _headerOffset; i++) + { + _sectorData[i] = new DataCell(0, CellType.Gap); + } + + _sectorData[_headerOffset] = new DataCell(1, CellType.Sync); + // inter-reccord delay between header & label (10 words) + for (int i = _headerOffset + 4; i < _labelOffset; i++) + { + _sectorData[i] = new DataCell(0, CellType.Gap); + } + + _sectorData[_labelOffset] = new DataCell(1, CellType.Sync); + // inter-reccord delay between label & data (10 words) + for (int i = _labelOffset + 10; i < _dataOffset; i++) + { + _sectorData[i] = new DataCell(0, CellType.Gap); + } + + _sectorData[_dataOffset] = new DataCell(1, CellType.Sync); + // read-postamble + for (int i = _dataOffset + 257; i < _sectorWords;i++) + { + _sectorData[i] = new DataCell(0, CellType.Gap); + } + } + + private ushort CalculateChecksum(DataCell[] sectorData, int offset, int length) + { + // + // From the uCode, the Alto's checksum algorithm is: + // 1. Load checksum with constant value of 521B (0x151) + // 2. For each word in the record, cksum <- word XOR cksum + // 3. Profit + // + ushort checksum = 0x151; + + for(int i = offset; i < length;i++) + { + // Sanity check that we're checksumming actual data + if (sectorData[i].Type != CellType.Data) + { + throw new InvalidOperationException("Attempt to checksum non-data area of sector."); + } + + checksum = (ushort)(checksum ^ sectorData[i].Data); + } + + return checksum; + } + private ushort _kData; private ushort _kAdr; private ushort _kCom; @@ -515,6 +535,9 @@ namespace Contralto.IO private int _head; private int _sector; + // bit clock flag + private bool _diskBitCounterEnable; + // WDINIT signal private bool _wdInit; @@ -523,37 +546,54 @@ namespace Contralto.IO private const double _sectorDuration = (40.0 / 12.0); // time in msec for one sector private const double _sectorClocks = _sectorDuration / 0.00017; // number of clock cycles per sector time. - // Sector data timing and associated state. Timings based on educated guesses at the moment. - private enum SectorState - { - HeaderReadDelay = 0, // gap between sector mark and first Header word - HeaderPreamble, - Header, // Header; two words - HeaderInterrecord,// gap between end of Header and first Label word - Label, // Label; 8 words - LabelInterrecord, // gap betweeen the end of Label and first Data word - Data, // Data; 256 words - Postamble // gap between the end of Data and the next sector mark - } - private SectorState _sectorState; - private double _elapsedSectorStateTime; + private int _sectorWordIndex; + private double _sectorWordTime; - // From altoconsts23.mu: + // From altoconsts23.mu: [all constants in octal, for reference] // $MFRRDL $177757; DISK HEADER READ DELAY IS 21 WORDS - // $MFR0BL $177744; DISK HEADER PREAMBLE IS 34 WORDS + // $MFR0BL $177744; DISK HEADER PREAMBLE IS 34 WORDS <<-- used for writing // $MIRRDL $177774; DISK INTERRECORD READ DELAY IS 4 WORDS - // $MIR0BL $177775; DISK INTERRECORD PREAMBLE IS 3 WORDS + // $MIR0BL $177775; DISK INTERRECORD PREAMBLE IS 3 WORDS <<-- writing // $MRPAL $177775; DISK READ POSTAMBLE LENGTH IS 3 WORDS - // $MWPAL $177773; DISK WRITE POSTAMBLE LENGTH IS 5 WORDS - private const double _wordDuration = (_sectorClocks / (266.0 + 21 + 34 + (4 + 3) * 3)); // Based on : 266 words / sector, + X words for delay / preamble - private const double _headerReadDelay = (_wordDuration * 21); - private const double _headerPreamble = (_wordDuration * 34); - private const double _interRecordDelay = (_wordDuration * 4); - private const double _interRecordPreamble = (_wordDuration * 3); + // $MWPAL $177773; DISK WRITE POSTAMBLE LENGTH IS 5 WORDS <<-- writing, clearly. + private const int _sectorWords = 269 + 22 + 34; // Based on : 269 data words (+ cksums) / sector, + X words for delay / preamble / sync + private const double _wordDuration = (_sectorClocks / (double)_sectorWords); + private const double _headerReadDelay = 17; + private const double _interRecordDelay = 4; + // offsets in words for start of data in sector + private const int _headerOffset = 22; + private const int _labelOffset = _headerOffset + 14; + private const int _dataOffset = _labelOffset + 20; + // The data for the current sector + private enum CellType + { + Data, + Gap, + Sync, + } + + private struct DataCell + { + public DataCell(ushort data, CellType type) + { + Data = data; + Type = type; + } + + public ushort Data; + public CellType Type; + + public override string ToString() + { + return String.Format("{0} {1}", Data, Type); + } + } + + private DataCell[] _sectorData = new DataCell[_sectorWords]; // Cylinder seek timing. Again, see the manual. diff --git a/Contralto/Memory/Memory.cs b/Contralto/Memory/Memory.cs index 00801f6..2ed1ee6 100644 --- a/Contralto/Memory/Memory.cs +++ b/Contralto/Memory/Memory.cs @@ -20,6 +20,7 @@ namespace Contralto.Memory public void Load(int address, ushort data) { + Console.WriteLine("wrote {0} to {1}", OctalHelpers.ToOctal(data), OctalHelpers.ToOctal(address)); _mem[address] = data; }