1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-13 15:18:23 +00:00

695 lines
28 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 System;
using Contralto.Memory;
using Contralto.Logging;
using System.IO;
namespace Contralto.CPU
{
public partial class AltoCPU
{
public enum InstructionCompletion
{
Normal,
TaskSwitch,
MemoryWait,
}
/// <summary>
/// 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.
/// </summary>
public abstract class Task
{
public Task(AltoCPU cpu)
{
_wakeup = false;
_mpc = 0xffff; // invalid, for sanity checking
_taskType = TaskType.Invalid;
_cpu = cpu;
_systemType = Configuration.SystemType;
}
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; }
}
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;
_wakeup = false;
_skip = 0;
}
public virtual void SoftReset()
{
//
// As above but we leave all other state alone.
//
_mpc = (ushort)_taskType;
}
/// <summary>
/// Removes the Wakeup signal for this task.
/// </summary>
public virtual void BlockTask()
{
_wakeup = false;
}
/// <summary>
/// Sets the Wakeup signal for this task.
/// </summary>
public virtual void WakeupTask()
{
_wakeup = true;
}
/// <summary>
/// Executes a single microinstruction.
/// </summary>
/// <returns>An InstructionCompletion indicating whether this instruction calls for a task switch or not.</returns>
public InstructionCompletion ExecuteNext()
{
MicroInstruction instruction = UCodeMemory.GetInstruction(_mpc, _taskType);
/*
if (_taskType == TaskType.Emulator && UCodeMemory.GetBank(_taskType) == MicrocodeBank.RAM0)
{
Console.WriteLine("{0}: {1}", Conversion.ToOctal(_mpc), UCodeDisassembler.DisassembleInstruction(instruction, _taskType));
}*/
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 (specific task implementations) may provide their own implementations.
/// </summary>
/// <returns>An InstructionCompletion indicating whether this instruction calls for a task switch or not.</returns>
protected virtual InstructionCompletion ExecuteInstruction(MicroInstruction instruction)
{
InstructionCompletion completion = InstructionCompletion.Normal;
bool swMode = false;
ushort aluData;
ushort nextModifier;
bool softReset = _softReset;
_loadR = false;
_loadS = false;
_rSelect = 0;
_busData = 0;
_softReset = false;
Shifter.Reset();
//
// 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;
_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(instruction); // task specific -- call into specific implementation
break;
case BusSource.ReadMD:
_busData = _cpu._system.MemoryBus.ReadMD();
break;
case BusSource.ReadMouse:
// "BUS[12-15]<-MOUSE; BUS[0-13]<- -1"
// (Note -- BUS[0-13] appears to be a typo, and should likely be BUS[0-11]).
_busData = (ushort)(_cpu._system.MouseAndKeyset.PollMouseBits() | 0xfff0);
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) != 0)
{
_busData |= (0xff00);
}
}
break;
default:
throw new InvalidOperationException(String.Format("Unhandled bus source {0}.", instruction.BS));
}
}
else
{
// See also comments below.
_busData = instruction.ConstantValue;
}
// 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."
// This is precached by the MicroInstruction object.
if (instruction.BS4)
{
_busData &= instruction.ConstantValue;
}
//
// Let F2s that need to modify bus data before the ALU runs do their thing.
// (This is used by the Trident KDTA special functions)
//
ExecuteSpecialFunction2PostBusSource(instruction);
//
// 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;
}
//
// 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<- (for Alto IIs), indicating an extended memory reference.
_cpu._system.MemoryBus.LoadMAR(
aluData,
_taskType,
_systemType == SystemType.AltoI ? false : 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 doesn'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);
break;
case SpecialFunction1.LLSH1:
Shifter.SetOperation(ShifterOp.ShiftLeft);
break;
case SpecialFunction1.LRSH1:
Shifter.SetOperation(ShifterOp.ShiftRight);
break;
case SpecialFunction1.LLCY8:
Shifter.SetOperation(ShifterOp.RotateLeft);
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:
// Handled below, after the Shifter runs
break;
case SpecialFunction2.ShEq0:
// Same as above.
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 on non-Alto I machines: if F1 is a LoadMAR we do nothing here;
// the handler for LoadMAR will load the correct bank.
if (_systemType == SystemType.AltoI)
{
_cpu._system.MemoryBus.LoadMD(_busData);
}
else 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;
}
//
// Do the shifter operation if we're doing an operation that requires the shifter output (loading R, doing a LoadDNS,
// modifying NEXT based on the shifter output.)
//
if (_loadR || instruction.NeedShifterOutput)
{
// A crude optimization: if there's no shifter operation,
// we bypass the call to DoOperation and stuff L in Shifter.Output ourselves.
if (Shifter.Op == ShifterOp.None)
{
Shifter.Output = _cpu._l;
}
else
{
Shifter.DoOperation(_cpu._l, _cpu._t);
}
}
//
// Handle NEXT modifiers that rely on the Shifter output.
//
switch(instruction.F2)
{
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 calculated the shifter output above, we're good to go here.
//
if ((short)Shifter.Output < 0)
{
_nextModifier |= 1;
}
break;
case SpecialFunction2.ShEq0:
// See note above.
if (Shifter.Output == 0)
{
_nextModifier |= 1;
}
break;
}
//
// 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][instruction.RSELECT] = _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.
if (_ramTask)
{
_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(LogType.Verbose, 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 (instruction.F1 == SpecialFunction1.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);
}
else
{
_cpu.SoftReset();
}
_firstInstructionAfterSwitch = false;
return completion;
}
/// <summary>
/// Provides task-specific implementations the opportunity to handle task-specific bus sources.
/// </summary>
/// <param name="bs"></param>
/// <returns></returns>
protected virtual ushort GetBusSource(MicroInstruction instruction)
{
// Nothing by default.
return 0;
}
/// <summary>
/// Executes before the ALU runs but after bus sources have been selected.
/// </summary>
/// <param name="instruction"></param>
protected virtual void ExecuteSpecialFunction1Early(MicroInstruction instruction)
{
// Nothing by default
}
/// <summary>
/// Executes after the ALU has run but before the shifter runs, provides task-specific implementations
/// the opportunity to handle task-specific F1s.
/// </summary>
/// <param name="instruction"></param>
protected virtual void ExecuteSpecialFunction1(MicroInstruction instruction)
{
// Nothing by default
}
/// <summary>
/// Executes before bus sources are selected. 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.
}
/// <summary>
/// Executes after bus sources are selected but before the ALU. Used to allow Task-specific F2s that need to
/// modify bus data to do so.
/// </summary>
/// <param name="f2"></param>
protected virtual void ExecuteSpecialFunction2PostBusSource(MicroInstruction instruction)
{
// Nothing by default.
}
/// <summary>
/// Executes after the ALU has run but before the shifter runs, provides task-specific implementations
/// the opportunity to handle task-specific F2s.
/// </summary>
protected virtual void ExecuteSpecialFunction2(MicroInstruction instruction)
{
// Nothing by default.
}
/// <summary>
/// Executes after the shifter has run, provides task-specific implementations the opportunity
/// to handle task-specific F2s late in the cycle.
/// </summary>
/// <param name="instruction"></param>
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
}
/// <summary>
/// Allows task-specific behavior when a new task begins execution.
/// (Generally this is used to remove wakeup immediately.)
/// </summary>
public virtual void OnTaskSwitch()
{
// Nothing by default
}
/// <summary>
/// Cache the system type.
/// </summary>
protected SystemType _systemType;
//
// Task metadata:
//
// Whether this task is RAM-enabled or not
// (can access S registers, etc.)
protected bool _ramTask;
//
// 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
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;
// 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;
}
}
}