1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-25 11:45:59 +00:00
Josh Dersch f849b795a6 - Fixed two memory state machine issues:
1) The Orbit microcode occasionally expects to be able to do a double-word read in cycles 6 and 7 instead of the usual 5 and 6.
  2) The double-word logic incorrectly handles several species of overlapped read/writes, which Spruce's layout microcode uses.

As a result, Spruce now works, as do the Smalltalk-80 images.

Several tweaks to microcode disassembly (still rough).

Completed Orbit / Dover ROS / Dover Print Engine emulation.  Still some rough edges; it works and can create PDF output.

Tweaked Disk Controller's Restore operation, it is now more accurate though wasn't causing any noticable issues.
2017-07-10 16:49:12 -07:00

843 lines
32 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.IO.Printing;
using Contralto.Logging;
using System;
namespace Contralto.IO
{
public enum AdapterCommand
{
BufferReset = 0,
SetScales = 1,
SetBitClockRegister = 2,
SetMotorSpeedRegister = 3,
SetLineSyncDelayRegister = 4,
SetPageSyncDelayRegister = 5,
ExternalCommand1 = 6,
ExternalCommand2 = 7,
}
/// <summary>
/// Encapsulates the logic for both the ROS (Raster Output Scanner) hardware and
/// the Dover print engine.
///
/// These two should probably be separated out in the event that we want to support
/// other kinds of printers.
///
/// The Dover print engine support is currently very hand-wavy as the documentation
/// isn't extremely specific on some details and we're emulating a mechanical construct
/// moving papers past sensors, etc. It mostly works but it has some rough edges.
/// In particular the "Cold Start" mechanic is not well understood. Many parameters
/// are currently ignored (some for good reason; we don't need to emulate the actual
/// laser scanning and polygon rotation in order to create a bitmap, for example) but
/// some of them might be put to good use.
///
/// None of the diagnostic switches or options are implemented.
/// </summary>
public class DoverROS
{
public DoverROS(AltoSystem system)
{
_system = system;
_printEngineEvent = new Event(0, null, PrintEngineCallback);
Reset();
}
public void Reset()
{
_testMode = false;
_commandBeamOn = false;
_commandLocal = false;
_testPageSync = false;
_extendVideo = false;
_motorScale = 0;
_bitScale = 0;
_bitClock = 0;
_motorSpeed = 0;
_lineSyncDelay = 0;
_pageSyncDelay = 0;
_packetsOK = true;
_state = PrintState.Idle;
_runoutCount = 0;
}
public void RecvCommand(ushort commandWord)
{
//
// Control of the adapter, ROS, and printer is accomplished with 16-bit commands.
// The high-order 4 bits of the command give a command code; the remaining 12 bits are
// used as an argument to the command.
//
_lastCommand = commandWord;
AdapterCommand command = (AdapterCommand)(commandWord >> 12);
int argument = commandWord & 0xfff;
switch (command)
{
case AdapterCommand.BufferReset:
break;
case AdapterCommand.SetScales:
_testMode = (argument & 0x1) != 0;
_commandBeamOn = (argument & 0x2) != 0;
_commandLocal = (argument & 0x4) != 0;
_testPageSync = (argument & 0x8) != 0;
_extendVideo = (argument & 0x20) != 0;
_motorScale = (argument & 0x1c0) >> 6;
_bitScale = (argument & 0xe00) >> 9;
Log.Write(LogComponent.DoverROS, "TestMode {0} CommandBeamOn {1} CommandLocal {2} TestPageSync {3} ExtendVideo {4} MotorScale {5} BitScale {6}",
_testMode,
_commandBeamOn,
_commandLocal,
_testPageSync,
_extendVideo,
_motorScale,
_bitScale);
break;
case AdapterCommand.SetBitClockRegister:
_bitClock = argument;
Log.Write(LogComponent.DoverROS, "BitClock set to {0}", argument);
break;
case AdapterCommand.SetMotorSpeedRegister:
_motorSpeed = argument;
Log.Write(LogComponent.DoverROS, "MotorSpeed set to {0}", argument);
break;
case AdapterCommand.SetLineSyncDelayRegister:
_lineSyncDelay = argument;
Log.Write(LogComponent.DoverROS, "LineSyncDelay set to {0}", argument);
break;
case AdapterCommand.SetPageSyncDelayRegister:
_pageSyncDelay = argument;
Log.Write(LogComponent.DoverROS, "PageSyncDelay set to {0}", argument);
break;
case AdapterCommand.ExternalCommand1:
bool lastPrintRequest = (_externalCommand1 & 0x1) == 0;
_externalCommand1 = argument;
//
// Dover uses the low-order bit to provide the PrintRequest signal.
// A 0-to-1 transition of the bit tells the printer to start feeding
// sheets.
//
Log.Write(LogComponent.DoverROS, "ExternalCommand1 written {0}", argument);
if (lastPrintRequest && (_externalCommand1 & 0x1) != 0)
{
PrintRequest();
}
break;
case AdapterCommand.ExternalCommand2:
_videoGate = argument;
Log.Write(LogComponent.DoverROS, "VideoGate set to {0}", argument);
break;
default:
Log.Write(LogType.Error, LogComponent.DoverROS,
String.Format("Unhandled ROS command {0}", command));
break;
}
}
public int ReadStatus(int address)
{
//
// Address is a value from 0-63 (as specified by the Orbit's OrbitControl function)
// specifying the address of 4 status bits in the ROS's status memory.
// 4 status bits -- 4*address to 4*address+3 inclusive are returned.
//
int bitAddress = (address * 4);
ushort statusWord = ReadStatusWord(bitAddress >> 4);
int bitOffset = 12 - (bitAddress & 0xf);
return (statusWord & ((0xf) << bitOffset)) >> bitOffset;
}
private ushort ReadStatusWord(int wordNumber)
{
int value = 0;
switch (wordNumber)
{
case 0:
//
// Special status from the ROS:
// bit
// 0 - SendVideo
// 1 - PrintMode
// 2 - Local
// 3 - BeamEnable
// 4 - StatusBeamOn
//
value |= (_sendVideo ? 0x8000 : 0);
value |= (_printMode ? 0x4000 : 0);
value |= (_local ? 0x2000 : 0);
value |= (_beamEnable ? 0x1000 : 0);
value |= (_statusBeamOn ? 0x0800 : 0);
break;
case 1:
// A copy of the command most recently received by the adapter.
value = _lastCommand;
break;
case 2:
//
// Bit clock:
// bit
// 0 - VideoPolarity
// 1-3 - BitScale
// 4-15 - BitClock
//
value |= (_videoPolarity ? 0x8000 : 0);
value |= (_bitScale << 12);
value |= _bitClock;
break;
case 3:
//
// Motor speed
// bit
// 0 - SelectLeadEdge
// 1-3 - MotorScale
// 4-15 - MotorSpeed
value |= (_selectLeadEdge ? 0x8000 : 0);
value |= (_motorScale << 12);
value |= _motorSpeed;
break;
case 4:
//
// Line sync delay
// bit
// 0 - Switch3
// 2 - ExtendVideo
// 3 - TestPageSync
// 4-15 - LineSyncDelay
//
value |= (_switch3 ? 0x8000 : 0);
value |= (_extendVideo ? 0x2000 : 0);
value |= (_testPageSync ? 0x1000 : 0);
value |= _lineSyncDelay;
break;
case 5:
//
// Page sync delay
// bit
// 0 - Switch4
// 1 - CommandLocal
// 2 - CommandBeamOn
// 3 - TestMode
// 4-15 - PageSyncDelay
//
value |= (_switch4 ? 0x8000 : 0);
value |= (_commandLocal ? 0x4000 : 0);
value |= (_commandBeamOn ? 0x2000 : 0);
value |= (_testMode ? 0x1000 : 0);
value |= _pageSyncDelay;
break;
case 6:
//
// External Command 1
// bit
// 0 - LineNoise
// 1 - CompareError
// 2 - BufferUnderflow
// 3 - PacketsOK
// 4-12 - ExternalCommand1
//
value |= (_lineNoise ? 0x8000 : 0);
value |= (_compareError ? 0x4000 : 0);
value |= (_bufferUnderflow ? 0x2000 : 0);
value |= (_packetsOK ? 0x1000 : 0);
value |= _externalCommand1;
break;
case 7:
//
// bit
// 0-3 - LineCount
// 4-15 - VideoGate
//
value |= (_lineCount << 12);
value |= _videoGate;
//
// LineCount is not well documented anywhere in the hardware documentation
// available. It is related to the motion of the laser as it sweeps across
// the page. The source code for Spruce (SprucePrinDover.bcpl)
// has this to say:
// "A check, designed chiefly for Dover, verifies that the laser is on and the polygon is
// scanning (SOS/EOS are seeing things), by making sure that the low four bits of the line
// count (indicated in status word) are changing. The code reads the line count, waits
// approximately the time needed for four scan lines (generous margin), then reads the
// count again, reporting a problem if the values are equal."
//
//
// Indeed, if this value does not increment, Spruce fails with a
// "Laser appears to be off" error.
// We fudge this here.
_lineCount++;
break;
case 8:
// Special Dover status bits 0-15
// Count-H -- indicates that a page is ready to be printed.
value |= (_countH ? 0x1000 : 0);
//
// OR in status bits that are expected to be "1"
// for normal operation (i.e. no malfunctions).
// These are:
// 8 - LS4 (adequate paper in tray)
// 11 - LaserOn
// 13 - ReadyTemp
value |= 0x0094;
break;
case 9:
// Special Dover status bits 16-31
//
// OR in status bits that are expected to be "1"
// for normal operation (i.e. no malfunctions).
// These are:
// 5 - ACMonitor
// 13 - LS24 & LS31
value |= 0x0404;
Log.Write(LogComponent.DoverROS, "Dover bits 16-31: {0}", Conversion.ToOctal(value));
break;
case 10:
value = _id;
Log.Write(LogComponent.DoverROS, "Device ID {0}", Conversion.ToOctal(value));
break;
case 11:
value = _serialNumber;
Log.Write(LogComponent.DoverROS, "Device Serial: {0}", Conversion.ToOctal(value));
break;
default:
Log.Write(LogComponent.DoverROS, "Unhandled ROS status word {0}", wordNumber);
value = 0xffff;
break;
}
return (ushort)value;
}
private void PrintRequest()
{
switch (_state)
{
case PrintState.Idle:
if (!_printMode)
{
_state = PrintState.ColdStart;
_printMode = true;
_sendVideo = true;
_keepGoing = false;
_runoutCount = 0;
ClearPageRaster();
//
// Calculate a few things based on ROS parameters
//
// PageSyncDelay is (4096-n/i) where n is the number of
// scan-lines to pass up after receiving PageSync from the engine
// before starting SendVideo. i is 1 for Dover II, and 4 for older
// ones.
// We assume a Dover I here.
_sendVideoStartScanline = 4 * (4096 - _pageSyncDelay);
//
// VideoGate is (4096-n/4) where n is the number of scan-lines to
// pass up after SendVideo starts before stopping SendVideo.
_sendVideoEndScanline = 4 * (4096 - _videoGate) + _sendVideoStartScanline;
Log.Write(LogComponent.DoverROS, "SendVideo start {0}, end {1}",
_sendVideoStartScanline,
_sendVideoEndScanline);
//
// Start printing engine running.
//
_printEngineEvent.TimestampNsec = _printEngineTimestepInterval;
_system.Scheduler.Schedule(_printEngineEvent);
_printEngineTimestep = -375; // Start 250ms before the first CS-5
//
// Start output
//
InitializePrintOutput();
Log.Write(LogComponent.DoverROS, "Cold Start initialized. Engine started.");
}
else
{
Log.Write(LogComponent.DoverROS, "Unexpected PrintRequest with PrintMode active during Idle.");
}
break;
case PrintState.ColdStart:
Log.Write(LogComponent.DoverROS, "PrintRequest received in cold start.");
_keepGoing = true;
break;
case PrintState.InnerLoop:
if (!_printMode)
{
// PrintRequest too late.
Log.Write(LogComponent.DoverROS, "PrintRequest too late during Inner Loop. Ignoring.");
}
else
{
Log.Write(LogComponent.DoverROS, "PrintRequest during inner loop. Continuing.");
_keepGoing = true;
}
break;
case PrintState.Runout:
if (!_printMode)
{
// PrintRequest too late.
Log.Write(LogComponent.DoverROS, "PrintRequest too late during Runout. Ignoring.");
}
else
{
Log.Write(LogComponent.DoverROS, "PrintRequest during Runout. Moving to Inner Loop.");
_state = PrintState.InnerLoop;
_keepGoing = true;
_runoutCount = 0;
}
break;
}
}
private void InitializePrintOutput()
{
//
// Select the appropriate output based on the configuration.
// For now it's either PDF or nothing.
//
if (Configuration.EnablePrinting)
{
_pageOutput = new PdfPageSink();
}
else
{
_pageOutput = new NullPageSink();
}
_pageOutput.StartDoc();
}
/// <summary>
/// This is invoked for every scanline that passes under the virtual print path.
/// At each scanline flags are updated and raster data is pulled from the Orbit and
/// copied to the page as necessary.
/// </summary>
/// <param name="timestampNsec"></param>
/// <param name="delta"></param>
/// <param name="context"></param>
private void PrintEngineCallback(ulong timestampNsec, ulong delta, object context)
{
Log.Write(LogComponent.DoverROS, "Scanline {0} (sendvideo {1})", _printEngineTimestep, _sendVideo);
switch (_state)
{
case PrintState.ColdStart:
{
// Go through the motions but don't rasterize anything
//
// The Cold-start loop starts 250ms before the first CS-5 signal.
if (_printEngineTimestep >= 0 && _printEngineTimestep < _sendVideoStartScanline)
{
_sendVideo = false;
}
else
{
_sendVideo = true;
}
if (_printEngineTimestep >= 0 &&
_printEngineTimestep < _sendVideoEndScanline)
{
// Pull rasters one scanline at a time out of Orbit,
// since we're in cold-start, these are discarded.
if (_system.OrbitController.SLOTTAKE)
{
// Read in one scanline of data -- this is 256 words
for (int x = 0; x < 256 - _system.OrbitController.FA; x++)
{
ushort word = _system.OrbitController.GetOutputDataROS();
// The assumption is that during this phase the Alto will just
// be sending zeroes, log if this assumption does not hold...
//
if (word != 0)
{
Log.Write(LogComponent.DoverROS, "Read non-zero orbit data during cold-start {0}", Conversion.ToOctal(word));
}
}
Log.Write(LogComponent.DoverROS, "Read cold-start band {0}", _readBands);
}
else
{
// Nothing right now
Log.Write(LogComponent.DoverROS, "No bands available from Orbit.");
}
_readBands++;
}
if (_printEngineTimestep >= 3136)
{
// After appx. 896ms (3136 scanlines) the first Count-H is raised.
_countH = true;
}
_printEngineTimestep++;
if (_printEngineTimestep >= 3500)
{
// End of cold-start "page."
_readBands = 0;
_printEngineTimestep = 0;
_countH = false;
ClearPageRaster();
if (_keepGoing)
{
//
// We got a PrintRequest during ColdStart, so we'll continue to the next page.
//
Log.Write(LogComponent.DoverROS, "End of Cold Start, switching to Inner Loop.");
_state = PrintState.InnerLoop;
}
else
{
//
// No PrintRequest, we will switch to Runout and begin shutting down.
//
Log.Write(LogComponent.DoverROS, "End of Cold Start, switching to Runout.");
_state = PrintState.Runout;
}
_keepGoing = false;
}
_printEngineEvent.TimestampNsec = _printEngineTimestepInterval;
_system.Scheduler.Schedule(_printEngineEvent);
}
break;
case PrintState.InnerLoop:
{
//
// Inner loop starts with CS-5 (timestep 0).
// After which there is a delay (PageSyncDelay) before
// SendVideo goes high and we can start reading raster data.
if (_printEngineTimestep < _sendVideoStartScanline)
{
_sendVideo = false;
}
else
{
_sendVideo = true;
}
if (_printEngineTimestep >= 0 &&
_printEngineTimestep < _sendVideoEndScanline)
{
// Video gate : pull rasters one scanline at a time out of Orbit.
if (_system.OrbitController.SLOTTAKE)
{
// Read in one scanline's worth of data
int scanlineWordCount = 256 - _system.OrbitController.FA;
for (int x = 0; x < scanlineWordCount; x++)
{
ushort word = _system.OrbitController.GetOutputDataROS();
int pageDataIndex = _readBands * scanlineWordCount * 2 + x * 2;
_pageData[pageDataIndex] = (byte)(~word >> 8);
_pageData[pageDataIndex + 1] = (byte)(~word & 0xff);
}
Log.Write(LogComponent.DoverROS, "Read band {0}", _readBands);
}
else
{
// Nothing right now
Log.Write(LogComponent.DoverROS, "No bands available from Orbit.");
}
_readBands++;
}
if (_printEngineTimestep >= 3136)
{
// After appx. 896ms (3136 scanlines) Count-H is raised.
_countH = true;
}
_printEngineTimestep++;
if (_printEngineTimestep >= 3500)
{
//
// Send page rasters to the print output.
//
_pageOutput.AddPage(_pageData, _readBands, (256 - _system.OrbitController.FA) * 16);
ClearPageRaster();
// End of page.
_countH = false;
_readBands = 0;
_printEngineTimestep = 0;
if (_keepGoing)
{
//
// A PrintRequest was recieved during the Inner Loop, so we'll keep going to the next page.
Log.Write(LogComponent.DoverROS, "End of Page, continuing in Inner Loop.");
_state = PrintState.InnerLoop;
}
else
{
Log.Write(LogComponent.DoverROS, "End of Page, switching to Runout.");
_state = PrintState.Runout;
}
_keepGoing = false;
}
_printEngineEvent.TimestampNsec = _printEngineTimestepInterval;
_system.Scheduler.Schedule(_printEngineEvent);
}
break;
case PrintState.Runout:
{
//
// SendVideo still gets toggled during Runout. CountH is
// toggled by paper moving past a sensor, which does not happen
// during runout.
//
if (_printEngineTimestep < _sendVideoStartScanline)
{
_sendVideo = false;
}
else
{
_sendVideo = true;
}
_printEngineTimestep++;
if (_printEngineTimestep >= 3500)
{
// End of runout cycle.
_countH = false;
_printEngineTimestep = 0;
if (_keepGoing)
{
Log.Write(LogComponent.DoverROS, "End of Runout cycle {0}, continuing in Inner Loop.", _runoutCount);
_state = PrintState.InnerLoop;
}
else
{
Log.Write(LogComponent.DoverROS, "End of Runout cycle {0}.", _runoutCount);
_state = PrintState.Runout;
}
_keepGoing = false;
_runoutCount++;
}
if (_runoutCount > 7)
{
// Just shut off.
_state = PrintState.Idle;
_printMode = false;
_countH = false;
_sendVideo = false;
//
// Finish the output.
//
_pageOutput.EndDoc();
Log.Write(LogComponent.DoverROS, "Runout: shutting down, switching to Idle state. Output completed.");
}
else
{
//
// Go around for another Runout cycle.
_printEngineEvent.TimestampNsec = _printEngineTimestepInterval;
_system.Scheduler.Schedule(_printEngineEvent);
}
}
break;
}
}
private void ClearPageRaster()
{
//
// reset to white
//
for(int i=0;i<_pageData.Length;i++)
{
_pageData[i] = 0xff;
}
}
private enum PrintState
{
Idle = 0,
ColdStart,
InnerLoop,
Runout
}
private PrintState _state;
private bool _keepGoing;
private int _readBands;
private AltoSystem _system;
// Last command sent to us
private ushort _lastCommand;
// Command registers
private bool _testMode;
private bool _commandBeamOn;
private bool _commandLocal;
private bool _testPageSync;
private bool _extendVideo;
private int _motorScale;
private int _bitScale;
private int _bitClock;
private int _motorSpeed;
private int _lineSyncDelay;
private int _pageSyncDelay;
private int _externalCommand1;
private int _videoGate;
// Status registers
private bool _sendVideo;
private bool _printMode;
private bool _local;
private bool _beamEnable = true;
private bool _statusBeamOn;
private bool _lineNoise;
private bool _compareError;
private bool _bufferUnderflow;
private bool _packetsOK = true;
private int _lineCount;
// Physical switches (test and otherwise)
private bool _videoPolarity;
private bool _selectLeadEdge;
private bool _switch3;
private bool _switch4;
// ID and serial number
private ushort _id = 0;
private ushort _serialNumber = 0;
//
// Dover specific status that we care to report.
// TODO: if we're printing to a real printer, we could choose
// to raise status (malfunction bits, etc.) that correspond to
// real printer status...
//
//
// This signal is raised if a sheet has been successfully fed to
// receive the image for the page being printed.
// Count-H comes on 896ms after CS-5 and persists until the next
// CS-5. (Note -- CS-5 is transmitted as the PageSync signal)
//
private bool _countH;
/// <summary>
/// Number of passes through the Runout state.
/// </summary>
private int _runoutCount;
// Events to drive the print state machine
//
private Event _printEngineEvent;
private int _printEngineTimestep;
private int _sendVideoStartScanline;
private int _sendVideoEndScanline;
/// <summary>
/// The timeslice for a single engine step (one scanline).
/// There are (in theory) 2975 scanlines per 8.5" page (at 350dpi) which moves
/// through the engine in 850ms.
///
/// The cycle time for an entire page is 1000ms, which we pretend
/// is 3500 scanline times.
///
/// </summary>
private ulong _printEngineTimestepInterval = (ulong)((1000.0 / 3500.0) * Conversion.MsecToNsec);
private byte[] _pageData = new byte[3500 * 512];
private int _rasterNum;
private IPageSink _pageOutput;
}
}