mirror of
https://github.com/livingcomputermuseum/ContrAlto.git
synced 2026-01-13 15:18:23 +00:00
368 lines
12 KiB
C#
368 lines
12 KiB
C#
/*
|
|
This file is part of ContrAlto.
|
|
|
|
ContrAlto is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
ContrAlto is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with ContrAlto. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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>
|
|
/// Defines the "type" of data in the current sector timeslice.
|
|
/// </summary>
|
|
public enum CellType
|
|
{
|
|
Data,
|
|
Gap,
|
|
Sync,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the data (or lack thereof) in the current sector timeslice.
|
|
/// This may be actual data (header, label, or data), interrecord gaps, or
|
|
/// sync words.
|
|
/// </summary>
|
|
public 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);
|
|
}
|
|
|
|
public static DataCell Empty = new DataCell(0, CellType.Data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encapsulates logic that belongs to the drive, including loading/saving packs,
|
|
/// seeking and reading sector data.
|
|
/// </summary>
|
|
public class DiabloDrive
|
|
{
|
|
public DiabloDrive(AltoSystem system)
|
|
{
|
|
_system = system;
|
|
Reset();
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_sector = 0;
|
|
_cylinder = 0;
|
|
_head = 0;
|
|
|
|
_sectorModified = false;
|
|
InitSector();
|
|
LoadSector();
|
|
}
|
|
|
|
// Offsets in words for start of data in sector
|
|
public static readonly int HeaderOffset = 44;
|
|
public static readonly int LabelOffset = HeaderOffset + 14;
|
|
public static readonly int DataOffset = LabelOffset + 20;
|
|
|
|
// Total "words" (really timeslices) per sector.
|
|
public static readonly int SectorWordCount = 269 + HeaderOffset + 34;
|
|
|
|
public void LoadPack(DiabloPack pack)
|
|
{
|
|
_pack = pack;
|
|
Reset();
|
|
}
|
|
|
|
public void UnloadPack()
|
|
{
|
|
_pack = null;
|
|
Reset();
|
|
}
|
|
|
|
public bool IsLoaded
|
|
{
|
|
get { return _pack != null; }
|
|
}
|
|
|
|
public DiabloPack Pack
|
|
{
|
|
get { return _pack; }
|
|
}
|
|
|
|
public int Sector
|
|
{
|
|
get { return _sector; }
|
|
set
|
|
{
|
|
// If the last sector was modified,
|
|
// commit it before moving to the next.
|
|
if (_sectorModified)
|
|
{
|
|
CommitSector();
|
|
_sectorModified = false;
|
|
}
|
|
|
|
_sector = value;
|
|
LoadSector();
|
|
}
|
|
}
|
|
|
|
public int Head
|
|
{
|
|
get { return _head; }
|
|
set
|
|
{
|
|
if (value != _head)
|
|
{
|
|
// If we switch heads, we need to reload the sector.
|
|
|
|
// If the last sector was modified,
|
|
// commit it before moving to the next.
|
|
if (_sectorModified)
|
|
{
|
|
CommitSector();
|
|
_sectorModified = false;
|
|
}
|
|
|
|
_head = value;
|
|
LoadSector();
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Cylinder
|
|
{
|
|
get { return _cylinder; }
|
|
set
|
|
{
|
|
if (value != _cylinder)
|
|
{
|
|
// If we switch cylinders, we need to reload the sector.
|
|
// If the last sector was modified,
|
|
// commit it before moving to the next.
|
|
if (_sectorModified)
|
|
{
|
|
CommitSector();
|
|
_sectorModified = false;
|
|
}
|
|
|
|
_cylinder = value;
|
|
LoadSector();
|
|
}
|
|
}
|
|
}
|
|
|
|
public DataCell ReadWord(int index)
|
|
{
|
|
if (_pack != null)
|
|
{
|
|
return _sectorData[index];
|
|
}
|
|
else
|
|
{
|
|
return DataCell.Empty;
|
|
}
|
|
}
|
|
|
|
public void WriteWord(int index, ushort data)
|
|
{
|
|
if (_pack!= null && index < _sectorData.Length)
|
|
{
|
|
if (_sectorData[index].Type == CellType.Data)
|
|
{
|
|
_sectorData[index].Data = data;
|
|
}
|
|
else
|
|
{
|
|
Log.Write(LogType.Warning, LogComponent.DiskController, "Data written to non-data section (Sector {0} Word {1} Rec {2} Data {3})", _sector, index, 666, Conversion.ToOctal(data));
|
|
}
|
|
|
|
_sectorModified = true;
|
|
}
|
|
}
|
|
|
|
|
|
private void LoadSector()
|
|
{
|
|
if (_pack == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
// 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);
|
|
|
|
// Header (2 words data, 1 word cksum)
|
|
for (int i = HeaderOffset + 1, j = 1; i < HeaderOffset + 3; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
_sectorData[i] = new DataCell(sector.Header[j], CellType.Data);
|
|
}
|
|
|
|
ushort checksum = CalculateChecksum(_sectorData, HeaderOffset + 1, 2);
|
|
_sectorData[HeaderOffset + 3].Data = checksum;
|
|
Log.Write(LogType.Verbose, LogComponent.DiskController, "Header checksum for C/H/S {0}/{1}/{2} is {3}", _cylinder, _head, _sector, Conversion.ToOctal(checksum));
|
|
|
|
// Label (8 words data, 1 word cksum)
|
|
for (int i = LabelOffset + 1, j = 7; i < LabelOffset + 9; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
_sectorData[i] = new DataCell(sector.Label[j], CellType.Data);
|
|
}
|
|
|
|
checksum = CalculateChecksum(_sectorData, LabelOffset + 1, 8);
|
|
_sectorData[LabelOffset + 9].Data = checksum;
|
|
Log.Write(LogType.Verbose, LogComponent.DiskController, "Label checksum for C/H/S {0}/{1}/{2} is {3}", _cylinder, _head, _sector, Conversion.ToOctal(checksum));
|
|
|
|
// sector data (256 words data, 1 word cksum)
|
|
for (int i = DataOffset + 1, j = 255; i < DataOffset + 257; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
_sectorData[i] = new DataCell(sector.Data[j], CellType.Data);
|
|
}
|
|
|
|
checksum = CalculateChecksum(_sectorData, DataOffset + 1, 256);
|
|
_sectorData[DataOffset + 257].Data = checksum;
|
|
Log.Write(LogType.Verbose, LogComponent.DiskController, "Data checksum for C/H/S {0}/{1}/{2} is {3}", _cylinder, _head, _sector, Conversion.ToOctal(checksum));
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits modified sector data back to the emulated disk.
|
|
/// Intended to be called at the end of the sector / beginning of the next.
|
|
/// </summary>
|
|
private void CommitSector()
|
|
{
|
|
if (_pack == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DiabloDiskSector sector = _pack.GetSector(_cylinder, _head, _sector);
|
|
|
|
// Header (2 words data, 1 word cksum)
|
|
for (int i = HeaderOffset + 1, j = 1; i < HeaderOffset + 3; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
sector.Header[j] = _sectorData[i].Data;
|
|
}
|
|
|
|
// Label (8 words data, 1 word cksum)
|
|
for (int i = LabelOffset + 1, j = 7; i < LabelOffset + 9; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
sector.Label[j] = _sectorData[i].Data;
|
|
}
|
|
|
|
// sector data (256 words data, 1 word cksum)
|
|
for (int i = DataOffset + 1, j = 255; i < DataOffset + 257; i++, j--)
|
|
{
|
|
// actual data to be loaded from disk / cksum calculated
|
|
sector.Data[j] = _sectorData[i].Data;
|
|
}
|
|
}
|
|
|
|
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 + 258; i < SectorWordCount; 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 < offset + 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 AltoSystem _system;
|
|
|
|
//
|
|
// Current disk position
|
|
//
|
|
private int _cylinder;
|
|
private int _head;
|
|
private int _sector;
|
|
|
|
private bool _sectorModified;
|
|
|
|
private DataCell[] _sectorData = new DataCell[SectorWordCount];
|
|
|
|
|
|
// The pack loaded into the drive
|
|
DiabloPack _pack;
|
|
}
|
|
}
|