Ports, Interfaces, Exports, and Channels
How modules connect through interfaces, why ports are typed, and where sc_export fits.
How to Read This Lesson
Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.
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.
Standard and source context
The standard contract lives in Docs/LRMs/SystemC_LRM_1666-2023.pdf around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is Accellera SystemC GitHub repository: sc_port, sc_export, sc_interface, sc_prim_channel, sc_signal, sc_fifo, and the writer policy helpers.
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.
Under the Hood: Port Binding and Virtual Interfaces
A port (sc_core::sc_port) is fundamentally a safe wrapper around a C++ pointer to an interface (sc_core::sc_interface).
When you bind a port p(signal), the sc_port_base::bind() method is invoked (sysc/communication/sc_port.cpp). The kernel does not immediately resolve the binding. Instead, it adds the binding pair to a list.
During the complete_binding() phase, the kernel walks through the connections. If multiple ports are bound hierarchically (Port -> Port -> Channel), the kernel traverses the chain until it finds the actual sc_interface implementation. It then caches this interface pointer directly inside the port.
Thus, calling p->read() has almost zero overhead during simulation—it is just a standard C++ virtual function call (interface_ptr->read()).
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections