[#59809] Add SystemC integration
diff --git a/Renode.sln b/Renode.sln
index 90eff76..5a56cf8 100644
--- a/Renode.sln
+++ b/Renode.sln
@@ -59,6 +59,8 @@
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TracePlugin", "src\Infrastructure\src\Plugins\TracePlugin\TracePlugin.csproj", "{887C6088-F483-466A-A671-06EEC42B8DC1}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemCPlugin", "src\Plugins\SystemCPlugin\SystemCPlugin.csproj", "{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WiresharkPlugin", "src\Plugins\WiresharkPlugin\WiresharkPlugin.csproj", "{66A9995A-13AE-4454-88A6-29EB2D6F5988}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VerilatorPlugin", "src\Plugins\VerilatorPlugin\VerilatorPlugin.csproj", "{45A27C6A-1831-4E0D-84C2-FB3893E7FBE5}"
@@ -413,6 +415,18 @@
 		{66A9995A-13AE-4454-88A6-29EB2D6F5988}.ReleaseWindows|Any CPU.Build.0 = Release|Any CPU
 		{66A9995A-13AE-4454-88A6-29EB2D6F5988}.DebugWindows|Any CPU.ActiveCfg = Debug|Any CPU
 		{66A9995A-13AE-4454-88A6-29EB2D6F5988}.DebugWindows|Any CPU.Build.0 = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugHeadless|Any CPU.ActiveCfg = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugHeadless|Any CPU.Build.0 = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseHeadless|Any CPU.ActiveCfg = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseHeadless|Any CPU.Build.0 = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugMono|Any CPU.ActiveCfg = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugMono|Any CPU.Build.0 = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseMono|Any CPU.ActiveCfg = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseMono|Any CPU.Build.0 = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseWindows|Any CPU.ActiveCfg = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.ReleaseWindows|Any CPU.Build.0 = Release|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugWindows|Any CPU.ActiveCfg = Debug|Any CPU
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}.DebugWindows|Any CPU.Build.0 = Debug|Any CPU
 		{45A27C6A-1831-4E0D-84C2-FB3893E7FBE5}.DebugHeadless|Any CPU.ActiveCfg = Debug|Any CPU
 		{45A27C6A-1831-4E0D-84C2-FB3893E7FBE5}.DebugHeadless|Any CPU.Build.0 = Debug|Any CPU
 		{45A27C6A-1831-4E0D-84C2-FB3893E7FBE5}.ReleaseHeadless|Any CPU.ActiveCfg = Release|Any CPU
@@ -575,6 +589,7 @@
 		{25FAECC1-55F0-4608-88BD-4207A7F993B0} = {CA585D43-7DF3-4486-B9B5-8DAA812716C3}
 		{C93D746E-1586-4D4F-B411-BF5A966E6A08} = {CA585D43-7DF3-4486-B9B5-8DAA812716C3}
 		{BB222124-707C-5EFD-8289-2728351FB7AA} = {4671F602-D5E2-4F4E-B942-3357C33C92E2}
+		{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}
 		{45A27C6A-1831-4E0D-84C2-FB3893E7FBE5} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}
 		{3A70B9B8-BBAB-47EA-8473-B7A0B4961D56} = {CA585D43-7DF3-4486-B9B5-8DAA812716C3}
 		{69B08E08-10DC-473C-BCB7-3B0C398F63CF} = {CD9D4CF9-BD98-4D57-A5EF-EA5C3C75CB0C}
diff --git a/Renode_NET.sln b/Renode_NET.sln
index e52b305..a61ad69 100644
--- a/Renode_NET.sln
+++ b/Renode_NET.sln
@@ -69,6 +69,8 @@
 EndProject

 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WiresharkPlugin_NET", "src\Plugins\WiresharkPlugin\WiresharkPlugin_NET.csproj", "{1FBC3B41-D61C-4528-8BA5-20EBA17C9031}"

 EndProject

+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemCPlugin_NET", "src\Plugins\SystemCPlugin\SystemCPlugin_NET.csproj", "{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}"

+EndProject

 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleCommandPlugin_NET", "src\Infrastructure\src\Plugins\SampleCommandPlugin\SampleCommandPlugin_NET.csproj", "{78DBBFB9-7193-4ABC-BE9F-DA839693C23A}"

 EndProject

 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TracePlugin_NET", "src\Infrastructure\src\Plugins\TracePlugin\TracePlugin_NET.csproj", "{2DB2469A-9F2E-4BA8-A4A0-333E4A319AF0}"

@@ -463,6 +465,22 @@
 		{3B25FD1F-CEBA-4450-8893-DC330FDB56A7}.ReleaseMono|Any CPU.Build.0 = Release|Any CPU

 		{3B25FD1F-CEBA-4450-8893-DC330FDB56A7}.ReleaseWindows|Any CPU.ActiveCfg = Release|Any CPU

 		{3B25FD1F-CEBA-4450-8893-DC330FDB56A7}.ReleaseWindows|Any CPU.Build.0 = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugHeadless|Any CPU.ActiveCfg = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugHeadless|Any CPU.Build.0 = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugMono|Any CPU.ActiveCfg = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugMono|Any CPU.Build.0 = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugWindows|Any CPU.ActiveCfg = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.DebugWindows|Any CPU.Build.0 = Debug|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.Release|Any CPU.Build.0 = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseHeadless|Any CPU.ActiveCfg = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseHeadless|Any CPU.Build.0 = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseMono|Any CPU.ActiveCfg = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseMono|Any CPU.Build.0 = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseWindows|Any CPU.ActiveCfg = Release|Any CPU

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2}.ReleaseWindows|Any CPU.Build.0 = Release|Any CPU

 		{A244A988-6ADC-4B90-912B-D66C680FA962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

 		{A244A988-6ADC-4B90-912B-D66C680FA962}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{A244A988-6ADC-4B90-912B-D66C680FA962}.DebugHeadless|Any CPU.ActiveCfg = Debug|Any CPU

@@ -724,6 +742,7 @@
 		{1FBC3B41-D61C-4528-8BA5-20EBA17C9031} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}

 		{78DBBFB9-7193-4ABC-BE9F-DA839693C23A} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}

 		{2DB2469A-9F2E-4BA8-A4A0-333E4A319AF0} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}

+		{D7CEC81D-8BF1-4B3E-8B6A-5D7B3CA0C2F2} = {8BD709BF-8C11-4CBA-9A7D-CC2CC639BD4F}

                 # {...} = Tests

 		{101B7A46-D5A6-41DE-ADC3-36939FF62942} = {CD9D4CF9-BD98-4D57-A5EF-EA5C3C75CB0C}

 		{16DBE3C1-83C5-4472-85AC-CF654E4559AD} = {CD9D4CF9-BD98-4D57-A5EF-EA5C3C75CB0C}

