1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-27 12:31:52 +00:00
Files
livingcomputermuseum.ContrAlto/Contralto/CPU/CPU.cs

845 lines
32 KiB
C#

using Contralto.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Contralto.CPU
{
public enum TaskType
{
Invalid = -1,
Emulator = 0,
DiskSector = 4,
Ethernet = 7,
MemoryRefresh = 8,
DisplayWord = 9,
Cursor = 10,
DisplayHorizontal = 11,
DisplayVertical = 12,
Parity = 13,
DiskWord = 14,
}
public class AltoCPU
{
public AltoCPU(AltoSystem system)
{
_system = system;
_tasks[(int)TaskType.Emulator] = new EmulatorTask(this);
Reset();
}
public Task[] Tasks
{
get { return _tasks; }
}
public Task CurrentTask
{
get { return _currentTask; }
}
public ushort[] R
{
get { return _r; }
}
public ushort[][] S
{
get { return _s; }
}
public ushort T
{
get { return _t; }
}
public ushort L
{
get { return _l; }
}
public ushort M
{
get { return _m; }
}
public ushort IR
{
get { return _ir; }
}
public ushort ALUC0
{
get { return _aluC0; }
}
public void Reset()
{
// Reset registers
_r = new ushort[32];
_s = new ushort[8][];
for(int i=0;i<_s.Length;i++)
{
_s[i] = new ushort[32];
}
_t = 0;
_l = 0;
_m = 0;
_ir = 0;
_aluC0 = 0;
_rb = 0;
// Reset tasks.
for (int i=0;i<_tasks.Length;i++)
{
if (_tasks[i] != null)
{
_tasks[i].Reset();
}
}
// Execute the initial task switch.
TaskSwitch();
_currentTask = _nextTask;
_nextTask = null;
}
public void ExecuteNext()
{
if (_currentTask.ExecuteNext())
{
// Invoke the task switch, this will take effect after
// the NEXT instruction, not this one.
TaskSwitch();
}
else
{
// If we have a new task, switch to it now.
if (_nextTask != null)
{
_currentTask = _nextTask;
_nextTask = null;
}
}
_clocks++;
}
/// <summary>
/// Used by hardware devices to cause a specific task to have its
/// "wakeup" signal triggered
/// </summary>
/// <param name="task"></param>
public void WakeupTask(int task)
{
if (_tasks[task] != null)
{
_tasks[task].WakeupTask();
}
}
/// <summary>
/// Used by hardware devices to cause a specific task to have its
/// "wakeup" signal cleared
/// </summary>
/// <param name="task"></param>
public void BlockTask(int task)
{
if (_tasks[task] != null)
{
_tasks[task].BlockTask();
}
}
private void TaskSwitch()
{
// Select the highest-priority eligible task
for (int i = _tasks.Length - 1; i >= 0; i--)
{
if (_tasks[i] != null && _tasks[i].Wakeup)
{
_nextTask = _tasks[i];
}
}
}
// Task:
// Base task class: provides implementation for non-task-specific microcode execution and
// state. Task subclasses implement and execute Task-specific behavior and are called into
// by the base class as necessary.
public abstract class Task
{
public Task(AltoCPU cpu)
{
_wakeup = false;
_mpc = 0xffff; // invalid, for sanity checking
_priority = -1; // invalid
_cpu = cpu;
}
public int Priority
{
get { return _priority; }
}
public bool Wakeup
{
get { return _wakeup; }
}
public ushort MPC
{
get { return _mpc; }
}
public virtual void Reset()
{
// From The Alto Hardware Manual (section 2, "Initialization"):
// "...each task start[s] at the location which is its task number"
//
_mpc = (ushort)_priority;
}
public virtual void BlockTask()
{
// Used only by hardware interfaces, where applicable
_wakeup = false;
}
public virtual void WakeupTask()
{
_wakeup = true;
}
public bool ExecuteNext()
{
// TODO: cache microinstructions (or pre-decode them) to save consing all these up every time.
MicroInstruction instruction = new MicroInstruction(UCodeMemory.UCodeROM[_mpc]);
return ExecuteInstruction(instruction);
}
/// <summary>
/// ExecuteInstruction causes the Task to execute the next instruction (the one
/// _mpc is pointing to). The base implementation covers non-task specific logic; subclasses may
/// provide their own overrides.
/// </summary>
/// <returns>True if a task switch has been requested by a TASK instruction, false otherwise.</returns>
protected virtual bool ExecuteInstruction(MicroInstruction instruction)
{
bool nextTask = false;
bool loadR = false;
ushort aluData = 0;
ushort nextModifier = 0;
_loadS = false;
_rSelect = 0;
_busData = 0;
Shifter.SetMagic(false);
//
// Wait for memory state machine if a memory operation is requested by this instruction and
// the memory isn't ready yet.
// TODO: this needs to be seriously cleaned up.
//
if (instruction.BS == BusSource.ReadMD ||
instruction.F1 == SpecialFunction1.LoadMAR ||
instruction.F2 == SpecialFunction2.StoreMD)
{
MemoryOperation op;
if (instruction.BS == BusSource.ReadMD)
{
op = MemoryOperation.Read;
}
else if(instruction.F1 == SpecialFunction1.LoadMAR)
{
op = MemoryOperation.LoadAddress;
}
else
{
op = MemoryOperation.Store;
}
if (!_cpu._system.MemoryBus.Ready(op))
{
// Suspend operation for this cycle.
return false;
}
}
// If we have a modified next field from the last instruction, make sure it gets applied to this one.
nextModifier = _nextModifier;
_nextModifier = 0;
_rSelect = instruction.RSELECT;
// Give tasks the chance to modify parameters early on (like RSELECT)
ExecuteSpecialFunction2Early((int)instruction.F2);
// Select BUS data.
if (instruction.F1 != SpecialFunction1.Constant &&
instruction.F2 != SpecialFunction2.Constant)
{
// Normal BUS data (not constant ROM access).
switch (instruction.BS)
{
case BusSource.ReadR:
_busData = _cpu._r[_rSelect];
break;
case BusSource.LoadR:
_busData = 0; // "Loading R forces the BUS to 0 so that an ALU function of 0 and T may be executed simultaneously"
loadR = true;
break;
case BusSource.None:
_busData = 0xffff; // "Enables no source to the BUS, leaving it all ones"
break;
case BusSource.TaskSpecific1:
case BusSource.TaskSpecific2:
_busData = GetBusSource((int)instruction.BS); // task specific -- call into specific implementation
break;
case BusSource.ReadMD:
_busData = _cpu._system.MemoryBus.ReadMD();
break;
case BusSource.ReadMouse:
//throw new NotImplementedException("ReadMouse bus source not implemented.");
_busData = 0; // TODO: implement;
break;
case BusSource.ReadDisp:
// "The high-order bits of IR cannot be read directly, but the displacement field of IR (8 low order bits),
// may be read with the <-DISP bus source. If the X field of the instruction is zero (i.e. it specifies page 0
// addressing) then the DISP field of the instruction is put on BUS[8-15] and BUS[0-7] is zeroed. If the X
// field of the instruction is nonzero (i.e. it specifies PC-relative or base-register addressing) then the DISP
// field is sign-extended and put on the bus."
// NB: the "X" field of the NOVA instruction is IR[6-7]
_busData = (ushort)(_cpu._ir & 0xff);
if ((_cpu._ir & 0x300) != 0)
{
// sign extend if necessary
if ((_cpu._ir & 0x80) == 0x80)
{
_busData |= (0xff00);
}
}
break;
default:
throw new InvalidOperationException(String.Format("Unhandled bus source {0}.", instruction.BS));
break;
}
}
else
{
// See also comments below.
_busData = ConstantMemory.ConstantROM[(instruction.RSELECT << 3) | ((uint)instruction.BS)];
}
// Constant ROM access:
// The constant memory is gated to the bus by F1=7, F2=7, or BS>4. The constant memory is addressed by the
// (8 bit) concatenation of RSELECT and BS. The intent in enabling constants with BS>4 is to provide a masking
// facility, particularly for the <-MOUSE and <-DISP bus sources. This works because the processor bus ANDs if
// more than one source is gated to it. Up to 32 such mask contans can be provided for each of the four bus sources
// > 4.
// NOTE also:
// "Note that the [emulator task F2] functions which replace the low bits of RSELECT with IR aaffect only the
// selection of R; they do not affect the address supplied to the constant ROM."
// Hence we use the unmodified RSELECT value here and above.
if ((int)instruction.BS > 4 ||
instruction.F1 == SpecialFunction1.Constant ||
instruction.F2 == SpecialFunction2.Constant)
{
_busData &= ConstantMemory.ConstantROM[(instruction.RSELECT << 3) | ((uint)instruction.BS)];
}
// Do ALU operation
aluData = ALU.Execute(instruction.ALUF, _busData, _cpu._t);
// Reset shifter op
Shifter.SetOperation(ShifterOp.None, 0);
//
// Do Special Functions
//
switch(instruction.F1)
{
case SpecialFunction1.None:
// Do nothing. Well, that was easy.
break;
case SpecialFunction1.LoadMAR:
_cpu._system.MemoryBus.LoadMAR(aluData); // Start main memory reference
break;
case SpecialFunction1.Task:
nextTask = true; // Yield to other more important tasks
break;
case SpecialFunction1.Block:
// "...this function is reserved by convention only; it is *not* done by the microprocessor"
throw new InvalidOperationException("BLOCK should never be invoked by microcode.");
break;
case SpecialFunction1.LLSH1:
Shifter.SetOperation(ShifterOp.ShiftLeft, 1);
break;
case SpecialFunction1.LRSH1:
Shifter.SetOperation(ShifterOp.ShiftRight, 1);
break;
case SpecialFunction1.LLCY8:
Shifter.SetOperation(ShifterOp.RotateLeft, 8);
break;
case SpecialFunction1.Constant:
// Ignored here; handled by Constant ROM access logic above.
break;
default:
// Let the specific task implementation take a crack at this.
ExecuteSpecialFunction1((int)instruction.F1);
break;
}
switch(instruction.F2)
{
case SpecialFunction2.None:
// Nothing!
break;
case SpecialFunction2.BusEq0:
if (_busData == 0)
{
_nextModifier = 1;
}
break;
case SpecialFunction2.ShLt0:
//
// Note:
// "the value of SHIFTER OUTPUT is determined by the value of L as the microinstruction
// *begins* execution and the shifter function specified during the *current* microinstruction.
//
// Since we haven't modifed L yet, and we've selected the shifter function above, we're good to go here.
//
if ((short)Shifter.DoOperation(_cpu._l, _cpu._t) < 0)
{
_nextModifier = 1;
}
break;
case SpecialFunction2.ShEq0:
// See note above.
if (Shifter.DoOperation(_cpu._l, _cpu._t) == 0)
{
_nextModifier = 1;
}
break;
case SpecialFunction2.Bus:
// Select bits 6-15 (bits 0-9 in modern parlance) of the bus
_nextModifier = (ushort)(_busData & 0x3ff);
break;
case SpecialFunction2.ALUCY:
// ALUC0 is the carry produced by the ALU during the most recent microinstruction
// that loaded L. It is *not* the carry produced during the execution of the microinstruction
// that contains the ALUCY function.
_nextModifier = _cpu._aluC0;
break;
case SpecialFunction2.StoreMD:
_cpu._system.MemoryBus.LoadMD(_busData);
break;
case SpecialFunction2.Constant:
// Ignored here; handled by Constant ROM access logic above.
break;
default:
// Let the specific task implementation take a crack at this.
ExecuteSpecialFunction2((int)instruction.F2);
break;
}
//
// Write back to registers:
//
// Load T
if (instruction.LoadT)
{
// Does this operation change the source for T?
bool loadTFromALU = false;
switch(instruction.ALUF)
{
case AluFunction.Bus:
case AluFunction.BusOrT:
case AluFunction.BusPlus1:
case AluFunction.BusMinus1:
case AluFunction.BusPlusTPlus1:
case AluFunction.BusPlusSkip:
case AluFunction.AluBusAndT:
loadTFromALU = true;
break;
}
_cpu._t = loadTFromALU ? aluData : _busData;
}
// Load L (and M) from ALU
if (instruction.LoadL)
{
_cpu._l = aluData;
_cpu._m = aluData;
// Save ALUC0 for use in the next ALUCY special function.
_cpu._aluC0 = (ushort)ALU.Carry;
}
// Do writeback to selected R register from shifter output
if (loadR)
{
_cpu._r[_rSelect] = Shifter.DoOperation(_cpu._l, _cpu._t);
}
// Do writeback to selected R register from M
if (_loadS)
{
_cpu._s[_cpu._rb][_rSelect] = _cpu._m;
}
//
// Select next address, using the address modifier from the last instruction.
//
_mpc = (ushort)(instruction.NEXT | nextModifier);
return nextTask;
}
protected abstract ushort GetBusSource(int bs);
protected abstract void ExecuteSpecialFunction1(int f1);
/// <summary>
/// Used to allow Task-specific F2s that need to modify RSELECT to do so.
/// </summary>
/// <param name="f2"></param>
protected virtual void ExecuteSpecialFunction2Early(int f2)
{
// Nothing by default.
}
protected abstract void ExecuteSpecialFunction2(int f2);
//
// Per uInstruction Task Data:
// Modified by both the base Task implementation and any subclasses
//
// TODO: maybe instead of these being shared (which feels kinda bad)
// these could be encapsulated in an object and passed to subclass implementations?
protected ushort _busData; // Data placed onto the bus (ANDed from multiple sources)
protected ushort _nextModifier; // Bits ORed onto the NEXT field of the current instruction
protected uint _rSelect; // RSELECT field from current instruction, potentially modified by task
protected bool _loadS; // Whether to load S from M at and of cycle
//
// Global Task Data
//
protected AltoCPU _cpu;
protected ushort _mpc;
protected int _priority;
protected bool _wakeup;
}
/// <summary>
/// EmulatorTask provides emulator (NOVA instruction set) specific operations.
/// </summary>
private class EmulatorTask : Task
{
public EmulatorTask(AltoCPU cpu) : base(cpu)
{
_priority = 0;
// The Wakeup signal is always true for the Emulator task.
_wakeup = true;
}
public override void BlockTask()
{
throw new InvalidOperationException("The emulator task cannot be blocked.");
}
public override void WakeupTask()
{
throw new InvalidOperationException("The emulator task is always in wakeup state.");
}
protected override ushort GetBusSource(int bs)
{
EmulatorBusSource ebs = (EmulatorBusSource)bs;
switch(ebs)
{
case EmulatorBusSource.ReadSLocation:
return _cpu._s[_cpu._rb][_rSelect];
case EmulatorBusSource.LoadSLocation:
_loadS = true;
return 0; // TODO: technically this is an "undefined value" not zero.
default:
throw new InvalidOperationException(String.Format("Unhandled bus source {0}", bs));
}
}
protected override void ExecuteSpecialFunction1(int f1)
{
EmulatorF1 ef1 = (EmulatorF1)f1;
switch (ef1)
{
case EmulatorF1.RSNF:
// TODO: make configurable
// "...decoded by the Ethernet interface, which gates the host address wired on the
// backplane onto BUS[8-15]. BUS[0-7] is not driven and will therefore be -1. If
// no Ethernet interface is present, BUS will be -1.
//
_busData &= (0xff00 | 0x42);
break;
case EmulatorF1.STARTF:
// Dispatch function to I/O based on contents of AC0... (TBD: what are these?)
throw new NotImplementedException();
break;
default:
throw new InvalidOperationException(String.Format("Unhandled emulator F1 {0}.", ef1));
}
}
protected override void ExecuteSpecialFunction2Early(int f2)
{
EmulatorF2 ef2 = (EmulatorF2)f2;
switch (ef2)
{
case EmulatorF2.ACSOURCE:
// Early: modify R select field:
// "...it replaces the two-low order bits of the R select field with
// the complement of the SrcAC field of IR, (IR[1-2] XOR 3), allowing the emulator
// to address its accumulators (which are assigned to R0-R3)."
_rSelect = (_rSelect & 0xfffc) | ((((uint)_cpu._ir & 0x6000) >> 13) ^ 3);
break;
case EmulatorF2.ACDEST:
// "...causes (IR[3-4] XOR 3) to be used as the low-order two bits of the RSELECT field.
// This address the accumulators from the destination field of the instruction. The selected
// register may be loaded or read."
_rSelect = (_rSelect & 0xfffc) | ((((uint)_cpu._ir & 0x1800) >> 11) ^ 3);
break;
}
}
protected override void ExecuteSpecialFunction2(int f2)
{
EmulatorF2 ef2 = (EmulatorF2)f2;
switch (ef2)
{
case EmulatorF2.LoadIR:
// based on block diagram, this always comes from the bus
_cpu._ir = _busData;
// "IR<- also merges bus bits 0, 5, 6 and 7 into NEXT[6-9] which does a first level
// instruction dispatch."
// TODO: is this an AND or an OR operation? (how is the "merge" done?)
// Assuming for now this is an OR operation like everything else that modifies NEXT.
_nextModifier = (ushort)(((_busData & 0x8000) >> 6) | ((_busData & 0x0700) >> 2));
break;
case EmulatorF2.IDISP:
// "The IDISP function (F2=15B) does a 16 way dispatch under control of a PROM and a
// multiplexer. The values are tabulated below:
// Conditions ORed onto NEXT Comment
//
// if IR[0] = 1 3-IR[8-9] complement of SH field of IR
// elseif IR[1-2] = 0 IR[3-4] JMP, JSR, ISZ, DSZ
// elseif IR[1-2] = 1 4 LDA
// elseif IR[1-2] = 2 5 STA
// elseif IR[4-7] = 0 1
// elseif IR[4-7] = 1 0
// elseif IR[4-7] = 6 16B CONVERT
// elseif IR[4-7] = 16B 6
// else IR[4-7]
// NB: as always, Xerox labels bits in the opposite order from modern convention;
// (bit 0 is bit 15...)
if ((_cpu._ir & 0x8000) != 0)
{
_nextModifier = (ushort)(3 - ((_cpu._ir & 0xc0) >> 6));
}
else if((_cpu._ir & 0x6000) == 0)
{
_nextModifier = (ushort)((_cpu._ir & 0x1800) >> 11);
}
else if((_cpu._ir & 0x6000) == 0x4000)
{
_nextModifier = 4;
}
else if ((_cpu._ir & 0x6000) == 0x6000)
{
_nextModifier = 5;
}
else if ((_cpu._ir & 0x0f00) == 0)
{
_nextModifier = 1;
}
else if ((_cpu._ir & 0x0f00) == 0x0100)
{
_nextModifier = 0;
}
else if ((_cpu._ir & 0x0f00) == 0x0600)
{
_nextModifier = 0xe;
}
else if ((_cpu._ir & 0x0f00) == 0x0e00)
{
_nextModifier = 0x6;
}
else
{
_nextModifier = (ushort)((_cpu._ir & 0x0f00) >> 8);
}
break;
case EmulatorF2.ACSOURCE:
// Late:
// "...a dispatch is performed:
// Conditions ORed onto NEXT Comment
//
// if IR[0] = 1 3-IR[8-9] complement of SH field of IR
// if IR[1-2] = 3 IR[5] the Indirect bit of R
// if IR[3-7] = 0 2 CYCLE
// if IR[3-7] = 1 5 RAMTRAP
// if IR[3-7] = 2 3 NOPAR -- parameterless opcode group
// if IR[3-7] = 3 6 RAMTRAP
// if IR[3-7] = 4 7 RAMTRAP
// if IR[3-7] = 11B 4 JSRII
// if IR[3-7] = 12B 4 JSRIS
// if IR[3-7] = 16B 1 CONVERT
// if IR[3-7] = 37B 17B ROMTRAP -- used by Swat, the debugger
// else 16B ROMTRAP
if ((_cpu._ir & 0x8000) != 0)
{
_nextModifier = (ushort)(3 - ((_cpu._ir & 0xc0) >> 6));
}
else if ((_cpu._ir & 0xc000) == 0xc000)
{
_nextModifier = (ushort)((_cpu._ir & 0x400) >> 10);
}
else if ((_cpu._ir & 0x1f00) == 0)
{
_nextModifier = 2;
}
else if ((_cpu._ir & 0x1f00) == 0x0100)
{
_nextModifier = 5;
}
else if ((_cpu._ir & 0x1f00) == 0x0200)
{
_nextModifier = 3;
}
else if ((_cpu._ir & 0x1f00) == 0x0300)
{
_nextModifier = 6;
}
else if ((_cpu._ir & 0x1f00) == 0x0400)
{
_nextModifier = 7;
}
else if ((_cpu._ir & 0x1f00) == 0x0900)
{
_nextModifier = 4;
}
else if ((_cpu._ir & 0x1f00) == 0x0a00)
{
_nextModifier = 4;
}
else if ((_cpu._ir & 0x1f00) == 0x0e00)
{
_nextModifier = 1;
}
else if ((_cpu._ir & 0x1f00) == 0x1f00)
{
_nextModifier = 0xf;
}
else
{
_nextModifier = 0xe;
}
break;
case EmulatorF2.ACDEST:
// Handled in early handler
break;
case EmulatorF2.BUSODD:
// "...merges BUS[15] into NEXT[9]."
// TODO: is this an AND or an OR?
_nextModifier = (ushort)((_nextModifier & 0xffbf) | ((_busData & 0x1) << 6));
break;
case EmulatorF2.MAGIC:
Shifter.SetMagic(true);
break;
default:
throw new InvalidOperationException(String.Format("Unhandled emulator F2 {0}.", ef2));
}
}
}
// AltoCPU registers
ushort _t;
ushort _l;
ushort _m;
ushort _ir;
// R and S register files and bank select
ushort[] _r;
ushort[][] _s;
ushort _rb; // S register bank select
// Stores the last carry from the ALU on a Load L
private ushort _aluC0;
// Task data
private Task _nextTask; // The task to switch two after the next microinstruction
private Task _currentTask; // The currently executing task
private Task[] _tasks = new Task[16];
private long _clocks;
// The system this CPU belongs to
private AltoSystem _system;
}
}