Chapter 5: Source Internals

Source Deep Dive: Ports, Signals, and TLM Sockets

How the library turns interface binding, deferred updates, and transaction sockets into usable APIs.

Three source areas explain most user-facing SystemC behavior: ports, signals, and TLM sockets. This section looks at the mechanics defined by the IEEE 1666 standard and how they map to C++.

Ports Are Typed Access Points

At the API level, a port looks like a simple templated object, but at elaboration time, that port must bind to a channel implementing the specified interface. The LRM dictates strict port binding rules to ensure the topological integrity of the system before simulation begins.

Exports Turn Hierarchy Inside Out

An export exposes an interface from a module boundary to its parent or peers, but the actual implementation of that interface is provided by a child module or channel instantiated within.

Signals Are Deferred-Update Channels

To understand how sc_signal provides deferred updates in accordance with LRM semantics, we can build a complete, custom primitive channel that mimics its evaluate-update behavior.

#include <systemc>
 
using namespace sc_core;
 
// 1. Define the Interface
template <typename T>
struct custom_signal_if : virtual public sc_interface {
  virtual const T& read() const = 0;
  virtual void write(const T&) = 0;
  virtual const sc_event& default_event() const = 0;
};
 
// 2. Implement the Primitive Channel
template <typename T>
class custom_signal : public sc_prim_channel, public custom_signal_if<T> {
private:
  T m_current_value;
  T m_new_value;
  sc_event m_value_changed;
 
public:
  explicit custom_signal(const char* name) : sc_prim_channel(name), m_current_value(T()), m_new_value(T()) {}
 
  const T& read() const override {
    return m_current_value;
  }
 
  void write(const T& val) override {
    if (val != m_new_value) {
      m_new_value = val;
      request_update(); // Register with the kernel for the update phase
    }
  }
 
  const sc_event& default_event() const override {
    return m_value_changed;
  }
 
protected:
  void update() override {
    // Called by the kernel during the update phase
    if (m_current_value != m_new_value) {
      m_current_value = m_new_value;
      m_value_changed.notify(SC_ZERO_TIME); // Delta notification
    }
  }
};
 
// 3. Test Module
SC_MODULE(TestModule) {
  sc_port< custom_signal_if<int> > port{"port"};
 
  SC_CTOR(TestModule) {
    SC_THREAD(run);
  }
 
  void run() {
    port->write(42);
    // Notice that read() still returns 0 here because update() hasn't happened yet!
    std::cout << "Immediate read: " << port->read() << "\n";
    wait(SC_ZERO_TIME);
    // After a delta cycle, the update phase has run
    std::cout << "Read after delta: " << port->read() << "\n";
  }
};
 
int sc_main(int, char*[]) {
  custom_signal<int> sig("sig");
  TestModule mod("mod");
  mod.port(sig);
  
  sc_start();
  return 0;
}

The real sc_signal handles writer policies, tracing, reset integration, and specialized logic types, but the core LRM mechanism relies directly on sc_prim_channel, request_update(), and the virtual update() callback.

TLM Sockets Package Binding Patterns

TLM sockets are built to make transaction-level binding ergonomic. They inherit from both a port (to make outbound calls) and an export (to receive inbound calls).

Utility sockets such as tlm_utils::simple_target_socket let you register a member function as the transport callback. We can demonstrate this wrapping inside a complete compilable example.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
 
using namespace sc_core;
 
SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket{"socket"};
 
  SC_CTOR(Memory) {
    socket.register_b_transport(this, &Memory::b_transport);
  }
 
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
    std::cout << "Target received TLM command at " << sc_time_stamp() << "\n";
  }
};
 
SC_MODULE(Cpu) {
  tlm_utils::simple_initiator_socket<Cpu> socket{"socket"};
  SC_CTOR(Cpu) { SC_THREAD(run); }
  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    socket->b_transport(trans, delay);
  }
};
 
int sc_main(int, char*[]) {
  Cpu cpu("cpu");
  Memory mem("mem");
  cpu.socket.bind(mem.socket);
  sc_start();
  return 0;
}

That single bind() line hides a lot of structure. The socket has to expose the target interface, receive calls from an initiator, and dispatch them to your callback with the payload and delay, complying with the IEEE 1666-2023 standard for TLM-2.0 core interfaces.

Comments and Corrections