diff --git a/src/Plugins/SystemCPlugin/Peripheral/SystemCPeripheral.cs b/src/Plugins/SystemCPlugin/Peripheral/SystemCPeripheral.cs
new file mode 100644
index 0000000..cff412a
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/Peripheral/SystemCPeripheral.cs
@@ -0,0 +1,499 @@
+//
+// Copyright (c) 2010-2024 Antmicro
+//
+// This file is licensed under the MIT License.
+// Full license text is available in 'licenses/MIT.txt'.
+//
+using Antmicro.Renode.Core;
+using Antmicro.Renode.Exceptions;
+using Antmicro.Renode.Logging;
+using Antmicro.Renode.Peripherals.Bus;
+using Antmicro.Renode.Peripherals.CPU;
+using Antmicro.Renode.Peripherals.Timers;
+using Antmicro.Renode.Time;
+using Antmicro.Renode.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace Antmicro.Renode.Peripherals.SystemC
+{
+    public enum RenodeAction : byte
+    {
+        Init = 0,
+        Read = 1,
+        Write = 2,
+        Timesync = 3,
+        GPIOWrite = 4,
+        Reset = 5,
+    }
+
+    [StructLayout(LayoutKind.Sequential, Pack = 1)]
+    public struct RenodeMessage
+    {
+        public RenodeMessage(RenodeAction actionId, byte dataLength, byte connectionIndex, ulong address, ulong payload)
+        {
+            ActionId = actionId;
+            DataLength = dataLength;
+            ConnectionIndex = connectionIndex;
+            Address = address;
+            Payload = payload;
+        }
+
+        public byte[] Serialize()
+        {
+            var size = Marshal.SizeOf(this);
+            var result = new byte[size];
+            var handler = default(GCHandle);
+
+            try
+            {
+                handler = GCHandle.Alloc(result, GCHandleType.Pinned);
+                Marshal.StructureToPtr(this, handler.AddrOfPinnedObject(), false);
+            }
+            finally
+            {
+                if(handler.IsAllocated)
+                {
+                    handler.Free();
+                }
+            }
+
+            return result;
+        }
+
+        public void Deserialize(byte[] message)
+        {
+            var handler = default(GCHandle);
+            try
+            {
+                handler = GCHandle.Alloc(message, GCHandleType.Pinned);
+                this = (RenodeMessage)Marshal.PtrToStructure(handler.AddrOfPinnedObject(), typeof(RenodeMessage));
+            }
+            finally
+            {
+                if(handler.IsAllocated)
+                {
+                    handler.Free();
+                }
+            }
+        }
+
+        public override string ToString()
+        {
+            return $"RenodeMessage [{ActionId}@{ConnectionIndex}:{Address}] {Payload}";
+        }
+
+        public bool IsSystemBusConnection() => ConnectionIndex == MainSystemBusConnectionIndex;
+        public bool IsDirectConnection() => !IsSystemBusConnection();
+
+        public byte GetDirectConnectionIndex()
+        {
+            if(!IsDirectConnection())
+            {
+                Logger.Log(LogLevel.Error, "Message for main system bus connection does not have a direct connection index.");
+                return 0xff;
+            }
+            return (byte)(ConnectionIndex - 1);
+        }
+
+        private const byte MainSystemBusConnectionIndex = 0;
+
+        public readonly RenodeAction ActionId;
+        public readonly byte DataLength;
+        public readonly byte ConnectionIndex;
+        public readonly ulong Address;
+        public readonly ulong Payload;
+    }
+
+    public interface IDirectAccessPeripheral : IPeripheral
+    {
+        ulong ReadDirect(byte dataLength, long offset, byte connectionIndex);
+        void WriteDirect(byte dataLength, long offset, ulong value, byte connectionIndex);
+    };
+
+    public class SystemCPeripheral : IQuadWordPeripheral, IDoubleWordPeripheral, IWordPeripheral, IBytePeripheral, INumberedGPIOOutput, IGPIOReceiver, IDirectAccessPeripheral, IDisposable
+    {
+        public SystemCPeripheral(
+                IMachine machine,
+                string address,
+                int port,
+                int timeSyncPeriodUS = 1000
+        )
+        {
+            this.address = address;
+            this.port = port;
+            this.machine = machine;
+            this.timeSyncPeriodUS = timeSyncPeriodUS;
+            sysbus = machine.GetSystemBus(this);
+
+            directAccessPeripherals = new Dictionary<int, IDirectAccessPeripheral>();
+
+            messageLock = new object();
+
+            backwardThread = new Thread(BackwardConnectionLoop)
+            {
+                IsBackground = true,
+                Name = "SystemC.BackwardThread"
+            };
+
+            var innerConnections = new Dictionary<int, IGPIO>();
+            for(int i = 0; i < NumberOfGPIOPins; i++)
+            {
+                innerConnections[i] = new GPIO();
+            }
+            Connections = new ReadOnlyDictionary<int, IGPIO>(innerConnections);
+        }
+
+        public void AddDirectConnection(byte connectionIndex, IDirectAccessPeripheral target)
+        {
+            if(directAccessPeripherals.ContainsKey(connectionIndex))
+            {
+                this.Log(LogLevel.Error, "Failed to add Direct Connection #{0} - connection with this index is already present", connectionIndex);
+                return;
+            }
+
+            directAccessPeripherals.Add(connectionIndex, target);
+        }
+
+        public string SystemCExecutablePath
+        {
+            get => systemcExecutablePath;
+            set
+            {
+                try
+                {
+                    systemcExecutablePath = value;
+                    var connectionParams = $"{address} {port}";
+                    StartSystemCProcess(systemcExecutablePath, connectionParams);
+                    SetupConnection();
+                    SetupTimesync();
+                }
+                catch(Exception e)
+                {
+                    throw new RecoverableException($"Failed to start SystemC process: {e.Message}");
+                }
+            }
+        }
+
+        public ulong ReadQuadWord(long offset)
+        {
+            return Read(8, offset);
+        }
+
+        public void WriteQuadWord(long offset, ulong value)
+        {
+            Write(8, offset, value);
+        }
+
+        public uint ReadDoubleWord(long offset)
+        {
+            return (uint)Read(4, offset);
+        }
+
+        public void WriteDoubleWord(long offset, uint value)
+        {
+            Write(4, offset, value);
+        }
+
+        public ushort ReadWord(long offset)
+        {
+            return (ushort)Read(2, offset);
+        }
+
+        public void WriteWord(long offset, ushort value)
+        {
+            Write(2, offset, value);
+        }
+
+        public byte ReadByte(long offset)
+        {
+            return (byte)Read(1, offset);
+        }
+
+        public void WriteByte(long offset, byte value)
+        {
+            Write(1, offset, value);
+        }
+
+        public ulong ReadDirect(byte dataLength, long offset, byte connectionIndex)
+        {
+            return Read(dataLength, offset, connectionIndex);
+        }
+
+        public void WriteDirect(byte dataLength, long offset, ulong value, byte connectionIndex)
+        {
+            Write(dataLength, offset, value, connectionIndex);
+        }
+
+        public void OnGPIO(int number, bool value)
+        {
+            BitHelper.SetBit(ref outGPIOState, (byte)number, value);
+            var request = new RenodeMessage(RenodeAction.GPIOWrite, 0, 0, 0, outGPIOState);
+            SendRequest(request);
+        }
+
+        public void Reset()
+        {
+            outGPIOState = 0;
+            var request = new RenodeMessage(RenodeAction.Reset, 0, 0, 0, 0);
+            SendRequest(request);
+        }
+
+        public void Dispose()
+        {
+            if(systemcProcess != null && !systemcProcess.HasExited)
+            {
+                // Init message sent after connection has been established signifies Renode terminated and SystemC process
+                // should exit.
+                var request = new RenodeMessage(RenodeAction.Init, 0, 0, 0, 0);
+                SendRequest(request);
+
+                if(!systemcProcess.WaitForExit(500)) {
+                    this.Log(LogLevel.Info, "SystemC process failed to exit gracefully - killing it.");
+                    systemcProcess.Kill();
+                }
+            }
+        }
+
+        public IReadOnlyDictionary<int, IGPIO> Connections { get; }
+
+        private ulong Read(byte dataLength, long offset, byte connectionIndex = 0)
+        {
+            var request = new RenodeMessage(RenodeAction.Read, dataLength, connectionIndex, (ulong)offset, 0);
+            var response = SendRequest(request);
+
+            TryToSkipTransactionTime(response.Address);
+
+            return response.Payload;
+        }
+
+        private void Write(byte dataLength, long offset, ulong value, byte connectionIndex = 0)
+        {
+            var request = new RenodeMessage(RenodeAction.Write, dataLength, connectionIndex, (ulong)offset, value);
+            var response = SendRequest(request);
+
+            TryToSkipTransactionTime(response.Address);
+        }
+
+        private ulong GetCurrentVirtualTimeUS()
+        {
+            return machine.LocalTimeSource.ElapsedVirtualTime.TotalMicroseconds;
+        }
+
+        private void StartSystemCProcess(string systemcExecutablePath, string connectionParams)
+        {
+            try
+            {
+                systemcProcess = new Process
+                {
+                    StartInfo = new ProcessStartInfo(systemcExecutablePath)
+                    {
+                        UseShellExecute = false,
+                        Arguments = connectionParams
+                    }
+                };
+
+                systemcProcess.Start();
+            }
+            catch(Exception e)
+            {
+                throw new RecoverableException(e.Message);
+            }
+        }
+
+        private void SetupConnection()
+        {
+            var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+            listener.Bind(new IPEndPoint(IPAddress.Parse(address), port));
+            listener.Listen(2);
+
+            this.Log(LogLevel.Info, "SystemCPeripheral waiting for forward SystemC connection on {0}:{1}", address, port);
+            forwardSocket = listener.Accept();
+            forwardSocket.SendTimeout = 1000;
+            forwardSocket.ReceiveTimeout = 1000;
+
+            backwardSocket = listener.Accept();
+            backwardSocket.SendTimeout = 1000;
+            // No ReceiveTimeout for backwardSocket - it runs on a dedicated thread and by design blocks on Receive until a message arrives from SystemC process.
+
+            SendRequest(new RenodeMessage(RenodeAction.Init, 0, 0, 0, (ulong)timeSyncPeriodUS));
+
+            backwardThread.Start();
+        }
+
+        private void SetupTimesync()
+        {
+            // NOTE: This function blocks simulation while awaiting for the response.
+
+            // Timer unit is microseconds
+            var timesyncFrequency = 1000000;
+            var timesyncLimit = (ulong)timeSyncPeriodUS;
+            var timerName = "RenodeSystemCTimesyncTimer";
+            var timer = new LimitTimer(machine.ClockSource, timesyncFrequency, this, timerName, limit: timesyncLimit, enabled: true, eventEnabled: true, autoUpdate: true);
+            timer.LimitReached += () =>
+            {
+                var request = new RenodeMessage(RenodeAction.Timesync, 0, 0, 0, GetCurrentVirtualTimeUS());
+                var response = SendRequest(request);
+            };
+        }
+
+        private RenodeMessage SendRequest(RenodeMessage request)
+        {
+            lock (messageLock)
+            {
+                var messageSize = Marshal.SizeOf(typeof(RenodeMessage));
+                var recvBytes = new byte[messageSize];
+
+                forwardSocket.Send(request.Serialize(), SocketFlags.None);
+                forwardSocket.Receive(recvBytes, 0, messageSize, SocketFlags.None);
+
+                var responseMessage = new RenodeMessage();
+                responseMessage.Deserialize(recvBytes);
+
+                return responseMessage;
+            }
+        }
+
+        private void BackwardConnectionLoop()
+        {
+            while(true)
+            {
+                var messageSize = Marshal.SizeOf(typeof(RenodeMessage));
+                var recvBytes = new byte[messageSize];
+
+                var nbytes = backwardSocket.Receive(recvBytes, 0, messageSize, SocketFlags.None);
+                if(nbytes == 0) {
+                    this.Log(LogLevel.Info, "Backward connection to SystemC process closed.");
+                    return;
+                }
+
+                var message = new RenodeMessage();
+                message.Deserialize(recvBytes);
+
+                ulong payload = 0;
+                switch(message.ActionId)
+                {
+                    case RenodeAction.GPIOWrite:
+                        // We have to respond before GPIO state is changed, because SystemC is blocked until
+                        // it receives the response. Setting the GPIO may require it to respond, e. g. when it
+                        // is interracted with from an interrupt handler.
+                        backwardSocket.Send(message.Serialize(), SocketFlags.None);
+                        for(int pin = 0; pin < NumberOfGPIOPins; pin++)
+                        {
+                            bool irqval = (message.Payload & (1UL << pin)) != 0;
+                            Connections[pin].Set(irqval);
+                        }
+                        break;
+                    case RenodeAction.Write:
+                        if(message.IsSystemBusConnection())
+                        {
+                            sysbus.TryGetCurrentCPU(out var icpu);
+                            switch(message.DataLength)
+                            {
+                                case 1:
+                                    sysbus.WriteByte(message.Address, (byte)message.Payload, context: icpu);
+                                    break;
+                                case 2:
+                                    sysbus.WriteWord(message.Address, (ushort)message.Payload, context: icpu);
+                                    break;
+                                case 4:
+                                    sysbus.WriteDoubleWord(message.Address, (uint)message.Payload, context: icpu);
+                                    break;
+                                case 8:
+                                    sysbus.WriteQuadWord(message.Address, message.Payload, context: icpu);
+                                    break;
+                                default:
+                                    this.Log(LogLevel.Error, "SystemC integration error - invalid data length {0} sent through backward connection from the SystemC process.", message.DataLength);
+                                    break;
+                            }
+                        }
+                        else
+                        {
+                            directAccessPeripherals[message.GetDirectConnectionIndex()].WriteDirect(
+                                    message.DataLength, (long)message.Address, message.Payload, message.ConnectionIndex);
+                        }
+                        backwardSocket.Send(message.Serialize(), SocketFlags.None);
+                        break;
+                    case RenodeAction.Read:
+                        if(message.IsSystemBusConnection())
+                        {
+                            sysbus.TryGetCurrentCPU(out var icpu);
+                            switch(message.DataLength)
+                            {
+                                case 1:
+                                    payload = (ulong)sysbus.ReadByte(message.Address, context: icpu);
+                                    break;
+                                case 2:
+                                    payload = (ulong)sysbus.ReadWord(message.Address, context: icpu);
+                                    break;
+                                case 4:
+                                    payload = (ulong)sysbus.ReadDoubleWord(message.Address, context: icpu);
+                                    break;
+                                case 8:
+                                    payload = (ulong)sysbus.ReadQuadWord(message.Address, context: icpu);
+                                    break;
+                                default:
+                                    this.Log(LogLevel.Error, "SystemC integration error - invalid data length {0} sent through backward connection from the SystemC process.", message.DataLength);
+                                    break;
+                            }
+                        }
+                        else
+                        {
+                            payload = directAccessPeripherals[message.GetDirectConnectionIndex()].ReadDirect(message.DataLength, (long)message.Address, message.ConnectionIndex);
+                        }
+                        var responseMessage = new RenodeMessage(message.ActionId, message.DataLength,
+                                message.ConnectionIndex, message.Address, payload);
+                        backwardSocket.Send(responseMessage.Serialize(), SocketFlags.None);
+                        break;
+                    default:
+                        this.Log(LogLevel.Error, "SystemC integration error - invalid message type {0} sent through backward connection from the SystemC process.", message.ActionId); 
+                        break;
+                }
+            }
+        }
+
+        private void TryToSkipTransactionTime(ulong timeUS)
+        {
+            if(machine.SystemBus.TryGetCurrentCPU(out var icpu))
+            {
+                var baseCPU = icpu as BaseCPU;
+                if(baseCPU != null)
+                {
+                     baseCPU.SkipTime(TimeInterval.FromMicroseconds(timeUS));
+                }
+                else
+                {
+                    this.Log(LogLevel.Error, "Failed to get CPU, all SystemC transactions processed as if they have no duration. This can desynchronize Renode and SystemC simulations.");
+                }
+            }
+        }
+
+        private readonly IBusController sysbus;
+        private readonly IMachine machine;
+
+        // NumberOfGPIOPins must be equal to renode_bridge.h:NUM_GPIO
+        private const int NumberOfGPIOPins = 64;
+
+        private readonly string address;
+        private readonly int port;
+        private readonly int timeSyncPeriodUS;
+        private readonly object messageLock;
+
+        private readonly Thread backwardThread;
+
+        private Dictionary<int, IDirectAccessPeripheral> directAccessPeripherals;
+        private string systemcExecutablePath;
+        private Process systemcProcess;
+
+        private ulong outGPIOState;
+
+        private Socket forwardSocket;
+        private Socket backwardSocket;
+    }
+}
diff --git a/src/Plugins/SystemCPlugin/SystemCModule/CMakeLists.txt b/src/Plugins/SystemCPlugin/SystemCModule/CMakeLists.txt
new file mode 100644
index 0000000..cf26517
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCModule/CMakeLists.txt
@@ -0,0 +1,20 @@
+cmake_minimum_required(VERSION 3.8)
+
+project(
+    RenodeSystemCBridge,
+    VERSION 0.1
+    DESCRIPTION "SystemC module for interfacing with Renode"
+    LANGUAGES CXX)
+
+file(GLOB_RECURSE SOURCES_SOCKET_CPP lib/socket-cpp/*.cpp)
+add_library(socket_cpp ${SOURCES_SOCKET_CPP})
+
+set(SOURCES_RENODE_BRIDGE
+    src/renode_bridge.cpp
+)
+add_library(renode_bridge ${SOURCES_RENODE_BRIDGE})
+target_include_directories(renode_bridge PUBLIC 
+    include
+    lib)
+
+target_link_libraries(renode_bridge socket_cpp)
diff --git a/src/Plugins/SystemCPlugin/SystemCModule/include/renode_bridge.h b/src/Plugins/SystemCPlugin/SystemCModule/include/renode_bridge.h
new file mode 100644
index 0000000..1d37462
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCModule/include/renode_bridge.h
@@ -0,0 +1,144 @@
+//
+// Copyright (c) 2010-2024 Antmicro
+//
+// This file is licensed under the MIT License.
+// Full license text is available in 'licenses/MIT.txt'.
+//
+#pragma once
+
+#include <memory>
+
+#include "tlm.h"
+#include "tlm_utils/simple_initiator_socket.h"
+#include "tlm_utils/simple_target_socket.h"
+
+struct CTCPClient;
+
+#ifndef RENODE_BUSWIDTH
+#define RENODE_BUSWIDTH 32
+#endif
+
+#define NUM_GPIO 64
+#define NUM_DIRECT_CONNECTIONS 4
+
+// ================================================================================
+// renode_bridge
+//
+//   SystemC module that serves as an interface with Renode.
+// ================================================================================
+
+class renode_bridge : sc_core::sc_module {
+public:
+  renode_bridge(sc_core::sc_module_name name, const char *address,
+                const char *port);
+  ~renode_bridge();
+
+public:
+  using renode_bus_target_socket =
+      tlm::tlm_target_socket<RENODE_BUSWIDTH, tlm::tlm_base_protocol_types, 1,
+                             sc_core::SC_ZERO_OR_MORE_BOUND>;
+  using renode_bus_initiator_socket =
+      tlm::tlm_initiator_socket<RENODE_BUSWIDTH, tlm::tlm_base_protocol_types,
+                                1, sc_core::SC_ZERO_OR_MORE_BOUND>;
+  using gpio_in_port = sc_core::sc_port<sc_core::sc_signal_in_if<bool>, 1,
+                                        sc_core::SC_ZERO_OR_MORE_BOUND>;
+  using gpio_out_port = sc_core::sc_port<sc_core::sc_signal_out_if<bool>, 1,
+                                         sc_core::SC_ZERO_OR_MORE_BOUND>;
+  using reset_port = sc_core::sc_port<sc_core::sc_signal_inout_if<bool>, 1,
+                                      sc_core::SC_ZERO_OR_MORE_BOUND>;
+
+  // Socket forwarding transactions performed in Renode to SystemC.
+  renode_bus_initiator_socket initiator_socket;
+
+  // Socket forwarding transactions performed in SystemC to Renode.
+  renode_bus_target_socket target_socket;
+
+  // Direct connections allow for binding peripherals directly to each other in
+  // Renode (bypassing System Bus).
+  renode_bus_initiator_socket
+      direct_connection_initiators[NUM_DIRECT_CONNECTIONS];
+  renode_bus_target_socket direct_connection_targets[NUM_DIRECT_CONNECTIONS];
+
+  // Input GPIO ports - signal changes are driven by SystemC and propagated to
+  // Renode.
+  gpio_in_port gpio_ports_in[NUM_GPIO];
+
+  // Output GPIO ports - signal changes are driven by Renode and propagated to
+  // SystemC.
+  gpio_out_port gpio_ports_out[NUM_GPIO];
+
+  // Reset signal.
+  // Raised when the peripheral is reset. Expected to be lowered by SystemC
+  // once the reset process is complete.
+  reset_port reset;
+
+private:
+  struct initiator_bw_handler: tlm::tlm_bw_transport_if<> {
+    initiator_bw_handler() = default;
+    void initialize(renode_bridge *);
+
+    virtual tlm::tlm_sync_enum nb_transport_bw(tlm::tlm_generic_payload &trans,
+                                               tlm::tlm_phase &phase,
+                                               sc_core::sc_time &t);
+    virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
+                                           sc_dt::uint64 end_range);
+
+  private:
+    renode_bridge *bridge;
+  };
+
+  struct target_fw_handler: tlm::tlm_fw_transport_if<> {
+    target_fw_handler() = default;
+
+    renode_bus_target_socket socket;
+
+    void initialize(renode_bridge *, uint8_t connection_idx);
+
+    virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
+                                           sc_dt::uint64 end_range);
+    virtual tlm::tlm_sync_enum
+    nb_transport_bw(tlm::tlm_generic_payload &payload, tlm::tlm_phase &phase,
+                    sc_core::sc_time &delta);
+    virtual tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload &trans,
+                                               tlm::tlm_phase &phase,
+                                               sc_core::sc_time &t);
+    virtual void b_transport(tlm::tlm_generic_payload &trans,
+                             sc_core::sc_time &delay);
+    virtual bool get_direct_mem_ptr(tlm::tlm_generic_payload &trans,
+                                    tlm::tlm_dmi &dmi_data);
+    virtual unsigned int transport_dbg(tlm::tlm_generic_payload &trans);
+
+  private:
+    renode_bridge *bridge;
+    uint8_t connection_idx;
+  };
+
+  void forward_loop();
+  void on_port_gpio();
+
+  void update_backward_gpio_state(uint64_t new_gpio_state);
+  void service_backward_request(tlm::tlm_generic_payload &payload,
+                                uint8_t connection_idx,
+                                sc_core::sc_time &delay);
+  int64_t get_systemc_time_us();
+
+  // Connection from Renode -> SystemC.
+  std::unique_ptr<CTCPClient> forward_connection;
+
+  // Connection from SystemC -> Renode
+  std::unique_ptr<CTCPClient> backward_connection;
+
+  // Construction/destruction of tlm_generic_payload is an expensive operation,
+  // so a single tlm_generic_payload object is reused, as recommended by OSCI
+  // TLM-2.0 Language Reference Manual. It also requires that the object is
+  // allocated on the heap.
+  std::unique_ptr<tlm::tlm_generic_payload> payload;
+
+  initiator_bw_handler bus_initiator_bw_handler;
+  target_fw_handler bus_target_fw_handler;
+
+  initiator_bw_handler dc_initiators[NUM_DIRECT_CONNECTIONS];
+  target_fw_handler dc_targets[NUM_DIRECT_CONNECTIONS];
+};
+
+// ================================================================================
diff --git a/src/Plugins/SystemCPlugin/SystemCModule/lib/socket-cpp b/src/Plugins/SystemCPlugin/SystemCModule/lib/socket-cpp
new file mode 120000
index 0000000..38ff62b
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCModule/lib/socket-cpp
@@ -0,0 +1 @@
+../../../VerilatorPlugin/VerilatorIntegrationLibrary/libs/socket-cpp
\ No newline at end of file
diff --git a/src/Plugins/SystemCPlugin/SystemCModule/src/renode_bridge.cpp b/src/Plugins/SystemCPlugin/SystemCModule/src/renode_bridge.cpp
new file mode 100644
index 0000000..7c239d3
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCModule/src/renode_bridge.cpp
@@ -0,0 +1,523 @@
+//
+// Copyright (c) 2010-2024 Antmicro
+//
+// This file is licensed under the MIT License.
+// Full license text is available in 'licenses/MIT.txt'.
+//
+#include "renode_bridge.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <chrono>
+#include <thread>
+
+#include "socket-cpp/Socket/TCPClient.h"
+
+// ================================================================================
+//  > Communication protocol
+// ================================================================================
+
+// Forward socket: Request from Renode, Response from SystemC
+// Backward socket: Request from SystemC, Response From Renode
+
+enum renode_action : uint8_t {
+  INIT = 0,
+  // Socket: forward only
+  // Init message received for the second time signifies Renode terminated and
+  // the process should exit. Request:
+  //     data_length: ignored
+  //     address: ignored
+  //     connection_index: ignored
+  //     payload: time synchronization granularity in us
+  //       TIMESYNC messages will be sent with this period. This does NOT
+  //       guarantee that the processes will never desynchronize by more than
+  //       this amount.
+  // Response:
+  //      Identical to the request message.
+  READ = 1,
+  // Socket: forward, backward
+  // Request:
+  //     data_length: number of bytes to read [1, 8]
+  //     address: address to read from, in target's address space payload: value
+  //     to write connection_index: 0 for SystemBus, [1, NUM_DIRECT_CONNECTIONS]
+  //     for direct connection
+  // Response:
+  //     address: duration of transaction in us
+  //     payload: read value
+  //     Otherwise identical to the request message.
+  WRITE = 2,
+  // Socket: forward, backward
+  // Request:
+  //     data_length: number of bytes to write [1, 8].
+  //     address: address to write to, in target's address space
+  //     connection_index: 0 for SystemBus, [1, NUM_DIRECT_CONNECTIONS] for
+  //     direct connection payload: value to write
+  // Response:
+  //     address: duration of transaction in us
+  //     Otherwise identical to the request message.
+  TIMESYNC = 3,
+  // Socket: forward only
+  // Request:
+  //     data_length: ignored
+  //     address: ignored
+  //     connection_index: ignored
+  // Response:
+  //     payload: current target virtual time in microseconds
+  //     Otherwise identical to the request message.
+  GPIOWRITE = 4,
+  // Socket: forward, backward
+  // Request:
+  //     data_length: ignored
+  //     address: ignored
+  //     connection_index: ignored
+  //     payload: state of GPIO bitfield
+  // Response:
+  //     Identical to the request message.
+  RESET = 5,
+  // Socket: forward
+  // Request:
+  //     data_length: ignored
+  //     address: ignored
+  //     connection_index: ignored
+  //     payload: ignored
+  // Response:
+  //     Identical to the request message.
+};
+
+#pragma pack(push, 1)
+struct renode_message {
+  renode_action action;
+  uint8_t data_length;
+  uint8_t connection_index;
+  uint64_t address;
+  uint64_t payload;
+};
+#pragma pack(pop)
+
+// ================================================================================
+//  > Debug printing
+// ================================================================================
+
+static void print_renode_message(renode_message *message) {
+  if (message->action == TIMESYNC)
+    return;
+  uint64_t thread_id = 0;
+  { // Get a cross-platform thread identifier
+    std::hash<std::thread::id> hasher;
+    thread_id = hasher(std::this_thread::get_id());
+  }
+  printf("[0x%08lX][RENODE MESSAGE] Action: ", thread_id);
+  switch (message->action) {
+  case INIT:
+    printf("INIT");
+    break;
+  case READ:
+    printf("READ");
+    break;
+  case WRITE:
+    printf("WRITE");
+    break;
+  case TIMESYNC:
+    printf("TIMESYNC");
+    break;
+  case GPIOWRITE:
+    printf("GPIOWRITE");
+    break;
+  case RESET:
+    printf("RESET");
+    break;
+  default:
+    printf("INVALID");
+  }
+  printf(" | Address: 0x%08lX", message->address);
+  printf(" | Payload: 0x%08lX", message->payload);
+  printf(" | ConnIdx: %u\n", message->connection_index);
+}
+
+static void print_transaction_status(tlm::tlm_generic_payload *payload) {
+  tlm::tlm_response_status status = payload->get_response_status();
+  std::string response_string = payload->get_response_string();
+  printf("Renode transport status: %s\n", response_string.c_str());
+}
+
+// ================================================================================
+//  > Renode Bridge SystemC module
+// ================================================================================
+
+static void initialize_payload(tlm::tlm_generic_payload *payload,
+                               const renode_message *message, uint8_t *data) {
+  tlm::tlm_command command = tlm::TLM_IGNORE_COMMAND;
+  switch (message->action) {
+  case WRITE:
+    command = tlm::TLM_WRITE_COMMAND;
+    break;
+  case READ:
+    command = tlm::TLM_READ_COMMAND;
+    break;
+  default:
+    assert(!"Only WRITE and READ messages should initialize TLM payload");
+  }
+
+  payload->set_command(command);
+  // Right now the address visible to SystemC is directly the offset
+  // from Renode; i. e. if we write to address 0x9000100 and the peripheral
+  // address is 0x9000000, then address in SystemC will be 0x100.
+  payload->set_address(message->address);
+  payload->set_data_ptr(data);
+  payload->set_data_length(message->data_length);
+  payload->set_byte_enable_ptr(nullptr);
+  payload->set_byte_enable_length(0);
+  payload->set_streaming_width(message->data_length);
+  payload->set_dmi_allowed(false);
+  payload->set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
+}
+
+static bool initialize_connection(CTCPClient *connection,
+                                  renode_message *message,
+                                  int64_t *out_max_desync_us) {
+  // Receive INIT message from Renode and use it to setup connection, e. g.
+  // time synchronization period.
+  // This is done during SystemC elaboration, once per lifetime of the module.
+  int nread = connection->Receive((char *)message, sizeof(renode_message));
+  if (nread <= 0) {
+    return false;
+  }
+
+#ifdef VERBOSE
+  print_renode_message(message);
+#endif
+
+  if (message->action != renode_action::INIT) {
+    fprintf(stderr, "Renode bridge connection error: missing INIT action.\n");
+    return false;
+  }
+  *out_max_desync_us = static_cast<int64_t>(message->payload);
+
+  // Acknowledge initialization is done.
+  connection->Send((char *)message, sizeof(renode_message));
+#ifdef VERBOSE
+  printf("Connection to Renode initialized with timesync period %lu us.\n",
+         *out_max_desync_us);
+#endif
+  return true;
+}
+
+static uint64_t sc_time_to_us(sc_core::sc_time time) {
+  // Converts sc_time to microseconds count.
+  return static_cast<int64_t>(time.to_seconds() * 1000000.0);
+}
+
+static uint64_t
+perform_transaction(renode_bridge::renode_bus_initiator_socket &socket,
+                    tlm::tlm_generic_payload *payload) {
+  sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
+  socket->b_transport(*payload, delay);
+#ifdef VERBOSE
+  print_transaction_status(payload);
+#endif
+  return sc_time_to_us(delay);
+}
+
+static void terminate_simulation(int exitstatus) {
+  sc_core::sc_stop();
+  exit(exitstatus);
+}
+
+static void connect_with_retry(CTCPClient* socket, const char* address, const char* port) {
+  constexpr uint32_t max_retry_s = 10;
+  constexpr uint32_t retry_interval_s = 2;
+
+  uint32_t retry_s = 0;
+  while (!socket->Connect(address, port)) {
+    fprintf(stderr, "Failed to connect to Renode, retrying in %us...\n", retry_interval_s);
+    std::this_thread::sleep_for(std::chrono::seconds(retry_interval_s));
+    retry_s += retry_interval_s;
+    if(retry_s >= max_retry_s) {
+        fprintf(stderr, "Maximum timeout reached. Failed to initialize Renode connection. Aborting.\n");
+        terminate_simulation(1);
+    }
+  }
+}
+
+SC_HAS_PROCESS(renode_bridge);
+renode_bridge::renode_bridge(sc_core::sc_module_name name, const char *address,
+                             const char *port)
+    : sc_module(name), initiator_socket("initiator_socket") {
+  SC_THREAD(forward_loop);
+  SC_THREAD(on_port_gpio);
+  for (int i = 0; i < NUM_GPIO; ++i) {
+    sensitive << gpio_ports_in[i];
+  }
+
+  bus_target_fw_handler.initialize(this, 0);
+
+  target_socket.bind(bus_target_fw_handler.socket);
+  for (int i = 0; i < NUM_DIRECT_CONNECTIONS; ++i) {
+    dc_initiators[i].initialize(this);
+    dc_targets[i].initialize(this, i + 1);
+    direct_connection_targets[i].bind(dc_targets[i]);
+    direct_connection_initiators[i].bind(dc_initiators[i]);
+  }
+
+  bus_initiator_bw_handler.initialize(this);
+  initiator_socket.bind(bus_initiator_bw_handler);
+
+  payload.reset(new tlm::tlm_generic_payload());
+
+  forward_connection.reset(new CTCPClient(NULL, ASocket::NO_FLAGS));
+  connect_with_retry(forward_connection.get(), address, port);
+
+  backward_connection.reset(new CTCPClient(NULL, ASocket::NO_FLAGS));
+  connect_with_retry(backward_connection.get(), address, port);
+}
+
+renode_bridge::~renode_bridge() {
+  forward_connection->Disconnect();
+  backward_connection->Disconnect();
+}
+
+void renode_bridge::forward_loop() {
+  // Processing of requests initiated by Renode.
+  uint8_t data[8] = {};
+
+  renode_message message;
+
+  int64_t max_desync_us;
+  if (!initialize_connection(forward_connection.get(), &message,
+                             &max_desync_us)) {
+    fprintf(stderr, "Failed to initialize Renode connection. Aborting.\n");
+    terminate_simulation(1);
+    return;
+  }
+
+  while (true) {
+    memset(data, 0, sizeof(data));
+
+    int nread =
+        forward_connection->Receive((char *)&message, sizeof(renode_message));
+    if (nread <= 0) {
+#ifdef VERBOSE
+      printf("Connection to Renode closed.\n");
+#endif
+      break;
+    }
+
+#ifdef VERBOSE
+    print_renode_message(&message);
+#endif
+
+    // Choose the appropriate initiator socket to initiate the transaction with.
+    renode_bus_initiator_socket *initiator_socket = nullptr;
+    if (message.connection_index > NUM_DIRECT_CONNECTIONS) {
+      fprintf(stderr,
+              "Invalid connection_index %u, exceeds available number of direct "
+              "connections (%u)\n",
+              message.connection_index, NUM_DIRECT_CONNECTIONS);
+      return;
+    }
+
+    if (message.connection_index == 0) {
+      initiator_socket = &this->initiator_socket;
+    } else {
+      initiator_socket =
+          &this->direct_connection_initiators[message.connection_index - 1];
+    }
+
+    switch (message.action) {
+    case renode_action::WRITE: {
+      initialize_payload(payload.get(), &message, data);
+
+      *((uint64_t *)data) = message.payload;
+
+      uint64_t delay = perform_transaction(*initiator_socket, payload.get());
+
+      // NOTE: address field is re-used here to pass timing information.
+      message.address = delay;
+      forward_connection->Send((char *)&message, sizeof(renode_message));
+
+      wait(sc_core::SC_ZERO_TIME);
+    } break;
+    case renode_action::READ: {
+      initialize_payload(payload.get(), &message, data);
+
+      uint64_t delay = perform_transaction(*initiator_socket, payload.get());
+
+      // NOTE: address field is re-used here to pass timing information.
+      message.address = delay;
+      message.payload = *((uint64_t *)data);
+      forward_connection->Send((char *)&message, sizeof(renode_message));
+      wait(sc_core::SC_ZERO_TIME);
+    } break;
+    case renode_action::TIMESYNC: {
+      // Renode drives the simulation time. This module never leaves the delta
+      // cycle loop until a TIMESYNC with future time is received. It then waits
+      // for the time difference between current virtual time and time from
+      // TIMESYNC, allowing the SystemC simulation to progress in time. This is
+      // effectively a synchronization barrier.
+      int64_t systemc_time_us = sc_time_to_us(sc_core::sc_time_stamp());
+      int64_t renode_time_us = (int64_t)message.payload;
+
+      int64_t dt = renode_time_us - systemc_time_us;
+      message.payload = systemc_time_us;
+      if (dt > max_desync_us) {
+        wait(dt, sc_core::SC_US);
+      }
+      message.payload = sc_time_to_us(sc_core::sc_time_stamp());
+      forward_connection->Send((char *)&message, sizeof(renode_message));
+    } break;
+    case renode_action::GPIOWRITE: {
+      for (int i = 0; i < NUM_GPIO; ++i) {
+        sc_core::sc_interface *interface = gpio_ports_out[i].get_interface();
+        if (interface != nullptr) {
+          gpio_ports_out[i]->write((message.payload & (1 << i)) != 0);
+        }
+      }
+      forward_connection->Send((char *)&message, sizeof(renode_message));
+    } break;
+    case renode_action::INIT: {
+      terminate_simulation(0);
+    } break;
+    case renode_action::RESET: {
+      sc_core::sc_interface *interface = reset.get_interface();
+      if (interface != nullptr) {
+        reset->write(true);
+      }
+      forward_connection->Send((char *)&message, sizeof(renode_message));
+    } break;
+    default:
+      fprintf(stderr, "Malformed message received from Renode - terminating simulation.\n");
+      terminate_simulation(1);
+    }
+  }
+}
+
+void renode_bridge::on_port_gpio() {
+  while (true) {
+    // Wait for a change in any of the GPIO ports.
+    wait();
+
+    uint64_t gpio_state = 0;
+    for (int i = 0; i < NUM_GPIO; ++i) {
+      sc_core::sc_interface *interface = gpio_ports_in[i].get_interface();
+      if (interface != nullptr) {
+        if (gpio_ports_in[i]->read()) {
+          gpio_state |= (1ull << i);
+        } else {
+          gpio_state &= ~(1ull << i);
+        }
+      }
+    }
+
+    renode_message message = {};
+    message.action = renode_action::GPIOWRITE;
+    message.payload = gpio_state;
+
+    backward_connection->Send((char *)&message, sizeof(renode_message));
+    // Response is ignored.
+    backward_connection->Receive((char *)&message, sizeof(renode_message));
+  }
+}
+
+void renode_bridge::service_backward_request(tlm::tlm_generic_payload &payload,
+                                             uint8_t connection_idx,
+                                             sc_core::sc_time &delay) {
+  renode_message message = {};
+  message.address = payload.get_address();
+  message.data_length = payload.get_data_length();
+  message.connection_index = connection_idx;
+
+  if (payload.is_read()) {
+    message.action = renode_action::READ;
+  } else if (payload.is_write()) {
+    message.action = renode_action::WRITE;
+    memcpy(&message.payload, payload.get_data_ptr(), message.data_length);
+  }
+
+  backward_connection->Send((char *)&message, sizeof(renode_message));
+  backward_connection->Receive((char *)&message, sizeof(renode_message));
+
+  if (payload.is_read()) {
+    memcpy(payload.get_data_ptr(), &message.payload, message.data_length);
+  }
+
+  payload.set_response_status(tlm::TLM_OK_RESPONSE);
+}
+
+// ================================================================================
+//   target_fw_handler
+// ================================================================================
+
+void renode_bridge::target_fw_handler::initialize(
+    renode_bridge *renode_bridge, uint8_t conn_idx) {
+  bridge = renode_bridge;
+  connection_idx = conn_idx;
+  socket.bind(*this);
+}
+
+void renode_bridge::target_fw_handler::b_transport(
+    tlm::tlm_generic_payload &payload, sc_core::sc_time &delay) {
+  bridge->service_backward_request(payload, connection_idx, delay);
+}
+
+tlm::tlm_sync_enum
+renode_bridge::target_fw_handler::nb_transport_fw(
+    tlm::tlm_generic_payload &trans, tlm::tlm_phase &phase,
+    sc_core::sc_time &t) {
+  bridge->service_backward_request(trans, connection_idx, t);
+  return tlm::TLM_COMPLETED;
+}
+
+tlm::tlm_sync_enum
+renode_bridge::target_fw_handler::nb_transport_bw(
+    tlm::tlm_generic_payload &, tlm::tlm_phase &, sc_core::sc_time &) {
+  fprintf(stderr, "[ERROR] nb_transport_bw not implemented for "
+                  "target_fw_handler.\n");
+  return tlm::TLM_COMPLETED;
+}
+
+void renode_bridge::target_fw_handler::invalidate_direct_mem_ptr(
+    sc_dt::uint64, sc_dt::uint64) {
+  fprintf(stderr, "[ERROR] invalidate_direct_mem_ptr not implemented for "
+                  "target_fw_handler.\n");
+}
+
+bool renode_bridge::target_fw_handler::get_direct_mem_ptr(
+    tlm::tlm_generic_payload &trans, tlm::tlm_dmi &dmi_data) {
+  fprintf(stderr, "[ERROR] get_direct_mem_ptr not implemented for "
+                  "target_fw_handler.\n");
+  return false;
+}
+
+unsigned int renode_bridge::target_fw_handler::transport_dbg(
+    tlm::tlm_generic_payload &trans) {
+  fprintf(stderr, "[ERROR] transport_dbg not implemented for "
+                  "target_fw_handler.\n");
+  return 0;
+}
+
+// ================================================================================
+//  initiator_bw_handler
+// ================================================================================
+
+void renode_bridge::initiator_bw_handler::initialize(
+    renode_bridge *renode_bridge) {
+  bridge = renode_bridge;
+}
+
+tlm::tlm_sync_enum renode_bridge::initiator_bw_handler::nb_transport_bw(
+    tlm::tlm_generic_payload &trans, tlm::tlm_phase &phase,
+    sc_core::sc_time &t) {
+  fprintf(stderr, "[ERROR] nb_transport_bw not implemented for "
+                  "initiator_bw_handler- this should never be called, "
+                  "as Renode integration only uses b_transfer.\n");
+  return tlm::TLM_COMPLETED;
+}
+
+void renode_bridge::initiator_bw_handler::invalidate_direct_mem_ptr(
+    sc_dt::uint64 start_range, sc_dt::uint64 end_range) {
+  fprintf(stderr, "[ERROR] invalidate_direct_mem_ptr not implemented for "
+                  "initiator_bw_handler - this should never be called, "
+                  "as Renode integration only uses b_transfer.\n");
+}
+
+// ================================================================================
diff --git a/src/Plugins/SystemCPlugin/SystemCPlugin.csproj b/src/Plugins/SystemCPlugin/SystemCPlugin.csproj
new file mode 100644
index 0000000..9501f41
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCPlugin.csproj
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>Antmicro.Renode.Plugins.SystemCPlugin</RootNamespace>
+    <AssemblyName>SystemCPlugin</AssemblyName>
+    <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
+    <PropertiesLocation>..\..\..\output\properties.csproj</PropertiesLocation>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+  </PropertyGroup>
+  <Import Project="$(PropertiesLocation)" />
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;$(DefineConstants)</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <LangVersion>7.2</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <LangVersion>7.2</LangVersion>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.ServiceModel" />
+    <Reference Include="System.Core" />
+    <Reference Include="Mono.Posix" Condition=" $(CurrentPlatform) != 'Windows'" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Peripheral\SystemCPeripheral.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Infrastructure\src\Emulator\Main\Emulator.csproj">
+      <Project>{2901AECB-A54F-4FD8-9AC1-033D86DC7257}</Project>
+      <Name>Emulator</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Infrastructure\src\Emulator\Peripherals\Peripherals.csproj">
+      <Project>{66400796-0C5B-4386-A859-50A2AC3F3DB7}</Project>
+      <Name>Peripherals</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="Peripheral\" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <Policies>
+          <DotNetNamingPolicy DirectoryNamespaceAssociation="PrefixedHierarchical" ResourceNamePolicy="FileName" />
+          <TextStylePolicy FileWidth="120" TabWidth="4" IndentWidth="4" RemoveTrailingWhitespace="True" TabsToSpaces="True" NoTabsAfterNonTabs="True" EolMarker="Unix" scope="text/x-csharp" />
+          <CSharpFormattingPolicy IndentBlock="True" IndentBraces="False" IndentSwitchSection="True" IndentSwitchCaseSection="True" LabelPositioning="OneLess" NewLinesForBracesInTypes="True" NewLinesForBracesInMethods="True" NewLinesForBracesInProperties="True" NewLinesForBracesInAccessors="True" NewLinesForBracesInAnonymousMethods="True" NewLinesForBracesInControlBlocks="True" NewLinesForBracesInAnonymousTypes="True" NewLinesForBracesInObjectCollectionArrayInitializers="True" NewLinesForBracesInLambdaExpressionBody="True" NewLineForElse="True" NewLineForCatch="True" NewLineForFinally="True" NewLineForMembersInObjectInit="True" NewLineForMembersInAnonymousTypes="True" NewLineForClausesInQuery="True" SpacingAfterMethodDeclarationName="False" SpaceWithinMethodDeclarationParenthesis="False" SpaceBetweenEmptyMethodDeclarationParentheses="False" SpaceAfterMethodCallName="False" SpaceWithinMethodCallParentheses="False" SpaceBetweenEmptyMethodCallParentheses="False" SpaceWithinExpressionParentheses="False" SpaceWithinCastParentheses="False" SpaceWithinOtherParentheses="False" SpaceAfterCast="False" SpacesIgnoreAroundVariableDeclaration="False" SpaceBeforeOpenSquareBracket="False" SpaceBetweenEmptySquareBrackets="False" SpaceWithinSquareBrackets="False" SpaceAfterColonInBaseTypeDeclaration="True" SpaceAfterComma="True" SpaceAfterDot="False" SpaceAfterSemicolonsInForStatement="True" SpaceBeforeColonInBaseTypeDeclaration="True" SpaceBeforeComma="False" SpaceBeforeDot="False" SpaceBeforeSemicolonsInForStatement="False" SpacingAroundBinaryOperator="Single" WrappingPreserveSingleLine="True" WrappingKeepStatementsOnSingleLine="True" PlaceSystemDirectiveFirst="True" SpaceAfterControlFlowStatementKeyword="False" scope="text/x-csharp" />
+          <TextStylePolicy FileWidth="120" TabWidth="4" IndentWidth="4" RemoveTrailingWhitespace="True" TabsToSpaces="True" NoTabsAfterNonTabs="True" EolMarker="Unix" scope="text/plain" />
+          <StandardHeader IncludeInNewFiles="True" Text="&#xA;Copyright (c) 2010-${Year} Antmicro&#xA;&#xA; This file is licensed under the MIT License.&#xA; Full license text is available in 'licenses/MIT.txt'.&#xA;" />
+        </Policies>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+</Project>
diff --git a/src/Plugins/SystemCPlugin/SystemCPlugin_NET.csproj b/src/Plugins/SystemCPlugin/SystemCPlugin_NET.csproj
new file mode 100644
index 0000000..47efd04
--- /dev/null
+++ b/src/Plugins/SystemCPlugin/SystemCPlugin_NET.csproj
@@ -0,0 +1,23 @@
+<Project DefaultTargets="Build" Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks Condition="$(OS) != 'Windows_NT'">net6.0</TargetFrameworks>
+    <TargetFrameworks Condition="$(OS) == 'Windows_NT'">net6.0-windows10.0.17763.0</TargetFrameworks>
+    <AssemblyName>SystemCPlugin</AssemblyName>
+    <PropertiesLocation>..\..\..\output\properties.csproj</PropertiesLocation>
+    <LangVersion>7.2</LangVersion>
+  </PropertyGroup>
+  <Import Project="$(PropertiesLocation)" />
+  <ItemGroup>
+    <PackageReference Include="System.ServiceModel.Duplex" Version="4.8.1" />
+    <PackageReference Include="System.ServiceModel.NetTcp" Version="4.8.1" />
+    <PackageReference Include="System.ServiceModel.Federation" Version="4.8.1" />
+    <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" Condition=" $(OS) != 'Windows_NT'" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Remove="SystemCModule\**\*"/>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Infrastructure\src\Emulator\Main\Emulator_NET.csproj"/>
+    <ProjectReference Include="..\..\Infrastructure\src\Emulator\Peripherals\Peripherals_NET.csproj"/>
+  </ItemGroup>
+</Project>
diff --git a/src/Renode/Renode.csproj b/src/Renode/Renode.csproj
index a558c2a..17bcfa7 100644
--- a/src/Renode/Renode.csproj
+++ b/src/Renode/Renode.csproj
@@ -232,6 +232,10 @@
       <Project>{887C6088-F483-466A-A671-06EEC42B8DC1}</Project>
       <Name>TracePlugin</Name>
     </ProjectReference>
+    <ProjectReference Include="..\Plugins\SystemCPlugin\SystemCPlugin.csproj">
+      <Project>{8882BDAF-FE52-4A39-B1F2-84C3F061D5A7}</Project>
+      <Name>SystemCPlugin</Name>
+    </ProjectReference>
     <ProjectReference Include="..\Plugins\WiresharkPlugin\WiresharkPlugin.csproj">
       <Project>{66A9995A-13AE-4454-88A6-29EB2D6F5988}</Project>
       <Name>WiresharkPlugin</Name>
diff --git a/tools/packaging/common_copy_files.sh b/tools/packaging/common_copy_files.sh
index 762fb2a..f9fb0a1 100644
--- a/tools/packaging/common_copy_files.sh
+++ b/tools/packaging/common_copy_files.sh
@@ -18,6 +18,12 @@
 cp -r $BASE/tools/sel4_extensions $DIR/tools
 cp -r $BASE/tools/csv2resd $DIR/tools
 cp -r $BASE/src/Plugins/VerilatorPlugin/VerilatorIntegrationLibrary $DIR/plugins
+cp -r $BASE/src/Plugins/SystemCPlugin/SystemCModule $DIR/plugins
+# For now, SystemCPlugin uses socket-cpp library from VerilatorIntegrationLibrary.
+# ln -f argument is quietly ignored in windows-package environment, so instead of updating remove the link
+# and create it again.
+rm -rf $DIR/plugins/SystemCModule/lib/socket-cpp
+ln -s ../../VerilatorIntegrationLibrary/libs/socket-cpp $DIR/plugins/SystemCModule/lib/socket-cpp
 
 #copy the test instrastructure and update the paths
 cp -r $BASE/tests/metrics-analyzer $DIR/tests/metrics-analyzer
diff --git a/tools/packaging/common_make_linux_portable.sh b/tools/packaging/common_make_linux_portable.sh
index 4ced2c2..344448f 100755
--- a/tools/packaging/common_make_linux_portable.sh
+++ b/tools/packaging/common_make_linux_portable.sh
@@ -15,6 +15,9 @@
 cp -r $RENODE_ROOT_DIR/tools/sel4_extensions $DESTINATION/tools
 cp -r $RENODE_ROOT_DIR/tools/csv2resd $DESTINATION/tools
 cp -r $RENODE_ROOT_DIR/src/Plugins/VerilatorPlugin/VerilatorIntegrationLibrary $DESTINATION/plugins
+cp -r $RENODE_ROOT_DIR/src/Plugins/SystemCPlugin/SystemCModule $DESTINATION/plugins
+# For now, SystemCPlugin uses socket-cpp library from VerilatorIntegrationLibrary.
+ln -fs ../../VerilatorIntegrationLibrary/libs/socket-cpp $DESTINATION/plugins/SystemCModule/lib/socket-cpp
 
 sed -i '/nunit/d' $DESTINATION/tests/run_tests.py
 sed -i 's#ROOT_PATH/tests/run_tests.py#TEST_PATH/run_tests.py#' $DESTINATION/renode-test