mirror of
https://github.com/livingcomputermuseum/ContrAlto.git
synced 2026-01-25 19:55:57 +00:00
1101 lines
43 KiB
C#
1101 lines
43 KiB
C#
using Contralto.CPU;
|
|
using Contralto.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Contralto.IO
|
|
{
|
|
/// <summary>
|
|
/// Implements the Alto's Trident disk controller ("Tricon").
|
|
/// This can talk to up to 8 (technically 16, but the software doesn't
|
|
/// appear to support more than 8) T-80 (80mb) or T-300 (300mb) drives.
|
|
///
|
|
/// TODO: This implementation is preliminary and still experimental.
|
|
/// There are a few major issues:
|
|
/// - The state machine for the Output FIFO is a tangled mess.
|
|
/// - TFU refuses to talk to more than drive 0. Unsure if this is
|
|
/// a bug in the Trident emulation, a bug elsewhere in Contralto
|
|
/// or user error. TriEx seems to be fine.
|
|
/// - TriEx gets many errors of status "00000" (meaning that TriEx
|
|
/// didn't get a response from the controller when it expected to.)
|
|
/// TFU works fine and can certify, erase, and exercise packs all day.
|
|
/// The TriEx issue seems to be related in some way to the timing of
|
|
/// the output FIFO. Probably a subtle issue with microcode wakeups.
|
|
/// Untangle the output FIFO state machine and revisit this.
|
|
/// - There is at this time no way to toggle the Read Only switch
|
|
/// on the emulated drive. (All drives are read/write).
|
|
/// - The Trident sector wakeup signal needs to go away when the TriCon
|
|
/// is put to sleep (for performance reasons, no sense keeping the
|
|
/// scheduler busy for no reason.)
|
|
/// </summary>
|
|
public class TridentController
|
|
{
|
|
public TridentController(AltoSystem system)
|
|
{
|
|
_system = system;
|
|
|
|
//
|
|
// We initialize 16 drives even though the
|
|
// controller only technically supports 8.
|
|
// TODO: detail
|
|
//
|
|
_drives = new TridentDrive[16];
|
|
|
|
for(int i=0;i<_drives.Length;i++)
|
|
{
|
|
_drives[i] = new TridentDrive(system);
|
|
}
|
|
|
|
Reset();
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_runEnable = false;
|
|
|
|
_seekIncomplete = false;
|
|
_headOverflow = false;
|
|
_deviceCheck = false;
|
|
_notSelected = false;
|
|
_sectorOverflow = false;
|
|
_outputLate = false;
|
|
_inputLate = false;
|
|
_compareError = false;
|
|
_readOnly = false;
|
|
_offset = false;
|
|
|
|
_selectedDrive = 0;
|
|
|
|
_sector = 0;
|
|
_checkDone = false;
|
|
_empty = false;
|
|
_pauseOutputProcessing = false;
|
|
_readState = ReadState.Idle;
|
|
_writeState = WriteState.Idle;
|
|
|
|
_outputFifo = new Queue<int>();
|
|
_inputFifo = new Queue<ushort>();
|
|
|
|
_sectorEvent = new Event(0, null, SectorCallback);
|
|
_outputFifoEvent = new Event(0, null, OutputFifoCallback);
|
|
_readWordEvent = new Event(0, null, ReadWordCallback);
|
|
|
|
// And schedule the first sector pulse.
|
|
_system.Scheduler.Schedule(_sectorEvent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// "RESET INPUT FIFO AND CLEAR ERRORS"
|
|
/// </summary>
|
|
public void ControllerReset()
|
|
{
|
|
_seekIncomplete = false;
|
|
_headOverflow = false;
|
|
_deviceCheck = false;
|
|
_notSelected = false;
|
|
_sectorOverflow = false;
|
|
_outputLate = false;
|
|
_inputLate = false;
|
|
_compareError = false;
|
|
_readOnly = false;
|
|
_offset = false;
|
|
|
|
_writeState = WriteState.Idle;
|
|
_readState = ReadState.Idle;
|
|
|
|
_empty = false;
|
|
//_pauseOutputProcessing = false;
|
|
|
|
ClearInputFIFO();
|
|
}
|
|
|
|
public void CommitDisk(int drive)
|
|
{
|
|
TridentDrive d = _drives[drive];
|
|
if (d.IsLoaded)
|
|
{
|
|
try
|
|
{
|
|
d.Pack.Save();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// TODO: this does not really belong here.
|
|
System.Windows.Forms.MessageBox.Show(String.Format("Unable to save Trident disk {0}'s contents. Error {1}. Any changes have been lost.", drive, e.Message), "Disk save error");
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public TridentDrive[] Drives
|
|
{
|
|
get { return _drives; }
|
|
}
|
|
|
|
public void STARTF(ushort value)
|
|
{
|
|
//
|
|
// "An SIO with bit 10 set will cause Run-Enable to be set.
|
|
// An SIO with bit 11 set to one will cause Run-Enable to be
|
|
// reset."
|
|
//
|
|
if ((value & 0x10) != 0)
|
|
{
|
|
_runEnable = false;
|
|
_system.CPU.BlockTask(TaskType.TridentInput);
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
|
|
//ClearOutputFIFO();
|
|
//ClearInputFIFO();
|
|
}
|
|
|
|
if ((value & 0x20) != 0)
|
|
{
|
|
_runEnable = true;
|
|
|
|
// "Issuing an SIO with bit 10 set will wake up the microcode
|
|
// once and thus may report status in the absence of sector
|
|
// pulses from the disk."
|
|
// So do that now.
|
|
// Based on a reading of the microcode and schematics it looks like the
|
|
// Write (Output) task is woken up here.
|
|
|
|
//
|
|
// Clear error flags.
|
|
//
|
|
_inputLate = false;
|
|
_outputLate = false;
|
|
_sectorOverflow = false;
|
|
|
|
// _empty = false;
|
|
// _pauseOutputProcessing = false;
|
|
|
|
//
|
|
// Clear the output FIFO
|
|
ClearOutputFIFO();
|
|
ClearInputFIFO();
|
|
|
|
//_system.CPU.WakeupTask(TaskType.TridentInput);
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
|
|
//_system.CPU.BlockTask(TaskType.TridentInput);
|
|
//_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
|
|
}
|
|
|
|
Log.Write(LogComponent.TridentController, "Trident STARTF {0}", Conversion.ToOctal(value));
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
_runEnable = false;
|
|
_system.CPU.BlockTask(TaskType.TridentInput);
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
}
|
|
|
|
public ushort KDTA
|
|
{
|
|
get
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Trident KDTA read");
|
|
return DequeueInputFIFO();
|
|
}
|
|
|
|
set
|
|
{
|
|
Log.Write(LogComponent.TridentController, "------> Trident KDTA queued {0}", Conversion.ToOctal(value));
|
|
WriteOutputFifo(value);
|
|
}
|
|
}
|
|
|
|
public ushort STATUS
|
|
{
|
|
get
|
|
{
|
|
//
|
|
// The status bits from the controller are:
|
|
// 0 - Seek Incomplete : drive was unable to correctly position the heads, a Rezero
|
|
// must be issued to clear this error.
|
|
// 1 - Head Overflow : head address given to the drive is invalid.
|
|
// 2 - Device check : One of the following errors occurred
|
|
// a) Head select or Cylinder select or Write commands and disk not ready
|
|
// b) An illegal cylinder address.
|
|
// c) Offset active and cylinder select command.
|
|
// d) Read-Only and write
|
|
// e) Certain errors during writing -- multiple heads selected, various read errors)
|
|
// 3 - Not selected : The selected drive is off-line or not powered up.
|
|
// 4 - Not Online : The drive is in test mode or the heads are not loaded.
|
|
// 5 - Not Ready : There is a cylinder seek in progress or the heads are not loaded
|
|
// 6 - Sector Overflow : The controller detected a write command was active when the next
|
|
// sector pulse occurred.
|
|
// 7 - Output Late : The 16 word output buffer became empty while a read or write command
|
|
// was in progress.
|
|
// 8 - Input Late : The 16 word input buffer became full.
|
|
// 9 - Compare Error : The data read during a "Read and Compare" operation did not match
|
|
// the data read off the disk.
|
|
// 10 - Read Only : The "Read-Only" switch on the disk drive is on.
|
|
// 11 - Offset : The cylinder position is currently offset. This is a mode used for
|
|
// recovery of bad data. (NOTE: Not emulated.)
|
|
// 12-15 Sector count : A value from 0 to count-1, where count is the number of sectors
|
|
// implemented in the disk drive. The value returned here is the sector
|
|
// count for the *next sector* on the disk.
|
|
|
|
ushort status = (ushort)(
|
|
(_seekIncomplete ? 0x8000 : 0) |
|
|
(_headOverflow ? 0x4000 : 0) |
|
|
(_deviceCheck ? 0x2000 : 0) |
|
|
(_notSelected ? 0x1000 : 0) |
|
|
(NotOnline() ? 0x0800 : 0) |
|
|
(NotReady() ? 0x0400 : 0) |
|
|
(_sectorOverflow ? 0x0200 : 0) |
|
|
(_outputLate ? 0x0100 : 0) |
|
|
(_inputLate ? 0x0080 : 0) |
|
|
(_compareError ? 0x0040 : 0) |
|
|
(SelectedDrive.ReadOnly ? 0x0020 : 0) |
|
|
(_offset ? 0x0010 : 0) |
|
|
((_sector) & 0x000f));
|
|
|
|
Log.Write(LogComponent.TridentController, "STATUS word is {0}", Conversion.ToOctal(status));
|
|
return status;
|
|
}
|
|
}
|
|
|
|
public void TagInstruction(ushort tag)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "------> Trident tag instruction queued {0}", Conversion.ToOctal(tag));
|
|
|
|
//
|
|
// Add tag to the output FIFO, with bit 16 set, identifying it as a tag.
|
|
//
|
|
WriteOutputFifo(0x10000 | tag);
|
|
}
|
|
|
|
public void WaitForEmpty()
|
|
{
|
|
// Block the Output task; it will be awoken when the output FIFO is emptied.
|
|
if (_outputFifo.Count > 0)
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
_empty = true;
|
|
Log.Write(LogComponent.TridentController, "Trident output task blocked until Output FIFO emptied.");
|
|
}
|
|
}
|
|
|
|
private bool NotReady()
|
|
{
|
|
return SelectedDrive.NotReady |
|
|
!SelectedDrive.IsLoaded |
|
|
_selectedDrive > 7;
|
|
}
|
|
|
|
private bool NotOnline()
|
|
{
|
|
return _selectedDrive < 8 ? !SelectedDrive.IsLoaded : true;
|
|
}
|
|
|
|
private void WriteOutputFifo(int value)
|
|
{
|
|
if (_outputFifo.Count > 16)
|
|
{
|
|
Log.Write(LogType.Error, LogComponent.TridentController, "Output FIFO full, dropping word.");
|
|
return;
|
|
}
|
|
|
|
EnqueueOutputFIFO(value);
|
|
|
|
//
|
|
// If this is the first word written to an empty FIFO, and we aren't waiting for the next
|
|
// sector pulse, queue up the fifo callback to pull words and process them.
|
|
//
|
|
if (!_pauseOutputProcessing && _outputFifo.Count == 1)
|
|
{
|
|
_outputFifoEvent.TimestampNsec = _outputFifoDuration;
|
|
_system.Scheduler.Schedule(_outputFifoEvent);
|
|
}
|
|
}
|
|
|
|
private void OutputFifoCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
ProcessCommandOutput();
|
|
}
|
|
|
|
private void ProcessCommandOutput()
|
|
{
|
|
if (_outputFifo.Count == 0)
|
|
{
|
|
if (_writeState == WriteState.Writing)
|
|
{
|
|
//
|
|
// We have run out of data on a write.
|
|
//
|
|
_outputLate = true;
|
|
|
|
Log.Write(LogComponent.TridentController, "Output FIFO underflowed.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (_readState != ReadState.Reading)
|
|
{
|
|
int fifoWord = DequeueOutputFIFO();
|
|
|
|
if ((fifoWord & 0x10000) != 0)
|
|
{
|
|
//
|
|
// This is a Tag word; process it accordingly.
|
|
//
|
|
Log.Write(LogComponent.TridentController, "Tag word {0} pulled from Output FIFO.", Conversion.ToOctal((ushort)fifoWord));
|
|
|
|
//
|
|
// From the TRICON schematic (page 16):
|
|
// Bit 3 is the Enable bit, which enables selection of one of four commands
|
|
// to the drive, specified in bits 1 and 2:
|
|
// (and as usual, these are in the backwards Nova/Alto bit ordering scheme)
|
|
// Bits 1 and 2 decode to:
|
|
// 0 0 - Control
|
|
// 0 1 - Set Head
|
|
// 1 0 - Set Cylinder
|
|
// 1 1 - Set Drive
|
|
//
|
|
// Head, Cylinder, Drive are straightforward -- the lower bits of the tag
|
|
// word contain the data for the command.
|
|
//
|
|
// The Control bits are described in the Trident T300 Theory of Operations manual,
|
|
// Page 3-13 and are the lower 10 bits of the tag command:
|
|
//
|
|
// 0 - Strobe Late : Skews read data detection 4ns late for attempted read-error recovery.
|
|
// 1 - Strobe Early : Same as above, but early.
|
|
// 2 - Write : Turns on circuits to write data,
|
|
// 3 - Read : Turns on read circuits an resets Attention interrupts.
|
|
// 4 - Address Mark : Commands an address mark to be generated, if writing; or
|
|
// enables the address mark detector, if reading.
|
|
// 5 - Reset Head Register : Resets HAR to 0
|
|
// 6 - Device Check Reset : Resets most types of Device Check errors unless an error
|
|
// condition is still present.
|
|
// 7 - Head Select : Tuns on the head-selection circuits. Head select must be active 5
|
|
// or 15 microseconds before Write or Read is commanded, respectively.
|
|
// 8 - Rezero : Repositions the heads to cylinder 000, selects Head Address 0, and resets
|
|
// some types of Device Checks.
|
|
// 9 - Head Advance : Increases Head Address count by one.
|
|
//
|
|
//
|
|
// Bit 0 of the Tag word, if set, tells the controller to hold off Output FIFO processing
|
|
// until the next sector pulse.
|
|
//
|
|
if ((fifoWord & 0x8000) != 0)
|
|
{
|
|
_pauseOutputProcessing = true;
|
|
Log.Write(LogComponent.TridentController, "Output FIFO processing paused until next sector pulse.");
|
|
}
|
|
// See if the enable bit (3) is set, in which case this is a command to the drive
|
|
//
|
|
if ((fifoWord & 0x1000) != 0)
|
|
{
|
|
//
|
|
// Switch on the specific command
|
|
switch ((fifoWord & 0x6000) >> 13)
|
|
{
|
|
case 0: // Control
|
|
Log.Write(LogComponent.TridentController, "Control word.");
|
|
|
|
if ((fifoWord & 0x0001) != 0) // 9 - Head advance
|
|
{
|
|
if (!SelectedDrive.IsLoaded)
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
else
|
|
{
|
|
if (SelectedDrive.Head + 1 >= SelectedDrive.Pack.Geometry.Heads)
|
|
{
|
|
_headOverflow = true;
|
|
_deviceCheck = true;
|
|
|
|
Log.Write(LogComponent.TridentController, "Head {0} is out of range on Head Advance.", SelectedDrive.Head + 1);
|
|
}
|
|
else
|
|
{
|
|
SelectedDrive.Head++;
|
|
Log.Write(LogComponent.TridentController, "Control: Head Advance. Head is now {0}", SelectedDrive.Head);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((fifoWord & 0x0002) != 0) // 8 - Rezero
|
|
{
|
|
_deviceCheck = false;
|
|
SelectedDrive.Head = 0;
|
|
|
|
InitSeek(0);
|
|
|
|
Log.Write(LogComponent.TridentController, "Control: Rezero.");
|
|
}
|
|
|
|
if ((fifoWord & 0x0004) != 0) // 7 - Head select
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Head Select.");
|
|
|
|
if (!SelectedDrive.IsLoaded)
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
|
|
// TODO: technically this needs to be active before a write or read is selected. Do I care?
|
|
}
|
|
|
|
if ((fifoWord & 0x0008) != 0) // 6 - Device Check Reset
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Device Check Reset.");
|
|
_deviceCheck = false;
|
|
}
|
|
|
|
if ((fifoWord & 0x0010) != 0) // 5 - Reset Head Register
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Reset Head Register.");
|
|
SelectedDrive.Head = 0;
|
|
}
|
|
|
|
if ((fifoWord & 0x0020) != 0) // 4 - Address Mark
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Address mark.");
|
|
|
|
// Not much to do here, emulation-wise.
|
|
}
|
|
|
|
if ((fifoWord & 0x0040) != 0) // 3 - Read
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Read.");
|
|
//
|
|
// Commence reading -- start reading a word at a time into the input FIFO,
|
|
// Waking up the Input task as necessary.
|
|
//
|
|
if (NotReady())
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
else
|
|
{
|
|
InitRead();
|
|
}
|
|
}
|
|
|
|
if ((fifoWord & 0x0080) != 0) // 2 - Write
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Write.");
|
|
|
|
//
|
|
// Commence writing -- start pulling a word at a time out of the output FIFO,
|
|
// Waking up the Output task as necessary.
|
|
//
|
|
if (NotReady())
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
else
|
|
{
|
|
InitWrite();
|
|
}
|
|
}
|
|
|
|
if ((fifoWord & 0x0100) != 0) // 1 - Strobe Early
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Strobe Early.");
|
|
|
|
// Not going to emulate this, as fun as it sounds.
|
|
}
|
|
|
|
if ((fifoWord & 0x0200) != 0) // 0 - Strobe Late
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Control: Strobe Late.");
|
|
|
|
// Not going to emulate this either.
|
|
}
|
|
|
|
break;
|
|
|
|
case 1: // Set Head
|
|
int head = fifoWord & 0x1f; // low 5 bits
|
|
Log.Write(LogComponent.TridentController, "Command is Set Head {0}", head);
|
|
|
|
if (!SelectedDrive.IsLoaded)
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
else
|
|
{
|
|
if (head >= SelectedDrive.Pack.Geometry.Heads)
|
|
{
|
|
_headOverflow = true;
|
|
_deviceCheck = true;
|
|
|
|
Log.Write(LogComponent.TridentController, "Head {0} is out of range.", head);
|
|
}
|
|
else
|
|
{
|
|
SelectedDrive.Head = head;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 2: // Set Cylinder
|
|
int cyl = fifoWord & 0x3ff; // low 10 bits
|
|
Log.Write(LogComponent.TridentController, "Command is Set Cylinder {0}.", cyl);
|
|
|
|
if (NotReady())
|
|
{
|
|
_deviceCheck = true;
|
|
}
|
|
else
|
|
{
|
|
InitSeek(cyl);
|
|
}
|
|
break;
|
|
|
|
case 3: // Set Drive
|
|
// We take all four drive-select bits even though only 8 drives are actually supported.
|
|
// The high bit is used by many trident utilities to select an invalid drive to test for
|
|
// the presence of the 8-drive multiplexer.
|
|
_selectedDrive = fifoWord & 0xf;
|
|
|
|
if (_selectedDrive > 7)
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
_notSelected = true;
|
|
}
|
|
else
|
|
{
|
|
_notSelected = false;
|
|
}
|
|
|
|
Log.Write(LogComponent.TridentController, "Command is Set Drive {0}", _selectedDrive);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// There are two values we expect to see with the enable bit off:
|
|
// 0 is sent to reset the tag bus (to the drive) to zeros, we simply ignore it.
|
|
// 2 is sent because the drive expects the Rezero signal to be held for a time
|
|
// after the Control signal is dropped. We will commence a Rezero operation
|
|
// if we get one here.
|
|
//
|
|
if (fifoWord == 2)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Rezero started.");
|
|
}
|
|
else if (fifoWord != 0)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Unexpected Tag word without enable bit set: {0}", Conversion.ToOctal(fifoWord));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// This is a data word.
|
|
//
|
|
Log.Write(LogComponent.TridentController, "Data word {0} pulled from Output FIFO.", Conversion.ToOctal((ushort)fifoWord));
|
|
|
|
if (_writeState != WriteState.Idle)
|
|
{
|
|
switch (_writeState)
|
|
{
|
|
case WriteState.Initialized:
|
|
// This word should be the MAX number of words to check.
|
|
_writeWordCount = fifoWord - 1;
|
|
_writeState = WriteState.WaitingForStartBit;
|
|
Log.Write(LogComponent.TridentController, "Write initializing {0} words (max) to write.", _writeWordCount);
|
|
break;
|
|
|
|
case WriteState.WaitingForStartBit:
|
|
if (fifoWord == 1)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Start bit recognized, commencing write operation.");
|
|
_writeState = WriteState.Writing;
|
|
}
|
|
break;
|
|
|
|
case WriteState.Writing:
|
|
ProcessDiskWrite(fifoWord);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_readState != ReadState.Idle)
|
|
{
|
|
switch (_readState)
|
|
{
|
|
case ReadState.Initialized:
|
|
_readWordCount = fifoWord - 1;
|
|
_readState = ReadState.Reading;
|
|
|
|
Log.Write(LogType.Verbose, LogComponent.TridentController, "Read initializing {0} words (max) to read.", _readWordCount);
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ReadState in ProcessCommandOutput.");
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!_pauseOutputProcessing && _outputFifo.Count > 0)
|
|
{
|
|
//
|
|
// Schedule next FIFO wakeup.
|
|
_outputFifoEvent.TimestampNsec = _outputFifoDuration;
|
|
_system.Scheduler.Schedule(_outputFifoEvent);
|
|
}
|
|
}
|
|
|
|
private void InitSeek(int destCylinder)
|
|
{
|
|
_deviceCheck = !SelectedDrive.Seek(destCylinder);
|
|
}
|
|
|
|
private void InitRead()
|
|
{
|
|
if (_readState == ReadState.Idle)
|
|
{
|
|
_readState = ReadState.Initialized;
|
|
_checkedWordCount = 0;
|
|
_readIndex = 0;
|
|
_readWordCount = 0;
|
|
_checkDone = false;
|
|
|
|
// Clear the FIFO: TODO check schematics.
|
|
// ClearInputFIFO();
|
|
|
|
_readWordEvent.TimestampNsec = _readWordDuration * 10;
|
|
_system.Scheduler.Schedule(_readWordEvent);
|
|
}
|
|
else
|
|
{
|
|
// Unexpected, throw for now.
|
|
throw new InvalidOperationException("Unexpected Read command while read active.");
|
|
}
|
|
}
|
|
|
|
private void ReadWordCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
if (_readWordCount > 0)
|
|
{
|
|
// Enqueue data from disk.
|
|
ushort dataWord;
|
|
|
|
switch (_sectorBlock)
|
|
{
|
|
case SectorBlock.Header:
|
|
dataWord = SelectedDrive.ReadHeader(_readIndex);
|
|
break;
|
|
|
|
case SectorBlock.Label:
|
|
dataWord = SelectedDrive.ReadLabel(_readIndex);
|
|
break;
|
|
|
|
case SectorBlock.Data:
|
|
dataWord = SelectedDrive.ReadData(_readIndex);
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidOperationException(String.Format("Unexpected Sector Block of {0} on read.", _sectorBlock));
|
|
}
|
|
|
|
Log.Write(LogType.Verbose, LogComponent.TridentController, "Read word {0}:{1}", _readIndex, Conversion.ToOctal(dataWord));
|
|
|
|
EnqueueInputFIFO(dataWord);
|
|
|
|
//
|
|
// Compare data with check data in output fifo, if any.
|
|
// From the microcode comments:
|
|
// "Note that the first two words of a block are always checked,
|
|
// followed by additional words until a zero word or the end of the block."
|
|
//
|
|
// If we hit a tag word, the check is also completed.
|
|
// TODO: verify this w/schematic & microcode.
|
|
|
|
if (_outputFifo.Count > 0)
|
|
{
|
|
int checkWord = _outputFifo.Peek();
|
|
if ((checkWord & 0x10000) == 0 && (!_checkDone || _checkedWordCount < 2))
|
|
{
|
|
// Actually pull the word off
|
|
checkWord = (ushort)DequeueOutputFIFO();
|
|
|
|
Log.Write(LogType.Verbose, LogComponent.TridentController, "Pulled checkword {0} from output FIFO", Conversion.ToOctal(checkWord));
|
|
|
|
// A zero word indicates the check is complete (we will
|
|
// still read the minimum two words in)
|
|
if (checkWord == 0)
|
|
{
|
|
_checkDone = true;
|
|
}
|
|
// Compare didn't.
|
|
else if (checkWord != dataWord)
|
|
{
|
|
_compareError = true;
|
|
}
|
|
_checkedWordCount++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Last four words (0 through -3) are checksum and ECC words.
|
|
// The checksum words are ignored by the microcode, and
|
|
// since we're not simulating faulty disks, these are always zero.
|
|
EnqueueInputFIFO(0);
|
|
Log.Write(LogComponent.TridentController, "Read ECC/checksum word 0");
|
|
}
|
|
|
|
_readIndex++;
|
|
_readWordCount--;
|
|
|
|
if (_readWordCount > -4)
|
|
{
|
|
// More words to read, queue up the next.
|
|
_readWordEvent.TimestampNsec = _readWordDuration;
|
|
_system.Scheduler.Schedule(_readWordEvent);
|
|
}
|
|
else
|
|
{
|
|
Log.Write(LogComponent.TridentController, "CHS {0}/{1}/{2} {3} read from drive {4} complete.", SelectedDrive.Cylinder, SelectedDrive.Head, _sector, _sectorBlock, _selectedDrive);
|
|
_readState = ReadState.Idle;
|
|
|
|
_sectorBlock++;
|
|
}
|
|
}
|
|
|
|
private void InitWrite()
|
|
{
|
|
if (_writeState == WriteState.Idle)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Write primed, waiting for start bit.");
|
|
_writeState = WriteState.Initialized;
|
|
_writeIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
// Unexpected, throw for now.
|
|
throw new InvalidOperationException("Unexpected Write command while write active.");
|
|
}
|
|
}
|
|
|
|
private void ProcessDiskWrite(int dataWord)
|
|
{
|
|
//
|
|
// Sanity check: if there's a tag bit set, we've picked up some
|
|
// invalid data...
|
|
//
|
|
if ((dataWord & 0x10000) != 0)
|
|
{
|
|
Log.Write(LogType.Error, LogComponent.TridentController, "Tag bit set in data word during Write ({0}).", Conversion.ToOctal(dataWord));
|
|
}
|
|
|
|
Log.Write(LogComponent.TridentController, "Write word {0}:{1}", _writeIndex, Conversion.ToOctal(dataWord));
|
|
|
|
//
|
|
// Commit to the proper block in the sector:
|
|
//
|
|
switch(_sectorBlock)
|
|
{
|
|
case SectorBlock.Header:
|
|
SelectedDrive.WriteHeader(_writeIndex, (ushort)dataWord);
|
|
break;
|
|
|
|
case SectorBlock.Label:
|
|
SelectedDrive.WriteLabel(_writeIndex, (ushort)dataWord);
|
|
break;
|
|
|
|
case SectorBlock.Data:
|
|
SelectedDrive.WriteData(_writeIndex, (ushort)dataWord);
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidOperationException(String.Format("Unexpected Sector Block of {0} on write.", _sectorBlock));
|
|
}
|
|
|
|
_writeIndex++;
|
|
|
|
_writeWordCount--;
|
|
if (_writeWordCount <= 0)
|
|
{
|
|
Console.WriteLine( "CHS {0}/{1}/{2} {3} write to drive {4} complete. {5} words written.", SelectedDrive.Cylinder, SelectedDrive.Head, _sector, _sectorBlock, _selectedDrive, _writeIndex);
|
|
_writeState = WriteState.Idle;
|
|
_writeIndex = 0;
|
|
|
|
// Move to the next block
|
|
_sectorBlock++;
|
|
}
|
|
}
|
|
|
|
private void SectorCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
// Move to the next sector.
|
|
if (_runEnable && !NotReady())
|
|
{
|
|
_sector = (_sector + 1) % 9;
|
|
SelectedDrive.Sector = _sector;
|
|
|
|
// Reset to the first block (header) in the sector.
|
|
_sectorBlock = SectorBlock.Header;
|
|
|
|
// Wake up the Output task
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
}
|
|
else
|
|
{
|
|
// Keep the output task asleep.
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
}
|
|
|
|
//
|
|
// Schedule the next sector pulse
|
|
//
|
|
_sectorEvent.TimestampNsec = _sectorDuration - skewNsec;
|
|
_system.Scheduler.Schedule(_sectorEvent);
|
|
|
|
//
|
|
// If output fifo processing was paused and there's data in the FIFO
|
|
// to be dealt with, wake up the callback now.
|
|
//
|
|
if (_pauseOutputProcessing && _outputFifo.Count > 0)
|
|
{
|
|
_pauseOutputProcessing = false;
|
|
|
|
_outputFifoEvent.TimestampNsec = _outputFifoDuration;
|
|
_system.Scheduler.Schedule(_outputFifoEvent);
|
|
}
|
|
}
|
|
|
|
private TridentDrive SelectedDrive
|
|
{
|
|
get { return _drives[_selectedDrive]; }
|
|
}
|
|
|
|
|
|
//
|
|
// Input FIFO semantics
|
|
//
|
|
private void ClearInputFIFO()
|
|
{
|
|
_inputFifo.Clear();
|
|
_system.CPU.BlockTask(TaskType.TridentInput);
|
|
}
|
|
|
|
private void EnqueueInputFIFO(ushort word)
|
|
{
|
|
if (_inputFifo.Count == 16)
|
|
{
|
|
// We have overflowed the input FIFO. Set the requisite error
|
|
// flags and shut this thing down.
|
|
_inputLate = true;
|
|
|
|
Log.Write(LogComponent.TridentController, "Input FIFO overflowed.");
|
|
}
|
|
else
|
|
{
|
|
_inputFifo.Enqueue(word);
|
|
|
|
if (_inputFifo.Count >= 4)
|
|
{
|
|
_system.CPU.WakeupTask(TaskType.TridentInput);
|
|
}
|
|
else
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentInput);
|
|
}
|
|
}
|
|
}
|
|
|
|
private ushort DequeueInputFIFO()
|
|
{
|
|
ushort word = 0;
|
|
if (_inputFifo.Count == 0)
|
|
{
|
|
Log.Write(LogType.Warning, LogComponent.TridentController, "Input FIFO underflowed, returning 0.");
|
|
}
|
|
else
|
|
{
|
|
word = _inputFifo.Dequeue();
|
|
}
|
|
|
|
if (_inputFifo.Count < 4)
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentInput);
|
|
}
|
|
else
|
|
{
|
|
_system.CPU.WakeupTask(TaskType.TridentInput);
|
|
}
|
|
return word;
|
|
}
|
|
|
|
//
|
|
// Output FIFO semantics
|
|
//
|
|
private void ClearOutputFIFO()
|
|
{
|
|
_outputFifo.Clear();
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
}
|
|
|
|
private void EnqueueOutputFIFO(int word)
|
|
{
|
|
if (_outputFifo.Count == 16)
|
|
{
|
|
Log.Write(LogComponent.TridentController, "Output FIFO overflowed, dropping word.");
|
|
}
|
|
else
|
|
{
|
|
_outputFifo.Enqueue(word);
|
|
|
|
Log.Write(LogComponent.TridentController, "Output FIFO enqueued, queue depth is now {0}", _outputFifo.Count);
|
|
}
|
|
|
|
if (_outputFifo.Count <= 12)
|
|
{
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
}
|
|
else
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
}
|
|
|
|
}
|
|
|
|
private int DequeueOutputFIFO()
|
|
{
|
|
int word = 0;
|
|
if (_outputFifo.Count == 0)
|
|
{
|
|
Log.Write(LogType.Warning, LogComponent.TridentController, "Output FIFO underflowed, returning 0.");
|
|
}
|
|
else
|
|
{
|
|
word = _outputFifo.Dequeue();
|
|
Log.Write(LogComponent.TridentController, "Output FIFO dequeued, queue depth is now {0}", _outputFifo.Count);
|
|
}
|
|
|
|
if (_empty)
|
|
{
|
|
// Only wake up the Output task when the output FIFO goes completely empty.
|
|
if (_outputFifo.Count == 0)
|
|
{
|
|
_empty = false;
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
Log.Write(LogComponent.TridentController, "Output FIFO emptied, waking Output task.");
|
|
}
|
|
else
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_outputFifo.Count <= 12)
|
|
{
|
|
_system.CPU.WakeupTask(TaskType.TridentOutput);
|
|
}
|
|
else
|
|
{
|
|
_system.CPU.BlockTask(TaskType.TridentOutput);
|
|
}
|
|
}
|
|
return word;
|
|
}
|
|
|
|
// Wakeup signals
|
|
private bool _runEnable;
|
|
|
|
// Status bits
|
|
private bool _seekIncomplete;
|
|
private bool _headOverflow;
|
|
private bool _deviceCheck;
|
|
private bool _notSelected;
|
|
private bool _sectorOverflow;
|
|
private bool _outputLate;
|
|
private bool _inputLate;
|
|
private bool _compareError;
|
|
private bool _readOnly;
|
|
private bool _offset;
|
|
|
|
// Current sector
|
|
private int _sector;
|
|
|
|
/// <summary>
|
|
/// Drive timings
|
|
/// </summary>
|
|
private static ulong _sectorDuration = (ulong)((16.66666 / 9.0) * Conversion.MsecToNsec); // time in nsec for one sector -- 9 sectors, 16.66ms per rotation
|
|
private Event _sectorEvent;
|
|
|
|
// Output FIFO. This is actually 17-bits wide (the 17th bit is the Tag bit).
|
|
private Queue<int> _outputFifo;
|
|
private Event _outputFifoEvent;
|
|
private bool _pauseOutputProcessing;
|
|
private bool _empty;
|
|
private static ulong _outputFifoDuration = (ulong)(_sectorDuration / 2048.0); // time in nsec for one word -- 1120 words per sector.
|
|
|
|
// Input FIFO.
|
|
private Queue<ushort> _inputFifo;
|
|
|
|
//
|
|
// Read timings
|
|
//
|
|
private static ulong _readWordDuration = (ulong)(_sectorDuration / 1120.0); // time in nsec for one word -- 1120 words per sector.
|
|
private Event _readWordEvent;
|
|
private int _readIndex;
|
|
private int _readWordCount;
|
|
private bool _checkDone;
|
|
private int _checkedWordCount;
|
|
|
|
//
|
|
// Write timings
|
|
//
|
|
private static ulong _writeWordDuration = (ulong)(_sectorDuration / 1120.0); // time in nsec for one word -- 1120 words per sector.
|
|
private int _writeIndex;
|
|
private int _writeWordCount;
|
|
|
|
private enum WriteState
|
|
{
|
|
Idle,
|
|
Initialized,
|
|
WaitingForStartBit,
|
|
Writing,
|
|
}
|
|
private WriteState _writeState;
|
|
|
|
private enum ReadState
|
|
{
|
|
Idle,
|
|
Initialized,
|
|
WaitingForStartBit,
|
|
Reading,
|
|
}
|
|
private ReadState _readState;
|
|
|
|
private enum SectorBlock
|
|
{
|
|
Header,
|
|
Label,
|
|
Data,
|
|
}
|
|
private SectorBlock _sectorBlock;
|
|
|
|
//
|
|
// Attached drives
|
|
//
|
|
private TridentDrive[] _drives;
|
|
private int _selectedDrive;
|
|
|
|
private AltoSystem _system;
|
|
}
|
|
}
|