Chapter 3: Communication

Ports, Interfaces, Exports, and Channels

How modules connect through interfaces, why ports are typed, and where sc_export fits.

SystemC communication is built around interfaces. A port requires an interface. A channel implements an interface. An export exposes an interface from inside a module.

That separation is one of the most important design choices in SystemC.

Interfaces

An interface is an abstract contract:

#include <systemc>
 
struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};

The interface says what can be done. It does not say how storage, timing, arbitration, tracing, or contention are implemented.

Channels

A channel implements the interface. Let's make a complete compilable example of a memory channel. Note how it inherits from sc_core::sc_channel and BusIf:

#include <systemc>
 
struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};
 
class SimpleMemory : public sc_core::sc_channel, public BusIf {
public:
  int data[256]{};
 
  SC_CTOR(SimpleMemory) {}
 
  int read(unsigned address) override {
    return data[address & 0xff];
  }
 
  void write(unsigned address, int value) override {
    data[address & 0xff] = value;
  }
};
 
int sc_main(int, char*[]) {
  SimpleMemory mem("mem");
  mem.write(0x0, 42);
  return 0;
}

This channel is untimed and simple. Later you could add wait() in a thread-aware interface, model bus latency, or record accesses for debugging.

Ports and Exports

A module uses a port to call an interface, and an export lets a module provide an interface implemented by a child object. Here is a complete model putting it all together:

#include <systemc>
 
struct BusIf : sc_core::sc_interface {
  virtual int read(unsigned address) = 0;
  virtual void write(unsigned address, int data) = 0;
};
 
class SimpleMemory : public sc_core::sc_channel, public BusIf {
public:
  int data[256]{};
  SC_CTOR(SimpleMemory) {}
  int read(unsigned address) override { return data[address & 0xff]; }
  void write(unsigned address, int value) override { data[address & 0xff] = value; }
};
 
SC_MODULE(CpuModel) {
  sc_core::sc_port<BusIf> bus{"bus"};
 
  SC_CTOR(CpuModel) {
    SC_THREAD(run);
  }
 
  void run() {
    bus->write(0x10, 7);
    int value = bus->read(0x10);
    std::cout << "CPU Read value: " << value << " at time " << sc_core::sc_time_stamp() << "\n";
  }
};
 
SC_MODULE(MemorySubsystem) {
  sc_core::sc_export<BusIf> target{"target"};
  SimpleMemory mem{"mem"};
 
  SC_CTOR(MemorySubsystem) {
    target.bind(mem);
  }
};
 
int sc_main(int, char*[]) {
  CpuModel cpu("cpu");
  MemorySubsystem mem_sys("mem_sys");
  
  cpu.bus.bind(mem_sys.target);
  sc_core::sc_start();
  return 0;
}

The port is typed by the interface, not by the concrete channel. This keeps the CPU model from depending on a particular memory implementation. The parent module exposes target, while the real implementation lives in mem. This is useful for hierarchy: external modules bind to the subsystem, while internal structure remains hidden.

Source-Code Angle

In the reference implementation, ports and exports are part of a binding system layered over C++ pointers and interface references. The kernel validates that a required interface exists and that the final object graph is consistent before simulation begins.

That is why binding errors are usually elaboration errors, not C++ type errors alone.

Comments and Corrections