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.
Source and LRM Trail
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 .codex-src/systemc/src/sysc/communication: 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()).
Standard and Source Deep Dive: Port Binding
Port binding is the topological glue of a SystemC model. The IEEE 1666-2023 LRM Sections 4.2.1 (Elaboration) and Section 6.11-6.13 (Ports, Exports, Interfaces) rigidly define how structural connections are made and verified.
Inside the Accellera Source: sc_port_b and sc_port_registry
In src/sysc/communication/sc_port.h/cpp, all specialized sc_port<IF> classes derive from a non-template base class sc_port_b.
When you declare sc_port<BusIf> bus{"bus"};, the constructor ultimately calls sc_simcontext::get_port_registry()->insert(this).
The sc_port_registry (located in src/sysc/kernel/sc_simcontext.cpp) is the global list of every port in the simulation.
When you write cpu.bus.bind(subsystem.target); in your C++ code, you are invoking the bind() method on sc_port. However, this does not immediately resolve the C++ pointer! Instead, the port simply stores a generic pointer to the bound object in an internal array (because a port can be bound to multiple channels if the port's N parameter is > 1).
The Elaboration Phase: complete_binding()
The real magic happens when sc_start() is called.
Before simulation begins, sc_start() invokes sc_simcontext::elaborate(), which ultimately calls sc_port_registry::complete_binding().
If you trace sysc/kernel/sc_simcontext.cpp, you will see complete_binding() iterate over every single port in the design. For each port:
- It traverses the binding tree. If Port A is bound to Port B, and Port B is bound to Channel C, it recursively walks from A -> B -> C to find the actual
sc_interfaceimplementation. - Type Checking: It uses C++ RTTI (
dynamic_cast) to verify that the target object actually implements the interface required by the port.// Abstract representation of the kernel's check: sc_interface* target_if = dynamic_cast<sc_interface*>(bound_object); if (!target_if) { SC_REPORT_ERROR("Port binding failed: interface mismatch"); } - It resolves the final interface pointer and stores it directly inside the port's
m_interfacepointer array.
Zero-Overhead Simulation Dispatch
Why delay pointer resolution until complete_binding()? Because once elaboration finishes, the port has an absolute, direct C++ pointer to the implementing channel.
In src/sysc/communication/sc_port.h, the overloaded operator-> is extraordinarily simple:
template <class IF>
inline IF* sc_port<IF>::operator -> () {
return m_interface;
}During simulation, when a thread executes bus->write(0x10, data);, there are no map lookups, no string comparisons, and no routing tables. It is exactly equivalent to a direct C++ virtual function call on the channel object.
Comments and Corrections