sc_port and sc_export in SystemC — Required and Provided Interfaces
You already know that a SystemC channel — like sc_signal or a custom channel — implements one or more sc_interface derivatives. But how does a module reach a channel it does not own? And how does a module advertise that it contains a channel to the outside world? That is exactly what sc_port and sc_export answer.
These two constructs split every connection into a requirer side and a provider side. Once you see that division clearly, the binding rules, the hierarchy constraints, and even the elaboration error messages become obvious. This article covers both thoroughly — declaration, binding, multi-binding, hierarchical chains, and the elaboration rules SystemC enforces before sc_start().
1. Two Sides of Every Connection
Every connection between a module and a channel has a direction: someone needs the service, and someone provides it.
sc_port<IF>— the module says "I need a service that implementsIF." It reaches outward, expecting something external to supply the implementation. Think of it as a connector jack on a PCB: it is deliberately left open for something to plug into.sc_export<IF>— the module says "I provide a service that implementsIF." It exposes an internal channel to the outside world as a named socket. Think of it as a socket on the PCB: something inside the board is connected to it, and external boards plug into it.
The hardware analogy maps cleanly: when board A plugs into board B's socket, board A has the jack (sc_port) and board B has the socket (sc_export). The trace inside board B connecting the socket to the actual component is the internal channel binding.
sc_port is visible to the module's processes — they call through it. An sc_export is visible to the outside — other modules bind to it. Neither is the channel itself; both are typed references to whichever channel gets bound during elaboration.
2. sc_port<IF> — Requiring a Service
Declare an sc_port as a member of SC_MODULE, templated on the interface you need. The module's processes then call through it using the -> operator, which dereferences to the bound interface pointer.
// Interface definition (see sc-interface-in-systemc.html) struct mem_if : virtual sc_interface { virtual uint32_t read (uint32_t addr) = 0; virtual void write(uint32_t addr, uint32_t data) = 0; }; SC_MODULE(Master) { sc_port<mem_if> mem_port; // requires exactly one binding (default) SC_CTOR(Master) { SC_THREAD(run); } void run() { mem_port->write(0x1000, 0xDEAD); // -> dereferences to bound interface uint32_t val = mem_port->read(0x1000); wait(10, SC_NS); } };
The full template signature is sc_port<IF, N, POLICY>. The second parameter, N, controls how many channels can be bound to a single port.
// Exactly 1 binding (default) — sc_port<IF, 1> sc_port<mem_if> mem_port; // Exactly 4 bindings — e.g. four memory banks sc_port<mem_if, 4> bank_port; // Unbounded — any number of bindings allowed sc_port<mem_if, 0> multi_port;
When N > 1 or N == 0, each bound channel is accessed by index:
// Access individual bindings by index uint32_t v0 = bank_port[0]->read(0x0000); // bank 0 uint32_t v1 = bank_port[1]->read(0x0000); // bank 1 // Single-binding port: operator-> and [0]-> are equivalent mem_port->read(0); // same as mem_port[0]->read(0)
port[N] where N >= actual_bindings is undefined behavior. At elaboration time SystemC validates the total binding count, but individual index accesses in process code are your responsibility.
3. sc_export<IF> — Providing a Service
Declare an sc_export as a member of SC_MODULE, templated on the interface it exposes. Inside the module's constructor, bind it to the internal channel that implements that interface. External modules then bind their sc_port directly to this export — they never see the internal channel type.
SC_MODULE(Ram) { sc_export<mem_if> mem_export; // what the outside world sees SC_CTOR(Ram) { mem_export(m_storage); // bind export to internal implementation } private: RamStorage m_storage; // implements mem_if, lives inside Ram };
From the outside, ram.mem_export is the public interface — it is what gets wired in sc_main or by the parent module. The RamStorage type is an implementation detail; callers only need to know about mem_if.
4. Flat Binding — sc_port to Channel
The most common case: both the module with the sc_port and the channel live at the same level of hierarchy, typically in sc_main. Binding uses the function-call syntax: module.port(channel).
int sc_main(int, char**) { RamChannel ram("ram"); // channel — implements mem_if Master master("master"); // has sc_port<mem_if> mem_port master.mem_port(ram); // flat binding: port → channel directly sc_start(100, SC_NS); return 0; }
SystemC resolves this binding during the elaboration phase, before sc_start() is called. At that point it walks the module hierarchy, verifies that every port has the required number of bindings, and builds the internal interface pointer table. After elaboration, the binding is fixed — you cannot rebind ports at runtime.
// Multiple sc_port bindings at the same level sc_port<mem_if, 2> dual_port; // requires exactly 2 bindings RamChannel bank0("bank0"); RamChannel bank1("bank1"); master.dual_port(bank0); // first binding — index 0 master.dual_port(bank1); // second binding — index 1
5. Hierarchical Binding — sc_port to sc_export
When the channel lives inside a sub-module, that sub-module wraps it with an sc_export. The parent or sc_main then binds the requirer's sc_port to the sub-module's sc_export. SystemC chains the binding internally so that calls through the port eventually reach the channel.
// MemorySubsystem hides its internal channel behind an export SC_MODULE(MemorySubsystem) { sc_export<mem_if> mem_export; // public socket SC_CTOR(MemorySubsystem) { mem_export(m_ram); // wire export to internal channel } private: RamChannel m_ram; // hidden from the outside }; int sc_main(int, char**) { MemorySubsystem mem_sys("mem_sys"); Master master("master"); // Hierarchical binding: sc_port → sc_export → internal channel master.mem_port(mem_sys.mem_export); sc_start(100, SC_NS); return 0; }
The full chain at runtime is: master.mem_port → mem_sys.mem_export → mem_sys.m_ram. From the master's perspective, mem_port->read(addr) calls m_ram.read(addr) with zero overhead after elaboration — the interface pointer is resolved once and stored directly.
sc_export: encapsulation. You can swap RamChannel for a different implementation — a model with latency, a TLM socket, a stub — without changing any code in Master or sc_main. Only MemorySubsystem's constructor changes.
6. sc_export to sc_port — Connecting Inside a Hierarchy
Hierarchical binding works in both directions. A parent module can also forward connections between its own ports/exports and those of its child modules. There are two patterns:
Pattern A — forwarding an inner sc_port outward as an sc_export. The parent module declares an sc_export and binds it to a child's sc_port — effectively promoting the child's requirement to the module boundary so the grandparent can satisfy it.
Pattern B — forwarding an outer sc_port inward to a child sc_port. The parent module has an sc_port (a requirement from its own parent) and wires it directly to a child module's sc_port. This propagates a requirement downward through the hierarchy.
// Pattern B — parent sc_port forwarded to child sc_port SC_MODULE(Cluster) { sc_port<mem_if> mem_port; // cluster's own requirement Core core0, core1; SC_CTOR(Cluster) : core0("core0"), core1("core1") { // Forward cluster's port to both cores' ports core0.mem_port(mem_port); // sc_port → sc_port (downward) core1.mem_port(mem_port); } };
The binding direction rules are:
sc_portbinds TO: a channel, ansc_exportat the same or parent level, or anothersc_portat a child level (downward forwarding).sc_exportbinds TO: a channel (direct), or anothersc_exportat a child level (upward delegation).
sc_export can only delegate to a child's sc_export (going deeper into the hierarchy) or bind directly to a channel. Binding two sibling sc_exports together is a structural error — only one of them can be the actual provider.
7. Elaboration Binding Rules
SystemC's elaboration phase runs after all module constructors complete but before sc_start() hands control to the scheduler. During elaboration, the kernel calls end_of_elaboration() on every module and then verifies that every port is correctly bound.
The policy is controlled by the third template parameter of sc_port:
// SC_ONE_OR_MORE_BOUND — default // Requires exactly N bindings (where N is the second parameter). // Unbound port triggers sc_report_error before sc_start(). sc_port<mem_if, 1, SC_ONE_OR_MORE_BOUND> mem_port; // default // SC_ZERO_OR_MORE_BOUND — optional port // Binding check is skipped entirely. Access without binding is undefined. sc_port<mem_if, 1, SC_ZERO_OR_MORE_BOUND> opt_port; // SC_ALL_BOUND — all N slots must be filled // The port accepts exactly the N bindings declared and no more. sc_port<mem_if, 4, SC_ALL_BOUND> bank_port;
In practice, SC_ZERO_OR_MORE_BOUND is used for optional connections — for example, a debug tap port that is only wired in certain configurations. Always guard access to such a port with a null check:
// Safe access for an optional port if (debug_port.size() > 0) { debug_port->dump(addr); }
An sc_export that is declared but never bound to a channel inside its module also triggers an elaboration error. The kernel cannot call through an export that has no backing implementation.
8. Complete Example — Memory Subsystem with Hidden Internals
This example shows the full picture: a MemorySubsystem module containing separate read-only and read-write channels, wrapped behind exports, with a Master that uses both without knowing what is inside.
#include <systemc.h> #include <iostream> using namespace std; // ── Interfaces ──────────────────────────────────────────────── struct mem_read_if : virtual sc_interface { virtual uint32_t read(uint32_t addr) = 0; }; struct mem_write_if : virtual sc_interface { virtual void write(uint32_t addr, uint32_t data) = 0; }; // ── Internal channels (implementation details) ─────────────── struct RomChannel : sc_module, mem_read_if { uint32_t rom[256] = {}; explicit RomChannel(sc_module_name n) : sc_module(n) { rom[0] = 0xBAADF00D; // pre-loaded firmware word } uint32_t read(uint32_t addr) override { return rom[addr & 0xFF]; } }; struct RamChannel : sc_module, mem_read_if, mem_write_if { uint32_t ram[256] = {}; explicit RamChannel(sc_module_name n) : sc_module(n) {} uint32_t read(uint32_t addr) override { return ram[addr & 0xFF]; } void write(uint32_t addr, uint32_t data) override { ram[addr & 0xFF] = data; } }; // ── MemorySubsystem — hides channels behind exports ─────────── SC_MODULE(MemorySubsystem) { sc_export<mem_read_if> rom_export; // read-only view sc_export<mem_read_if> ram_read_export; sc_export<mem_write_if> ram_write_export; SC_CTOR(MemorySubsystem) : m_rom("rom"), m_ram("ram") { rom_export(m_rom); // bind exports to channels ram_read_export(m_ram); ram_write_export(m_ram); // same channel, two interface views } private: RomChannel m_rom; RamChannel m_ram; }; // ── Master — only knows about the interfaces ────────────────── SC_MODULE(Master) { sc_port<mem_read_if> rom_port; sc_port<mem_read_if> ram_rd_port; sc_port<mem_write_if> ram_wr_port; SC_CTOR(Master) { SC_THREAD(run); } void run() { // Read firmware word from ROM uint32_t fw = rom_port->read(0); cout << "[MASTER] ROM[0] = 0x" << hex << fw << endl; // Write and read back from RAM ram_wr_port->write(0x10, 0xCAFE); uint32_t rd = ram_rd_port->read(0x10); cout << "[MASTER] RAM[0x10] = 0x" << hex << rd << endl; wait(10, SC_NS); } }; // ── sc_main ─────────────────────────────────────────────────── int sc_main(int, char**) { MemorySubsystem mem("mem"); Master master("master"); // Hierarchical binding: sc_port → sc_export → internal channel master.rom_port (mem.rom_export); master.ram_rd_port (mem.ram_read_export); master.ram_wr_port (mem.ram_write_export); sc_start(50, SC_NS); return 0; }
Expected output:
[MASTER] ROM[0] = 0xbaadf00d [MASTER] RAM[0x10] = 0xcafe
Note that m_ram is bound to two different exports simultaneously — ram_read_export and ram_write_export. This is perfectly valid: the same channel object implements both mem_read_if and mem_write_if, and each export holds a pointer to its respective interface vtable entry. The master sees them as two independent ports with different types, even though they bottom out at the same object.
9. Common Pitfalls
Pitfall 1 — Binding sc_export to sc_export at the same hierarchy level
sc_export can only delegate downward — to a child module's export or directly to a channel. Two sibling modules cannot wire their exports together: neither is the provider; both are sockets waiting to be filled. The correct fix is to create a channel that both can bind to, or restructure so one is a requirer (sc_port) and the other is a provider (sc_export).
// WRONG — two exports at the same level cannot be bound to each other mod_a.some_export(mod_b.some_export); // elaboration error // CORRECT — create a channel and bind both sides to it MyChannel ch("ch"); mod_a.some_export(ch); mod_b.some_port(ch); // or bind mod_b's export to ch if mod_b provides it
Pitfall 2 — Declaring sc_export but forgetting to bind it inside the module
An sc_export that has no backing channel is an empty socket. SystemC will report an elaboration error when it tries to resolve the interface pointer. The binding must happen in the module constructor, not in before_end_of_elaboration() or later.
SC_MODULE(Broken) { sc_export<mem_if> mem_export; RamChannel m_ram; SC_CTOR(Broken) : m_ram("ram") { // BUG: forgot mem_export(m_ram); — elaboration will fail } }; // Fix: always bind exports in the constructor SC_CTOR(Fixed) : m_ram("ram") { mem_export(m_ram); // must be here }
Pitfall 3 — Using sc_port where sc_export is needed
If your module implements a service (contains a channel) and wants to expose it, you need sc_export, not sc_port. Using sc_port declares a requirement — it means "I need something bound to me from outside." An external caller trying to bind their port to your port will get an elaboration type mismatch.
// WRONG — sc_port says "I need a service", not "I provide one" SC_MODULE(BadRam) { sc_port<mem_if> mem_port; // implies BadRam is a consumer — wrong ... }; // CORRECT — sc_export says "I provide this service" SC_MODULE(GoodRam) { sc_export<mem_if> mem_export; // correct: GoodRam is a provider ... };
Summary
sc_port<IF>is a requirement: the module needs an implementation ofIFsupplied from outside. Processes call through it withport->method().sc_export<IF>is a provision: the module contains an implementation ofIFand makes it accessible to the outside. Bind it to the internal channel in the constructor.- Flat binding:
module.port(channel)when both live at the same hierarchy level. - Hierarchical binding:
module.port(sub.export)when the channel lives inside a sub-module. The interface pointer is resolved to the channel once during elaboration. - Multi-binding:
sc_port<IF, N>allowsNbindings; access individual ones withport[i]->method(). - All ports must be bound before
sc_start(). UseSC_ZERO_OR_MORE_BOUNDonly for genuinely optional connections, and guard access with a size check. - The same channel can back multiple exports — each export holds a pointer to a different interface slice of the same object.
Channels & Interfaces — 4-part series