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

568 lines
22 KiB
C#

using Contralto.CPU;
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
{
public class EthernetController
{
public EthernetController(AltoSystem system)
{
_system = system;
_receiverLock = new System.Threading.ReaderWriterLockSlim();
_fifo = new Queue<ushort>();
Reset();
_fifoTransmitWakeupEvent = new Event(_fifoTransmitDuration, null, OutputFifoCallback);
// Attach real Ethernet device if user has specified one, otherwise leave unattached; output data
// will go into a bit-bucket.
try
{
switch (Configuration.HostPacketInterfaceType)
{
case PacketInterfaceType.UDPEncapsulation:
_hostInterface = new UDPEncapsulation(Configuration.HostPacketInterfaceName);
_hostInterface.RegisterReceiveCallback(OnHostPacketReceived);
break;
case PacketInterfaceType.EthernetEncapsulation:
_hostInterface = new HostEthernetEncapsulation(Configuration.HostPacketInterfaceName);
_hostInterface.RegisterReceiveCallback(OnHostPacketReceived);
break;
default:
_hostInterface = null;
break;
}
}
catch
{
_hostInterface = null;
}
// More words than the Alto will ever send.
_outputData = new ushort[4096];
_nextPackets = new Queue<MemoryStream>();
}
public void Reset()
{
_inputPollEvent = null;
ResetInterface();
}
public byte Address
{
get { return Configuration.HostAddress; }
}
/// <summary>
/// The ICMD and OCMD flip-flops, combined into a single value
/// as written by STARTF.
/// (bit 15 = OCMD, bit 14 = ICMD)
/// </summary>
public int IOCMD
{
get { return _ioCmd; }
}
public bool FIFOEmpty
{
get { return _fifo.Count == 0; }
}
public bool OperationDone
{
get { return !_oBusy && !_iBusy; }
}
public bool Collision
{
get { return _collision; }
}
public bool DataLate
{
get { return _dataLate; }
}
public ushort Status
{
get
{
return _status;
}
}
public bool CountdownWakeup
{
get { return _countdownWakeup; }
set { _countdownWakeup = value; }
}
public void ResetInterface()
{
// Latch status before resetting
_status = (ushort)(
(0xffc0) | // bits always set
(_dataLate ? 0x00 : 0x20) |
(_collision ? 0x00 : 0x10) |
(_crcBad ? 0x00 : 0x08) |
((~0 & 0x3) << 1) | // TODO: we're clearing the IOCMD bits here early -- validate why this works.
(_incomplete ? 0x00 : 0x01));
_ioCmd = 0;
_oBusy = false;
_iBusy = false;
_dataLate = false;
_collision = false;
_crcBad = false;
_incomplete = false;
_fifo.Clear();
_incomingPacket = null;
_incomingPacketLength = 0;
_inGone = false;
_inputState = InputState.ReceiverOff;
if (_system.CPU != null)
{
_system.CPU.BlockTask(TaskType.Ethernet);
}
Log.Write(LogComponent.EthernetController, "Interface reset.");
if (_inputPollEvent == null)
{
// Kick off the input poll event which will run forever.
_inputPollEvent = new Event(_inputPollPeriod, null, InputHandler);
_system.Scheduler.Schedule(_inputPollEvent);
}
}
public ushort ReadInputFifo(bool lookOnly)
{
if (FIFOEmpty)
{
Log.Write(LogComponent.EthernetController, "Read from empty Ethernet FIFO, returning 0.");
return 0;
}
ushort read = 0;
if (lookOnly)
{
Log.Write(LogComponent.EthernetController, "Peek into FIFO, returning {0} (length {1})", Conversion.ToOctal(_fifo.Peek()), _fifo.Count);
read = _fifo.Peek();
}
else
{
read = _fifo.Dequeue();
Log.Write(LogComponent.EthernetController, "Read from FIFO, returning {0} (length now {1})", Conversion.ToOctal(read), _fifo.Count);
if (_fifo.Count < 2)
{
if (_inGone)
{
//
// Receiver is done and we're down to the last word (the checksum)
// which never gets pulled from the FIFO.
// clear IBUSY to indicate to the microcode that we've finished.
//
_iBusy = false;
_system.CPU.WakeupTask(TaskType.Ethernet);
}
else
{
//
// Still more data, but we block the Ethernet task until it is put
// into the FIFO.
//
_system.CPU.BlockTask(TaskType.Ethernet);
}
}
}
return read;
}
public void WriteOutputFifo(ushort data)
{
if (_fifo.Count == 16)
{
Log.Write(LogComponent.EthernetController, "Write to full Ethernet FIFO, losing first entry.");
_fifo.Dequeue();
}
_fifo.Enqueue(data);
// If the FIFO is full, start transmitting and clear Wakeups
if (_fifo.Count == 15)
{
if (_oBusy)
{
TransmitFIFO(false /* not end */);
}
_system.CPU.BlockTask(TaskType.Ethernet);
}
Log.Write(LogComponent.EthernetController, "FIFO written with {0}, length now {1}", data, _fifo.Count);
}
public void StartOutput()
{
// Sets the OBusy flip-flop in the interface
_oBusy = true;
// Enables wakeups to fill the FIFO
_system.CPU.WakeupTask(TaskType.Ethernet);
Log.Write(LogComponent.EthernetController, "Output started.");
}
public void StartInput()
{
InitializeReceiver();
Log.Write(LogComponent.EthernetController, "Input started.");
}
public void EndTransmission()
{
// Clear FIFO wakeup and transmit the remainder of the data in the FIFO
TransmitFIFO(true /* end */);
_system.CPU.BlockTask(TaskType.Ethernet);
Log.Write(LogComponent.EthernetController, "Transmission ended.");
}
public void STARTF(ushort busData)
{
Log.Write(LogComponent.EthernetController, "Ethernet STARTF {0}", Conversion.ToOctal(busData));
//
// HW Manual, p. 54:
// "The emulator task sets [the ICMD and OCMD flip flops] from BUS[14 - 15] with
// the STARTF function, causing the Ethernet task to wakeup, dispatch on them
// and then reset them with EPFCT."
//
_ioCmd = busData & 0x3;
_system.CPU.WakeupTask(TaskType.Ethernet);
}
private void TransmitFIFO(bool end)
{
// Schedule a callback to pick up the data and shuffle it out the host interface.
_fifoTransmitWakeupEvent.Context = end;
_fifoTransmitWakeupEvent.TimestampNsec = _fifoTransmitDuration;
_system.Scheduler.Schedule(_fifoTransmitWakeupEvent);
}
private void OutputFifoCallback(ulong timeNsec, ulong skewNsec, object context)
{
bool end = (bool)context;
if (!_oBusy)
{
// If OBUSY is no longer set then the interface was reset before
// we got to run; abandon this operation.
Log.Write(LogComponent.EthernetController, "FIFO callback after reset, abandoning output.");
return;
}
Log.Write(LogComponent.EthernetController, "Sending {0} words from fifo.", _fifo.Count);
// Copy FIFO to host ethernet output buffer
_fifo.CopyTo(_outputData, _outputIndex);
_outputIndex += _fifo.Count;
_fifo.Clear();
if (!end)
{
// Enable FIFO microcode wakeups for next batch of data
_system.CPU.WakeupTask(TaskType.Ethernet);
}
else
{
// This is the last of the data, clear the OBUSY flipflop, the transmitter is done.
Log.Write(LogComponent.EthernetController, "Packet complete.");
_oBusy = false;
// Wakeup at end of transmission. ("OUTGONE Post wakeup.")
_system.CPU.WakeupTask(TaskType.Ethernet);
// And actually tell the host ethernet interface to send the data.
// NOTE: We do not append a checksum to the outgoing 3mbit packet. See comments on the
// receiving end for an explanation.
if (_hostInterface != null)
{
_hostInterface.Send(_outputData, _outputIndex);
}
_outputIndex = 0;
}
}
private void InitializeReceiver()
{
// " Sets the IBusy flip flop in the interface..."
// "...restarting the receiver... causes [the controller] to ignore the current packet and hunt
// for the beginning of the next packet."
//
// So, two things:
// 1) Cancel any pending input packet
// 2) Start listening for more packets if we weren't already doing so.
//
if (_iBusy)
{
Log.Write(LogComponent.EthernetController, "Receiver initializing, dropping current activity.");
_incomingPacket = null;
_incomingPacketLength = 0;
}
_inputState = InputState.ReceiverWaiting;
_iBusy = true;
_system.CPU.BlockTask(TaskType.Ethernet);
Log.Write(LogComponent.EthernetController, "Receiver initialized.");
}
/// <summary>
/// Invoked when the host ethernet interface receives a packet destined for us.
/// NOTE: This runs on the PCap receiver thread (see HostEthernet), not the main emulator thread.
/// Any access to emulator structures must be properly protected.
///
/// Due to the nature of the "ethernet" we're simulating, there will never be any collisions or corruption and
/// everything is completely asynchronous with regard to all receivers, as such it's completely possible
/// for packets to be received by the host interface when the emulated interface is already sending/receiving
/// a 3mbit packet (something that could never happen in reality). There is no reasonable way to change this behavior
/// without having a distributed synchronization across emulator processes to more accurately simulate the behavior
/// of a real ethernet, and that seems like complete overkill (and gets even more complicated if we end up using transports
/// other than raw Ethernet in the future.)
///
/// To compensate for this somewhat, we queue up received packets (to an upper limit of 32), these will either be consumed or discarded
/// by InputHandler (which runs periodically on the emulator thread) depending on the current state of the interface.
/// This reduces the number of dropped packets and seems to work fairly well.
///
/// </summary>
/// <param name="data"></param>
private void OnHostPacketReceived(MemoryStream data)
{
_receiverLock.EnterWriteLock();
if (_nextPackets.Count < _maxQueuedPackets)
{
_nextPackets.Enqueue(data);
}
else
{
Log.Write(LogType.Error, LogComponent.EthernetPacket, "Input packet queue has reached its limit of {0} packets, dropping oldest packet.", _maxQueuedPackets);
_nextPackets.Dequeue();
_nextPackets.Enqueue(data);
}
_receiverLock.ExitWriteLock();
}
/// <summary>
/// Runs the input state machine. This runs periodically (as scheduled by the Scheduler) and:
/// 1) Ignores incoming packets if the receiver is off.
/// 2) Pulls incoming packets from the queue if the interface is active
/// 3) Reads words from incoming packets into the controller's FIFO
/// </summary>
/// <param name="timeNsec"></param>
/// <param name="skewNsec"></param>
/// <param name="context"></param>
private void InputHandler(ulong timeNsec, ulong skewNsec, object context)
{
switch(_inputState)
{
case InputState.ReceiverOff:
// Receiver is off, if we have any incoming packets, they are ignored.
// TODO: would it make sense to expire really old packets (say more than a couple of seconds old)
// so that the receiver doesn't pick up ancient history the next time it runs?
// We already cycle out packets as new ones come in, so this would only be an issue on very quiet networks.
// (And even then I don't know if it's really an issue.)
_receiverLock.EnterReadLock();
if (_nextPackets.Count > 0)
{
Log.Write(LogComponent.EthernetPacket, "Receiver is off, ignoring incoming packet from packet queue.");
}
_receiverLock.ExitReadLock();
break;
case InputState.ReceiverWaiting:
// Receiver is on, waiting for a new packet. If we have one now, start an
// input operation.
_receiverLock.EnterReadLock();
if (_nextPackets.Count > 0)
{
_incomingPacket = _nextPackets.Dequeue();
//
// Read the packet length (in words) (first word of the packet as provided by the sending emulator). Convert to bytes.
//
_incomingPacketLength = ((_incomingPacket.ReadByte()) | (_incomingPacket.ReadByte() << 8)) * 2;
// Add one word to the count for the checksum.
// NOTE: This is not provided by the sending emulator and is not computed here either.
// The microcode does not use it and any corrupted packets will be dealt with transparently by the host interface,
// not the emulator.
// We add the word to the count because the microcode expects to read it in from the input FIFO, it is then dropped.
//
_incomingPacketLength += 2;
// Sanity check:
if (_incomingPacketLength > _incomingPacket.Length ||
(_incomingPacketLength % 2) != 0)
{
throw new InvalidOperationException(
String.Format("Invalid 3mbit packet length header ({0} vs {1}.", _incomingPacketLength, _incomingPacket.Length));
}
Log.Write(LogComponent.EthernetPacket, "Accepting incoming packet (length {0}).", _incomingPacketLength);
//LogPacket(_incomingPacketLength, _incomingPacket);
// Move to the Receiving state.
_inputState = InputState.Receiving;
}
_receiverLock.ExitReadLock();
break;
case InputState.Receiving:
Log.Write(LogComponent.EthernetController, "Processing word from input packet ({0} bytes left in input, {1} words in FIFO.)", _incomingPacketLength, _fifo.Count);
if (_fifo.Count >= 16)
{
// This shouldn't happen.
Log.Write(LogComponent.EthernetController, "Input FIFO full, Scheduling next wakeup. No words added to the FIFO.");
break;
}
if (_incomingPacketLength >= 2)
{
// Stuff 1 word into the FIFO, if we run out of data to send then we clear _iBusy further down.
ushort nextWord = (ushort)((_incomingPacket.ReadByte()) | (_incomingPacket.ReadByte() << 8));
_fifo.Enqueue(nextWord);
_incomingPacketLength -= 2;
}
else if (_incomingPacketLength == 1)
{
// Should never happen.
throw new InvalidOperationException("Packet length not multiple of 2 on receive.");
}
// All out of data? Finish the receive operation.
if (_incomingPacketLength == 0)
{
_inGone = true;
_incomingPacket = null;
_inputState = InputState.ReceiverDone;
// Wakeup Ethernet task for end of data.
_system.CPU.WakeupTask(TaskType.Ethernet);
Log.Write(LogComponent.EthernetController, "Receive complete.");
}
// Wake up the Ethernet task to process data if we have
// more than two words in the FIFO.
if (_fifo.Count >= 2)
{
_system.CPU.WakeupTask(TaskType.Ethernet);
}
break;
case InputState.ReceiverDone:
// Nothing, we just wait in this state for the receiver to be reset by the microcode.
break;
}
// Schedule the next wakeup.
_inputPollEvent.TimestampNsec = _inputPollPeriod - skewNsec;
_system.Scheduler.Schedule(_inputPollEvent);
}
private void LogPacket(int length, MemoryStream packet)
{
Log.Write(LogComponent.EthernetPacket,
" - Packet src {0}, dest {1}, length {2}",
packet.ReadByte(), packet.ReadByte(), length);
// Return to top of packet
packet.Position = 2;
}
private Queue<ushort> _fifo;
// Bits in Status register
private int _ioCmd;
private bool _dataLate;
private bool _collision;
private bool _crcBad;
private bool _incomplete;
private ushort _status;
private bool _countdownWakeup;
private bool _oBusy;
private bool _iBusy;
private bool _inGone;
// FIFO scheduling
// Transmit:
private ulong _fifoTransmitDuration = 87075; // ~87000 nsec to transmit 16 words at 3mbit, assuming no collision
private Event _fifoTransmitWakeupEvent;
// Receive:
private ulong _inputPollPeriod = 5400; // ~5400 nsec to receive 1 word at 3mbit
private Event _inputPollEvent;
// Input states
private enum InputState
{
ReceiverOff = 0,
ReceiverWaiting,
Receiving,
ReceiverDone,
}
private InputState _inputState;
private const int _maxQueuedPackets = 32;
// The actual connection to a real network device of some sort on the host
IPacketEncapsulation _hostInterface;
// Buffer to hold outgoing data to the host ethernet
ushort[] _outputData;
int _outputIndex;
// Incoming data and locking
private MemoryStream _incomingPacket;
private Queue<MemoryStream> _nextPackets;
private int _incomingPacketLength;
private System.Threading.ReaderWriterLockSlim _receiverLock;
private AltoSystem _system;
}
}