1 // 2 // Copyright (c) 2010-2025 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.Linq; 9 using System.Threading; 10 using System.Collections.Generic; 11 using Antmicro.Renode.Core; 12 using Antmicro.Renode.Exceptions; 13 using Antmicro.Renode.Logging; 14 using Antmicro.Renode.Peripherals; 15 using Antmicro.Renode.Peripherals.Bus; 16 using Antmicro.Renode.Peripherals.CPU; 17 using Antmicro.Renode.Peripherals.CoSimulated; 18 using Antmicro.Renode.Peripherals.Timers; 19 using Antmicro.Renode.Plugins.CoSimulationPlugin.Connection.Protocols; 20 using Range = Antmicro.Renode.Core.Range; 21 22 namespace Antmicro.Renode.Plugins.CoSimulationPlugin.Connection 23 { 24 public static class CoSimulationConnectionExtensions { 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 )25 public static void ConnectToCoSimulation(this Emulation emulation, 26 string machineName, 27 string name = null, 28 long frequency = DefaultTimeunitFrequency, 29 string simulationFilePathLinux = null, 30 string simulationFilePathWindows = null, 31 string simulationFilePathMacOS = null, 32 string simulationContextLinux = null, 33 string simulationContextWindows = null, 34 string simulationContextMacOS = null, 35 ulong limitBuffer = DefaultLimitBuffer, 36 int timeout = DefaultTimeout, 37 string address = null 38 ) 39 { 40 EmulationManager.Instance.CurrentEmulation.TryGetMachine(machineName, out var machine); 41 if(machine == null) 42 { 43 throw new ConstructionException($"Machine {machineName} does not exist."); 44 } 45 46 var cosimConnection = new CoSimulationConnection(machine, name, frequency, 47 simulationFilePathLinux, simulationFilePathWindows, simulationFilePathMacOS, 48 simulationContextLinux, simulationContextWindows, simulationContextMacOS, 49 limitBuffer, timeout, address); 50 } 51 52 public const ulong DefaultLimitBuffer = 1000000; 53 public const long DefaultTimeunitFrequency = 1000000000; 54 public const int DefaultTimeout = 3000; 55 } 56 57 public partial class CoSimulationConnection : IHostMachineElement, IConnectable<ICoSimulationConnectible>, IDisposable { 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)58 public CoSimulationConnection(IMachine machine, 59 string name, 60 long frequency, 61 string simulationFilePathLinux, 62 string simulationFilePathWindows, 63 string simulationFilePathMacOS, 64 string simulationContextLinux, 65 string simulationContextWindows, 66 string simulationContextMacOS, 67 ulong limitBuffer, 68 int timeout, 69 string address) 70 { 71 this.machine = machine; 72 this.gpioEntries = new List<GPIOEntry>(); 73 74 RegisterInHostMachine(name); 75 cosimConnection = SetupConnection(address, timeout, frequency, limitBuffer); 76 77 cosimIdxToPeripheral = new Dictionary<int, ICoSimulationConnectible>(); 78 } 79 AttachTo(ICoSimulationConnectible peripheral)80 public void AttachTo(ICoSimulationConnectible peripheral) 81 { 82 if(cosimIdxToPeripheral.ContainsKey(peripheral.CosimToRenodeIndex)) 83 { 84 throw new RecoverableException($"Failed to add a peripheral to co-simulated connection: Duplicate CosimToRenode index {peripheral.CosimToRenodeIndex}. Make sure all connected peripherals have unique cosimToRenodeIndex and renodeToCosimIndex parameters in platform definition."); 85 } 86 cosimIdxToPeripheral.Add(peripheral.CosimToRenodeIndex, peripheral); 87 peripheral.OnConnectionAttached(this); 88 } 89 DetachFrom(ICoSimulationConnectible peripheral)90 public void DetachFrom(ICoSimulationConnectible peripheral) 91 { 92 peripheral.OnConnectionDetached(this); 93 cosimIdxToPeripheral.Remove(peripheral.CosimToRenodeIndex); 94 } 95 Dispose()96 public void Dispose() 97 { 98 disposeInitiated = true; 99 cosimConnection.Dispose(); 100 } 101 102 public string SimulationContextLinux 103 { 104 get => SimulationContext; 105 set 106 { 107 #if PLATFORM_LINUX 108 SimulationContext = value; 109 #endif 110 } 111 } 112 113 public string SimulationContextWindows 114 { 115 get => SimulationContext; 116 set 117 { 118 #if PLATFORM_WINDOWS 119 SimulationContext = value; 120 #endif 121 } 122 } 123 124 public string SimulationContextMacOS 125 { 126 get => SimulationContext; 127 set 128 { 129 #if PLATFORM_OSX 130 SimulationContext = value; 131 #endif 132 } 133 } 134 135 public string SimulationContext 136 { 137 get => cosimConnection.Context; 138 set 139 { 140 cosimConnection.Context = value; 141 } 142 } 143 144 public string SimulationFilePathLinux 145 { 146 get => simulationFilePath; 147 set 148 { 149 #if PLATFORM_LINUX 150 SimulationFilePath = value; 151 #endif 152 } 153 } 154 155 public string SimulationFilePathWindows 156 { 157 get => simulationFilePath; 158 set 159 { 160 #if PLATFORM_WINDOWS 161 SimulationFilePath = value; 162 #endif 163 } 164 } 165 166 public string SimulationFilePathMacOS 167 { 168 get => simulationFilePath; 169 set 170 { 171 #if PLATFORM_OSX 172 SimulationFilePath = value; 173 #endif 174 } 175 } 176 177 public string SimulationFilePath 178 { 179 get => simulationFilePath; 180 set 181 { 182 if(String.IsNullOrWhiteSpace(value)) 183 { 184 return; 185 } 186 if(!String.IsNullOrWhiteSpace(simulationFilePath)) 187 { 188 var message = $"Co-simulated peripheral already connected to \"{simulationFilePath}\", cannot change the file name!"; 189 this.Log(LogLevel.Error, message); 190 throw new RecoverableException(message); 191 } 192 193 if(!String.IsNullOrWhiteSpace(value)) 194 { 195 cosimConnection.SimulationFilePath = value; 196 simulationFilePath = value; 197 Connect(); 198 } 199 } 200 } 201 HandleMessage()202 public void HandleMessage() 203 { 204 cosimConnection.HandleMessage(); 205 } 206 Connect()207 public void Connect() 208 { 209 if(cosimConnection.IsConnected) 210 { 211 this.Log(LogLevel.Warning, "The co-simulated peripheral is already connected."); 212 return; 213 } 214 cosimConnection.Connect(); 215 } 216 Send(ICoSimulationConnectible connectible, ActionType actionId, ulong offset, ulong value)217 public void Send(ICoSimulationConnectible connectible, ActionType actionId, ulong offset, ulong value) 218 { 219 int renodeToCosimIndex = connectible != null ? connectible.RenodeToCosimIndex : ProtocolMessage.NoPeripheralIndex; 220 var message = new ProtocolMessage(actionId, offset, value, renodeToCosimIndex); 221 if(!cosimConnection.TrySendMessage(message)) 222 { 223 AbortAndLogError($"Failed to send message: {message}"); 224 } 225 } 226 Respond(ActionType actionId, ulong offset, ulong value, int peripheralIdx)227 public void Respond(ActionType actionId, ulong offset, ulong value, int peripheralIdx) 228 { 229 if(!cosimConnection.TryRespond(new ProtocolMessage(actionId, offset, value, peripheralIdx))) 230 { 231 AbortAndLogError("Respond error!"); 232 } 233 } 234 235 public bool IsConnected => cosimConnection.IsConnected; 236 Reset()237 public void Reset() 238 { 239 // We currently have no way to tell the simulation that a particular peripheral is resetting. 240 // Reset is treated as a global event. 241 if(timer != null) 242 { 243 timer.Reset(); 244 } 245 Send(null, ActionType.ResetPeripheral, 0, 0); 246 } 247 SendGPIO(int number, bool value)248 public void SendGPIO(int number, bool value) 249 { 250 Write(null, ActionType.Interrupt, number, value ? 1ul : 0ul); 251 } 252 253 public string ConnectionParameters => (cosimConnection as SocketConnection)?.ConnectionParameters ?? ""; 254 OnReceiveDelegate(ProtocolMessage message)255 public delegate bool OnReceiveDelegate(ProtocolMessage message); 256 public OnReceiveDelegate OnReceive { get; set; } 257 RegisterOnGPIOReceive(Action<int, bool> callback, Range translationRange)258 public void RegisterOnGPIOReceive(Action<int, bool> callback, Range translationRange) 259 { 260 foreach(GPIOEntry entry in gpioEntries) 261 { 262 if(entry.range.Intersects(translationRange)) 263 { 264 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}]"); 265 } 266 } 267 gpioEntries.Add(new GPIOEntry(translationRange, callback)); 268 } 269 UnregisterOnGPIOReceive(Range translationRange)270 public void UnregisterOnGPIOReceive(Range translationRange) 271 { 272 gpioEntries.RemoveAll(entry => entry.range.Equals(translationRange)); 273 } 274 Write(ICoSimulationConnectible connectible, ActionType type, long offset, ulong value)275 public void Write(ICoSimulationConnectible connectible, ActionType type, long offset, ulong value) 276 { 277 if(!IsConnected) 278 { 279 this.Log(LogLevel.Warning, "Cannot write to peripheral. Set SimulationFilePath or connect to a simulator first!"); 280 return; 281 } 282 Send(connectible, type, (ulong)offset, value); 283 ValidateResponse(Receive()); 284 } 285 Read(ICoSimulationConnectible connectible, ActionType type, long offset)286 public ulong Read(ICoSimulationConnectible connectible, ActionType type, long offset) 287 { 288 if(!IsConnected) 289 { 290 this.Log(LogLevel.Warning, "Cannot read from peripheral. Set SimulationFilePath or connect to a simulator first!"); 291 return 0; 292 } 293 Send(connectible, type, (ulong)offset, 0); 294 var result = Receive(); 295 ValidateResponse(result); 296 297 return result.Data; 298 } 299 AbortAndLogError(string message)300 private void AbortAndLogError(string message) 301 { 302 // It's safe to call AbortAndLogError from any thread. 303 // Calling it from many threads may cause throwing more than one exception. 304 if(disposeInitiated) 305 { 306 return; 307 } 308 this.Log(LogLevel.Error, message); 309 cosimConnection.Abort(); 310 311 // Due to deadlock, we need to abort CPU instead of pausing emulation. 312 throw new CpuAbortException(); 313 } 314 SetupConnection(string address, int timeout, long frequency, ulong limitBuffer)315 private ICoSimulationConnection SetupConnection(string address, int timeout, long frequency, ulong limitBuffer) 316 { 317 ICoSimulationConnection cosimConnection = null; 318 if(address != null) 319 { 320 cosimConnection = new SocketConnection(this, timeout, HandleReceivedMessage, address); 321 } 322 else 323 { 324 cosimConnection = new LibraryConnection(this, timeout, HandleReceivedMessage); 325 } 326 327 // Setup time synchronization 328 // Frequency 0 means we never sync the time 329 if(frequency != 0) 330 { 331 allTicksProcessedARE = new AutoResetEvent(initialState: false); 332 timer = new LimitTimer(machine.ClockSource, frequency, null, LimitTimerName, limitBuffer, enabled: true, eventEnabled: true, autoUpdate: true); 333 timer.LimitReached += () => 334 { 335 if(!cosimConnection.TrySendMessage(new ProtocolMessage(ActionType.TickClock, 0, limitBuffer, ProtocolMessage.NoPeripheralIndex))) 336 { 337 AbortAndLogError("Failed to send or didn't receive TickClock action response."); 338 } 339 this.NoisyLog("Tick: TickClock sent, waiting for the verilated peripheral..."); 340 if(!allTicksProcessedARE.WaitOne(timeout)) 341 { 342 AbortAndLogError("Timeout reached while waiting for a tick response."); 343 } 344 this.NoisyLog("Tick: Co-simulation peripheral finished evaluating the model."); 345 }; 346 } 347 348 return cosimConnection; 349 } 350 ValidateResponse(ProtocolMessage message)351 private void ValidateResponse(ProtocolMessage message) 352 { 353 if(message.ActionId == ActionType.Error) 354 { 355 this.Log(LogLevel.Warning, "Operation error reported by the co-simulation!"); 356 } 357 } 358 Receive()359 private ProtocolMessage Receive() 360 { 361 if(!cosimConnection.TryReceiveMessage(out var message)) 362 { 363 AbortAndLogError("Receive error!"); 364 } 365 366 return message; 367 } 368 HandleReceivedMessage(ProtocolMessage message)369 private void HandleReceivedMessage(ProtocolMessage message) 370 { 371 ICoSimulationConnectible peripheral = null; 372 if(message.PeripheralIndex != ProtocolMessage.NoPeripheralIndex && !cosimIdxToPeripheral.TryGetValue(message.PeripheralIndex, out peripheral)) 373 { 374 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); 375 return; 376 } 377 378 if(OnReceive != null) 379 { 380 foreach(OnReceiveDelegate or in OnReceive.GetInvocationList()) 381 { 382 if(or(message)) 383 { 384 return; 385 } 386 } 387 } 388 389 IBusController systemBus = machine.SystemBus; 390 var busPeripheral = peripheral as IBusPeripheral; 391 if(busPeripheral != null) 392 { 393 systemBus = machine.GetSystemBus(busPeripheral); 394 } 395 396 switch(message.ActionId) 397 { 398 case ActionType.InvalidAction: 399 this.Log(LogLevel.Warning, "Invalid action received"); 400 break; 401 case ActionType.Interrupt: 402 HandleGPIO(message); 403 break; 404 case ActionType.PushByte: 405 this.Log(LogLevel.Noisy, "Writing byte: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address); 406 systemBus.WriteByte(message.Address, (byte)message.Data); 407 Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex); 408 break; 409 case ActionType.PushWord: 410 this.Log(LogLevel.Noisy, "Writing word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address); 411 systemBus.WriteWord(message.Address, (ushort)message.Data); 412 Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex); 413 break; 414 case ActionType.PushDoubleWord: 415 this.Log(LogLevel.Noisy, "Writing double word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address); 416 systemBus.WriteDoubleWord(message.Address, (uint)message.Data); 417 Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex); 418 break; 419 case ActionType.PushQuadWord: 420 this.Log(LogLevel.Noisy, "Writing quad word: 0x{0:X} to address: 0x{1:X}", message.Data, message.Address); 421 systemBus.WriteQuadWord(message.Address, message.Data); 422 Respond(ActionType.PushConfirmation, 0, 0, message.PeripheralIndex); 423 break; 424 case ActionType.GetByte: 425 this.Log(LogLevel.Noisy, "Requested byte from address: 0x{0:X}", message.Address); 426 Respond(ActionType.WriteToBus, 0, systemBus.ReadByte(message.Address), message.PeripheralIndex); 427 break; 428 case ActionType.GetWord: 429 this.Log(LogLevel.Noisy, "Requested word from address: 0x{0:X}", message.Address); 430 Respond(ActionType.WriteToBus, 0, systemBus.ReadWord(message.Address), message.PeripheralIndex); 431 break; 432 case ActionType.GetDoubleWord: 433 this.Log(LogLevel.Noisy, "Requested double word from address: 0x{0:X}", message.Address); 434 Respond(ActionType.WriteToBus, 0, systemBus.ReadDoubleWord(message.Address), message.PeripheralIndex); 435 break; 436 case ActionType.GetQuadWord: 437 this.Log(LogLevel.Noisy, "Requested quad word from address: 0x{0:X}", message.Address); 438 Respond(ActionType.WriteToBus, 0, systemBus.ReadQuadWord(message.Address), message.PeripheralIndex); 439 break; 440 case ActionType.TickClock: 441 allTicksProcessedARE.Set(); 442 break; 443 case ActionType.Error: 444 AbortAndLogError("Fatal error message received from a co-simulation"); 445 break; 446 default: 447 this.Log(LogLevel.Warning, "Unhandled message: ActionId = {0}; Address: 0x{1:X}; Data: 0x{2:X}!", 448 message.ActionId, message.Address, message.Data); 449 break; 450 } 451 } 452 HandleGPIO(ProtocolMessage message)453 private void HandleGPIO(ProtocolMessage message) 454 { 455 var gpioNumber = message.Address; 456 bool newValue = message.Data != 0; 457 foreach(GPIOEntry entry in gpioEntries) 458 { 459 if(entry.range.Contains(gpioNumber)) 460 { 461 // NOTE: Callback is responsible for translating into local interrupt offsets using 462 // the range it registered with - local GPIO number is (gpioNumber - range.StartAddress). 463 entry.callback((int)gpioNumber, newValue); 464 return; 465 } 466 } 467 } 468 RegisterInHostMachine(string name)469 private void RegisterInHostMachine(string name) 470 { 471 if(name == null) 472 { 473 name = "cosimulation_connection"; 474 } 475 476 var hostMachineElementNames = EmulationManager.Instance.CurrentEmulation.HostMachine.GetNames(); 477 478 // Assure the name is unique inside of the HostMachine by appending a number at the end. 479 if(hostMachineElementNames.Contains(name)) 480 { 481 var uniqueNo = 0; 482 while(hostMachineElementNames.Contains($"{name}{uniqueNo}")) 483 { 484 uniqueNo += 1; 485 } 486 name = $"{name}{uniqueNo}"; 487 } 488 EmulationManager.Instance.CurrentEmulation.HostMachine.AddHostMachineElement(this, name); 489 } 490 491 private struct GPIOEntry 492 { GPIOEntryAntmicro.Renode.Plugins.CoSimulationPlugin.Connection.CoSimulationConnection.GPIOEntry493 public GPIOEntry(Range range, Action<int, bool> callback) 494 { 495 this.range = range; 496 this.callback = callback; 497 } 498 499 public readonly Range range; 500 public readonly Action<int, bool> callback; 501 }; 502 503 504 private readonly ICoSimulationConnection cosimConnection; 505 506 private const int DefaultTimeout = 3000; 507 508 private readonly Dictionary<int, ICoSimulationConnectible> cosimIdxToPeripheral; 509 private string simulationFilePath; 510 private IMachine machine; 511 private volatile bool disposeInitiated; 512 private const string LimitTimerName = "CoSimulationClock"; 513 private LimitTimer timer; 514 private AutoResetEvent allTicksProcessedARE; 515 private List<GPIOEntry> gpioEntries; 516 } 517 } 518