blob: 4368951ab11533222b66f768d6a3918964ff0782 [file]
//
// Copyright (c) 2010-2024 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in 'licenses/MIT.txt'.
//
using System;
using System.Linq;
using System.Threading;
using System.Collections.Generic;
using Antmicro.Renode.Core;
using Antmicro.Renode.Exceptions;
using Antmicro.Renode.Logging;
using Antmicro.Renode.Peripherals;
using Antmicro.Renode.Peripherals.Bus;
using Antmicro.Renode.Peripherals.CPU;
using Antmicro.Renode.Peripherals.CoSimulated;
using Antmicro.Renode.Peripherals.Timers;
using Antmicro.Renode.Plugins.CoSimulationPlugin.Connection.Protocols;
using Range = Antmicro.Renode.Core.Range;
namespace Antmicro.Renode.Plugins.CoSimulationPlugin.Connection
{
public static class CoSimulationConnectionExtensions {
public static void ConnectToCoSimulation(this Emulation emulation,
string machineName,
string name = null,
long frequency = DefaultTimeunitFrequency,
string simulationFilePathLinux = null,
string simulationFilePathWindows = null,
string simulationFilePathMacOS = null,
string simulationContextLinux = null,
string simulationContextWindows = null,
string simulationContextMacOS = null,
ulong limitBuffer = DefaultLimitBuffer,
int timeout = DefaultTimeout,
string address = null
)
{
EmulationManager.Instance.CurrentEmulation.TryGetMachine(machineName, out var machine);
if(machine == null)
{
throw new ConstructionException($"Machine {machineName} does not exist.");
}
var cosimConnection = new CoSimulationConnection(machine, name, frequency,
simulationFilePathLinux, simulationFilePathWindows, simulationFilePathMacOS,
simulationContextLinux, simulationContextWindows, simulationContextMacOS,
limitBuffer, timeout, address);
}
public const ulong DefaultLimitBuffer = 1000000;
public const long DefaultTimeunitFrequency = 1000000000;
public const int DefaultTimeout = 3000;
}
public partial class CoSimulationConnection : IHostMachineElement, IConnectable<ICoSimulationConnectible>, IDisposable {
public CoSimulationConnection(IMachine machine,
string name,
long frequency,
string simulationFilePathLinux,
string simulationFilePathWindows,
string simulationFilePathMacOS,
string simulationContextLinux,
string simulationContextWindows,
string simulationContextMacOS,
ulong limitBuffer,
int timeout,
string address)
{
this.machine = machine;
this.gpioEntries = new List<GPIOEntry>();
RegisterInHostMachine(name);
cosimConnection = SetupConnection(address, timeout, frequency, limitBuffer);
cosimIdxToPeripheral = new Dictionary<int, ICoSimulationConnectible>();
}
public void AttachTo(ICoSimulationConnectible peripheral)
{
if(cosimIdxToPeripheral.ContainsKey(peripheral.CosimToRenodeIndex))
{
throw new RecoverableException("Failed to add a peripheral to co-simulated connection. Make sure all connected peripherals have correctly assigned, unique cosimulation subordinate and manager indices in platform definition.");
}
cosimIdxToPeripheral.Add(peripheral.CosimToRenodeIndex, peripheral);
peripheral.OnConnectionAttached(this);
}
public void DetachFrom(ICoSimulationConnectible peripheral)
{
peripheral.OnConnectionDetached(this);
cosimIdxToPeripheral.Remove(peripheral.CosimToRenodeIndex);
}
public void Dispose()
{
disposeInitiated = true;
cosimConnection.Dispose();
}
public string SimulationContextLinux
{
get => SimulationContext;
set
{
#if PLATFORM_LINUX
SimulationContext = value;
#endif
}
}
public string SimulationContextWindows
{
get => SimulationContext;
set
{
#if PLATFORM_WINDOWS
SimulationContext = value;
#endif
}
}
public string SimulationContextMacOS
{
get => SimulationContext;
set
{
#if PLATFORM_OSX
SimulationContext = value;
#endif
}
}
public string SimulationContext
{
get => cosimConnection.Context;
set
{
cosimConnection.Context = value;
}
}
public string SimulationFilePathLinux
{
get => simulationFilePath;
set
{
#if PLATFORM_LINUX
SimulationFilePath = value;
#endif
}
}
public string SimulationFilePathWindows
{
get => simulationFilePath;
set
{
#if PLATFORM_WINDOWS
SimulationFilePath = value;
#endif
}
}
public string SimulationFilePathMacOS
{
get => simulationFilePath;
set
{
#if PLATFORM_OSX
SimulationFilePath = value;
#endif
}
}
public string SimulationFilePath
{
get => simulationFilePath;
set
{
if(String.IsNullOrWhiteSpace(value))
{
return;
}
if(!String.IsNullOrWhiteSpace(simulationFilePath))
{
var message = $"Co-simulated peripheral already connected to \"{simulationFilePath}\", cannot change the file name!";
this.Log(LogLevel.Error, message);
throw new RecoverableException(message);
}
if(!String.IsNullOrWhiteSpace(value))
{
cosimConnection.SimulationFilePath = value;
simulationFilePath = value;
Connect();
}
}
}
public void HandleMessage()
{
cosimConnection.HandleMessage();
}
public void Connect()
{
if(cosimConnection.IsConnected)
{
this.Log(LogLevel.Warning, "The co-simulated peripheral is already connected.");
return;
}
cosimConnection.Connect();
}
public void Send(ICoSimulationConnectible connectible, ActionType actionId, ulong offset, ulong value)
{
int renodeToCosimIndex = connectible != null ? connectible.RenodeToCosimIndex : ProtocolMessage.NoPeripheralIndex;
if(!cosimConnection.TrySendMessage(new ProtocolMessage(actionId, offset, value, renodeToCosimIndex)))
{
AbortAndLogError("Send error!");
}
}
public void Respond(ActionType actionId, ulong offset, ulong value, int peripheralIdx)
{
if(!cosimConnection.TryRespond(new ProtocolMessage(actionId, offset, value, peripheralIdx)))
{
AbortAndLogError("Respond error!");
}
}
public bool IsConnected => cosimConnection.IsConnected;
public void Reset()
{
// We currently have no way to tell the simulation that a particular peripheral is resetting.
// Reset is treated as a global event.
if(timer != null)
{
timer.Reset();
}
Send(null, ActionType.ResetPeripheral, 0, 0);
}
public void SendGPIO(int number, bool value)
{
Write(null, ActionType.Interrupt, number, value ? 1ul : 0ul);
}
public string ConnectionParameters => (cosimConnection as SocketConnection)?.ConnectionParameters ?? "";
public delegate bool OnReceiveDelegate(ProtocolMessage message);
public OnReceiveDelegate OnReceive { get; set; }
public void RegisterOnGPIOReceive(Action<int, bool> callback, Range translationRange)
{
foreach(GPIOEntry entry in gpioEntries)
{
if(entry.range.Intersects(translationRange))
{
throw new ConfigurationException($"Cannot register cosimulation GPIO receive callback on range [{translationRange.StartAddress}, {translationRange.EndAddress}] - there already is a callback registered on intersecting range [{entry.range.StartAddress}, {entry.range.EndAddress}]");
}
}
gpioEntries.Add(new GPIOEntry(translationRange, callback));
}
public void UnregisterOnGPIOReceive(Range translationRange)
{
gpioEntries.RemoveAll(entry => entry.range.Equals(translationRange));
}
public void Write(ICoSimulationConnectible connectible, ActionType type, long offset, ulong value)
{
if(!IsConnected)
{
this.Log(LogLevel.Warning, "Cannot write to peripheral. Set SimulationFilePath or connect to a simulator first!");
return;
}
Send(connectible, type, (ulong)offset, value);
ValidateResponse(Receive());
}
public ulong Read(ICoSimulationConnectible connectible, ActionType type, long offset)
{
if(!IsConnected)
{
this.Log(LogLevel.Warning, "Cannot read from peripheral. Set SimulationFilePath or connect to a simulator first!");
return 0;
}
Send(connectible, type, (ulong)offset, 0);
var result = Receive();
ValidateResponse(result);
return result.Data;
}
private void AbortAndLogError(string message)
{
// It's safe to call AbortAndLogError from any thread.
// Calling it from many threads may cause throwing more than one exception.
if(disposeInitiated)
{
return;
}
this.Log(LogLevel.Error, message);
cosimConnection.Abort();
// Due to deadlock, we need to abort CPU instead of pausing emulation.
throw new CpuAbortException();
}
private ICoSimulationConnection SetupConnection(string address, int timeout, long frequency, ulong limitBuffer)
{
ICoSimulationConnection cosimConnection = null;
if(address != null)
{
cosimConnection = new SocketConnection(this, timeout, HandleReceivedMessage, address);
}
else
{
cosimConnection = new LibraryConnection(this, timeout, HandleReceivedMessage);
}
// Setup time synchronization
// Frequency 0 means we never sync the time
if(frequency != 0)
{
allTicksProcessedARE = new AutoResetEvent(initialState: false);
timer = new LimitTimer(machine.ClockSource, frequency, null, LimitTimerName, limitBuffer, enabled: true, eventEnabled: true, autoUpdate: true);
timer.LimitReached += () =>
{
if(!cosimConnection.TrySendMessage(new ProtocolMessage(ActionType.TickClock, 0, limitBuffer, ProtocolMessage.NoPeripheralIndex)))
{
AbortAndLogError("Send error!");
}
this.NoisyLog("Tick: TickClock sent, waiting for the verilated peripheral...");
if(!allTicksProcessedARE.WaitOne(timeout))
{
AbortAndLogError("Timeout reached while waiting for a tick response.");
}
this.NoisyLog("Tick: Co-simulation peripheral finished evaluating the model.");
};
}
return cosimConnection;
}
private void ValidateResponse(ProtocolMessage message)
{
if(message.ActionId == ActionType.Error)
{
this.Log(LogLevel.Warning, "Operation error reported by the co-simulation!");
}
}
private ProtocolMessage Receive()
{
if(!cosimConnection.TryReceiveMessage(out var message))
{
AbortAndLogError("Receive error!");
}
return message;
}
private void HandleReceivedMessage(ProtocolMessage message)
{
ICoSimulationConnectible peripheral = null;
if(message.PeripheralIndex != ProtocolMessage.NoPeripheralIndex && !cosimIdxToPeripheral.TryGetValue(message.PeripheralIndex, out peripheral))
{
this.Log(LogLevel.Error, "Received co-simulation message {} to a peripheral with index {}, not registered in Renode. Make sure \"cosimToRenodeIndex\" property is provided in platform definition. Message will be ignored.", message.ActionId, message.PeripheralIndex);
return;
}
if(OnReceive != null)
{
foreach(OnReceiveDelegate or in OnReceive.GetInvocationList())
{
if(or(message))
{
return;
}
}
}
IBusController systemBus = machine.SystemBus;
var busPeripheral = peripheral as IBusPeripheral;
if(busPeripheral != null)
{
systemBus = machine.GetSystemBus(busPeripheral);
}
switch(message.ActionId)
{
case ActionType.InvalidAction:
this.Log(LogLevel.Warning, "Invalid action received");
break;
case ActionType.Interrupt:
HandleGPIO(message);
break;
case ActionType.PushByte:
this.Log(LogLevel.Noisy, "Writing byte: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address);
systemBus.WriteByte(message.Address, (byte)message.Data);
Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex);
break;
case ActionType.PushWord:
this.Log(LogLevel.Noisy, "Writing word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address);
systemBus.WriteWord(message.Address, (ushort)message.Data);
Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex);
break;
case ActionType.PushDoubleWord:
this.Log(LogLevel.Noisy, "Writing double word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address);
systemBus.WriteDoubleWord(message.Address, (uint)message.Data);
Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex);
break;
case ActionType.PushQuadWord:
this.Log(LogLevel.Noisy, "Writing quad word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address);
systemBus.WriteQuadWord(message.Address, message.Data);
Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex);
break;
case ActionType.GetByte:
this.Log(LogLevel.Noisy, "Requested byte from address: 0x{0:X}", message.Address);
Respond(ActionType.WriteToBus, 0, systemBus.ReadByte(message.Address), message.PeripheralIndex);
break;
case ActionType.GetWord:
this.Log(LogLevel.Noisy, "Requested word from address: 0x{0:X}", message.Address);
Respond(ActionType.WriteToBus, 0, systemBus.ReadWord(message.Address), message.PeripheralIndex);
break;
case ActionType.GetDoubleWord:
this.Log(LogLevel.Noisy, "Requested double word from address: 0x{0:X}", message.Address);
Respond(ActionType.WriteToBus, 0, systemBus.ReadDoubleWord(message.Address), message.PeripheralIndex);
break;
case ActionType.GetQuadWord:
this.Log(LogLevel.Noisy, "Requested quad word from address: 0x{0:X}", message.Address);
Respond(ActionType.WriteToBus, 0, systemBus.ReadQuadWord(message.Address), message.PeripheralIndex);
break;
case ActionType.TickClock:
allTicksProcessedARE.Set();
break;
default:
this.Log(LogLevel.Warning, "Unhandled message: ActionId = {0}; Address: 0x{1:X}; Data: 0x{2:X}!",
message.ActionId, message.Address, message.Data);
break;
}
}
private void HandleGPIO(ProtocolMessage message)
{
var gpioNumber = message.Address;
bool newValue = message.Data != 0;
foreach(GPIOEntry entry in gpioEntries)
{
if(entry.range.Contains(gpioNumber))
{
// NOTE: Callback is responsible for translating into local interrupt offsets using
// the range it registered with - local GPIO number is (gpioNumber - range.StartAddress).
entry.callback((int)gpioNumber, newValue);
return;
}
}
}
private void RegisterInHostMachine(string name)
{
if(name == null)
{
name = "cosimulation_connection";
}
var hostMachineElementNames = EmulationManager.Instance.CurrentEmulation.HostMachine.GetNames();
// Assure the name is unique inside of the HostMachine by appending a number at the end.
if(hostMachineElementNames.Contains(name))
{
var uniqueNo = 0;
while(hostMachineElementNames.Contains($"{name}{uniqueNo}"))
{
uniqueNo += 1;
}
name = $"{name}{uniqueNo}";
}
EmulationManager.Instance.CurrentEmulation.HostMachine.AddHostMachineElement(this, name);
}
private struct GPIOEntry
{
public GPIOEntry(Range range, Action<int, bool> callback)
{
this.range = range;
this.callback = callback;
}
public readonly Range range;
public readonly Action<int, bool> callback;
};
private readonly ICoSimulationConnection cosimConnection;
private const int DefaultTimeout = 3000;
private readonly Dictionary<int, ICoSimulationConnectible> cosimIdxToPeripheral;
private string simulationFilePath;
private IMachine machine;
private volatile bool disposeInitiated;
private const string LimitTimerName = "CoSimulationClock";
private LimitTimer timer;
private AutoResetEvent allTicksProcessedARE;
private List<GPIOEntry> gpioEntries;
}
}