1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-16 16:19:33 +00:00

583 lines
24 KiB
C#

using System;
using Contralto.Memory;
using Contralto.Logging;
namespace Contralto.CPU
{
public partial class AltoCPU
{
public enum InstructionCompletion
{
Normal,
TaskSwitch,
MemoryWait,
}
// 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
_taskType = TaskType.Invalid;
_cpu = cpu;
_block = false;
}
public int Priority
{
get { return (int)_taskType; }
}
public TaskType TaskType
{
get { return _taskType; }
}
public bool Wakeup
{
get { return _wakeup; }
}
public ushort MPC
{
get { return _mpc; }
}
/// <summary>
/// Indicates whether a task switch just happened. TASK instructions behave differently on the
/// first instruction after a switch. This is not documented, but observed on the real hardware.
/// (See the implementation of the Task SF for more details.)
/// </summary>
public bool FirstInstructionAfterSwitch
{
get { return _firstInstructionAfterSwitch; }
set { _firstInstructionAfterSwitch = value; }
}
/// <summary>
/// Indicates whether the current uInstruction asserts BLOCK.
/// Used by hardware for various tasks.
/// </summary>
public bool BLOCK
{
get { return _block; }
}
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)_taskType;
_rdRam = false;
_rb = 0;
_firstInstructionAfterSwitch = false;
_swMode = false;
_wrtRam = false;
}
public virtual void SoftReset()
{
//
// As above but we leave all other state alone.
//
_mpc = (ushort)_taskType;
}
public virtual void BlockTask()
{
_wakeup = false;
}
public virtual void WakeupTask()
{
_wakeup = true;
}
public InstructionCompletion ExecuteNext()
{
MicroInstruction instruction = UCodeMemory.GetInstruction(_mpc, _taskType);
// Grab BLOCK bit so that other tasks / hardware can look at it
_block = instruction.F1 == SpecialFunction1.Block;
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 InstructionCompletion ExecuteInstruction(MicroInstruction instruction)
{
InstructionCompletion completion = InstructionCompletion.Normal;
bool swMode = false;
bool block = false;
ushort aluData;
ushort nextModifier;
_loadR = false;
_loadS = false;
_rSelect = 0;
_srSelect = 0;
_busData = 0;
_softReset = false;
Shifter.SetMagic(false);
Shifter.SetDNS(false, 0);
//
// Wait for memory state machine if a memory operation is requested by this instruction and
// the memory isn't ready yet.
//
if (instruction.MemoryAccess)
{
if (!_cpu._system.MemoryBus.Ready(instruction.MemoryOperation))
{
// Suspend operation for this cycle.
return InstructionCompletion.MemoryWait;
}
}
// If we have a modified next field from the last instruction, make sure it gets applied to this one.
nextModifier = _nextModifier;
_nextModifier = 0;
_srSelect = _rSelect = instruction.RSELECT;
// Give tasks the chance to modify parameters early on (like RSELECT)
ExecuteSpecialFunction2Early(instruction);
// Select BUS data.
if (!instruction.ConstantAccess)
{
// 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:
_busData = _cpu._system.Mouse.PollMouseBits();
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));
}
}
else
{
// See also comments below.
_busData = ControlROM.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 affect 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.ConstantAccess)
{
_busData &= ControlROM.ConstantROM[(instruction.RSELECT << 3) | ((uint)instruction.BS)];
}
//
// If there was a RDRAM operation last cycle, we AND in the uCode RAM data here.
//
if (_rdRam)
{
_busData &= UCodeMemory.ReadRAM();
_rdRam = false;
}
//
// Let F1s that need to modify bus data before the ALU runs do their thing
// (this is used by the emulator RSNF and Ethernet EILFCT)
//
ExecuteSpecialFunction1Early(instruction);
// Do ALU operation.
// Small optimization: if we're just taking bus data across the ALU, we
// won't go through the ALU.Execute call; this is a decent performance gain for a bit
// more ugly code...
if (instruction.ALUF != AluFunction.Bus)
{
aluData = ALU.Execute(instruction.ALUF, _busData, _cpu._t, _skip);
}
else
{
aluData = _busData;
ALU.Carry = 0;
}
//
// If there was a WRTRAM operation last cycle, we write the uCode RAM here
// using the results of this instruction's ALU operation and the M register
// from the last instruction.
//
if (_wrtRam)
{
UCodeMemory.WriteRAM(aluData, _cpu._m);
_wrtRam = false;
}
//
// If there was an SWMODE operation last cycle, we set the flag to ensure it
// takes effect at the end of this cycle.
//
if (_swMode)
{
_swMode = false;
swMode = true;
}
// 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:
// Do MAR or XMAR reference based on whether F2 is MD<-, indicating an extended memory reference.
_cpu._system.MemoryBus.LoadMAR(aluData, _taskType, instruction.F2 == SpecialFunction2.StoreMD);
break;
case SpecialFunction1.Task:
//
// If the first uOp executed after a task switch contains a TASK F1, it does not take effect.
// This is observed on the real hardware, and does not appear to be documented.
// It also doensn't appear to affect the execution of the standard Alto uCode in any significant
// way, but is included here for correctness.
//
//if (!_firstInstructionAfterSwitch)
{
// Yield to other more important tasks
completion = InstructionCompletion.TaskSwitch;
}
break;
case SpecialFunction1.Block:
// Technically this is to be invoked by the hardware device associated with a task.
// That logic would be circuituous and unless there's a good reason not to that is discovered
// later, I'm just going to directly block the current task here.
_cpu.BlockTask(this._taskType);
// Let task-specific behavior take place at the end of this cycle.
block = true;
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(instruction);
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:
// Special case for XMAR; if F1 is a LoadMAR we do nothing here;
// the handler for LoadMAR will load the correct bank.
if (instruction.F1 != SpecialFunction1.LoadMAR)
{
_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(instruction);
break;
}
// We always do the shifter operation; DNS may need its output.
//
Shifter.DoOperation(_cpu._l, _cpu._t);
//
// Write back to registers:
//
// Do writeback to selected R register from shifter output.
//
if (_loadR)
{
_cpu._r[_rSelect] = Shifter.Output;
}
// Do writeback to selected S register from M
if (_loadS)
{
_cpu._s[_rb][_srSelect] = _cpu._m;
}
// Load T
if (instruction.LoadT)
{
// Does this operation change the source for T?
_cpu._t = instruction.LoadTFromALU ? aluData : _busData;
//
// Control RAM: "...the control RAM address is specified by the control RAM
// address register... which is loaded from the ALU output whenver T is loaded
// from its source."
//
UCodeMemory.LoadControlRAMAddress(aluData);
}
// Load L (and M) from ALU outputs.
if (instruction.LoadL)
{
_cpu._l = aluData;
// Only RAM-related tasks can modify M. (Currently only the Emulator.)
if (_taskType == TaskType.Emulator)
{
_cpu._m = aluData;
}
// Save ALUC0 for use in the next ALUCY special function.
_cpu._aluC0 = (ushort)ALU.Carry;
}
//
// Execute special functions that happen late in the cycle
//
ExecuteSpecialFunction2Late(instruction);
//
// Switch banks if the last instruction had an SWMODE F1;
// this depends on the value of the NEXT field in this instruction.
// (And apparently the modifier applied to NEXT in this instruction -- MADTEST expects this.)
//
if (swMode)
{
// Log.Write(Logging.LogComponent.Microcode, "SWMODE: uPC {0}, next uPC {1} (NEXT is {2})", Conversion.ToOctal(_mpc), Conversion.ToOctal(instruction.NEXT | nextModifier), Conversion.ToOctal(instruction.NEXT));
UCodeMemory.SwitchMode((ushort)(instruction.NEXT | nextModifier), _taskType);
}
//
// Do task-specific BLOCK behavior if the last instruction had a BLOCK F1.
//
if (block)
{
ExecuteBlock();
}
//
// Select next address, using the address modifier from the last instruction.
// (Unless a soft reset occurred during this instruction)
//
if (!_softReset)
{
_mpc = (ushort)(instruction.NEXT | nextModifier);
}
_firstInstructionAfterSwitch = false;
return completion;
}
protected virtual ushort GetBusSource(int bs)
{
// Nothing by default.
return 0;
}
protected virtual void ExecuteSpecialFunction1Early(MicroInstruction instruction)
{
// Nothing by default
}
protected virtual void ExecuteSpecialFunction1(MicroInstruction instruction)
{
// Nothing by default
}
/// <summary>
/// Used to allow Task-specific F2s that need to modify RSELECT to do so.
/// </summary>
/// <param name="f2"></param>
protected virtual void ExecuteSpecialFunction2Early(MicroInstruction instruction)
{
// Nothing by default.
}
protected virtual void ExecuteSpecialFunction2(MicroInstruction instruction)
{
// Nothing by default.
}
protected virtual void ExecuteSpecialFunction2Late(MicroInstruction instruction)
{
// Nothing by default.
}
/// <summary>
/// Allows task-specific handling for BLOCK microinstructions.
/// (Disk and Display BLOCKs have side effects apart from removing Wakeup from the task, for example).
/// </summary>
protected virtual void ExecuteBlock()
{
// Nothing by default
}
//
// 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 uint _srSelect; // RSELECT field as used by S register access (not modified in the same way as normal _rSelect).
protected bool _loadS; // Whether to load S from M at and of cycle
protected bool _loadR; // Whether to load R from shifter at end of cycle.
protected bool _rdRam; // Whether to load uCode RAM onto the bus during the next cycle.
protected bool _wrtRam; // Whether to write uCode RAM from M and ALU outputs during the next cycle.
protected bool _swMode; // Whether to switch uCode banks during the next cycle.
protected bool _softReset; // Whether this instruction caused a soft reset (so MPC should not come from instruction's NEXT field)
//
// Global Task Data
//
protected AltoCPU _cpu;
protected ushort _mpc;
protected ushort _rb; // S register bank select
protected TaskType _taskType;
protected bool _wakeup;
protected bool _firstInstructionAfterSwitch;
protected bool _block;
// Emulator Task-specific data. This is placed here because it is used by the ALU and it's easier to reference in the
// base class even if it does break encapsulation. See notes in the EmulatorTask class for meaning.
protected int _skip;
}
}
}