1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-20 17:57:29 +00:00

295 lines
9.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Contralto.Memory;
namespace Contralto.IO
{
public class DiskController
{
public DiskController(AltoSystem system)
{
_system = system;
}
public ushort KDATA
{
get { return _kData; }
set { _kData = 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]."
_head = (_kData & 0x4) >> 2;
}
}
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;
}
}
public ushort KSTAT
{
get { return _kStat; }
set { _kStat = value; }
}
public ushort RECNO
{
get { return _recMap[_recNo]; }
}
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 void Reset()
{
ClearStatus();
_recNo = 0;
_elapsedSectorTime = 0.0;
_cylinder = 0;
_sector = 0;
_head = 0;
_kStat = 0;
}
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));
_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)
{
_elapsedSectorTime -= _seekClocks;
if (_cylinder < _destCylinder)
{
_cylinder++;
}
else if (_cylinder > _destCylinder)
{
_cylinder--;
}
// Are we *there* yet?
if (_cylinder == _destCylinder)
{
// clear Seek bit
_kStat &= 0xffbf;
}
}
}
//
// 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.
//
// pseudocode so far:
// if (elapsed word time > word time)
// {
// elapsed word time = 0 (modulo remainder)
// _kData = next word
// if (!_wdInhib) DiskSectorTask.Wakeup();
// }
}
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.");
}
_destCylinder = (_kData & 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;
}
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;
}
}
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;
}
private ushort _kData;
private ushort _kAdr;
private ushort _kCom;
private ushort _kStat;
private int _recNo;
private ushort[] _recMap =
{
0, 2, 3, 1
};
// KCOM bits
private bool _xferOff;
private bool _wdInhib;
private bool _bClkSource;
private bool _wffo;
private bool _sendAdr;
// Current disk position
private int _cylinder;
private int _destCylinder;
private int _head;
private int _sector;
// Sector timing. Based on table on pg. 43 of the Alto Hardware Manual
private double _elapsedSectorTime; // elapsed time in this sector (in clocks)
private const double _sectorDuration = (40.0 / 12.0); // time in msec for one sector
private readonly double _sectorClocks = _sectorDuration / AltoSystem.ClockInterval; // number of clock cycles per sector time.
// Cylinder seek timing. Again, see the manual.
// Timing varies based on how many cylinders are being traveled during a seek; see
// CalculateSeekTime() for more.
private double _elapsedSeekTime;
private double _seekClocks;
private AltoSystem _system;
}
}