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