LRM Bridge: Ports, Exports, Interfaces, and Channels
The standard communication model: sc_interface, sc_port, sc_export, sc_prim_channel, sc_channel, and binding policies.
How to Read This Lesson
This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.
SystemC hardware communication is strictly interface-based, utilizing a separation of concerns defined by the LRM. Let's dig into the IEEE 1666 rules and the Accellera C++ source code to see how ports actually resolve into pointers.
Standard and source context
The Four Pillars of Communication
sc_interface: An abstract C++ class defining what can be done (e.g.,read(),write()). Under the hood, it registers itself directly with the simulation context to enable type-safe dynamic casting and binding during elaboration.- Channel (
sc_channelorsc_prim_channel): A module that implements the interface. It contains the actual state and logic. sc_port: An outward-facing connection on a module that requires an interface to function. Under the hood,sc_portis just a proxy object that internally holds an array ofsc_interface*pointers.sc_export: An inward-facing connection on a module boundary that provides an interface implemented by a child module. It acts as a reverse proxy, delegating incoming bindings down to the internalsc_interface.
End-to-End Interface and Channel Example
This fully compliant IEEE 1666 example demonstrates defining an interface, implementing a hierarchical channel, and binding a port to it.
#include <systemc>
// 1. Define the Interface (inheriting from sc_interface)
struct RegisterIf : virtual public sc_core::sc_interface {
virtual uint32_t read(uint32_t offset) = 0;
virtual void write(uint32_t offset, uint32_t data) = 0;
};
// 2. Implement the Channel (inheriting from sc_channel and the interface)
class RegisterBank : public sc_core::sc_channel, public RegisterIf {
private:
uint32_t memory[256];
public:
SC_HAS_PROCESS(RegisterBank);
RegisterBank(sc_core::sc_module_name name) : sc_core::sc_channel(name) {
for (int i = 0; i < 256; i++) memory[i] = 0;
}
uint32_t read(uint32_t offset) override {
if (offset < 256) {
std::cout << "@" << sc_core::sc_time_stamp() << " [Bank] Read 0x"
<< std::hex << memory[offset] << " from " << offset << std::endl;
return memory[offset];
}
return 0;
}
void write(uint32_t offset, uint32_t data) override {
if (offset < 256) {
std::cout << "@" << sc_core::sc_time_stamp() << " [Bank] Wrote 0x"
<< std::hex << data << " to " << offset << std::endl;
memory[offset] = data;
}
}
};
// 3. Define a Module requiring the interface via a Port
SC_MODULE(CPU_Model) {
// Requires a RegisterIf implementation
sc_core::sc_port<RegisterIf> regs{"regs"};
SC_CTOR(CPU_Model) {
SC_THREAD(execute_logic);
}
void execute_logic() {
wait(10, sc_core::SC_NS);
regs->write(0x10, 0xDEADBEEF); // Accesses the channel via the port interface
wait(10, sc_core::SC_NS);
uint32_t val = regs->read(0x10);
}
};
// 4. Encapsulate with an Export
SC_MODULE(Subsystem) {
// Exposes the internal RegisterBank to the outside world
sc_core::sc_export<RegisterIf> target_export{"target_export"};
RegisterBank regs{"regs"};
SC_CTOR(Subsystem) {
// Bind the export to the internal channel
target_export.bind(regs);
}
};
int sc_main(int argc, char* argv[]) {
CPU_Model cpu("cpu");
Subsystem subsys("subsys");
// Bind the CPU's port to the Subsystem's export
cpu.regs.bind(subsys.target_export);
sc_core::sc_start(50, sc_core::SC_NS);
return 0;
}Primitive vs Hierarchical Channels and the Kernel
- Primitive Channels (
sc_prim_channel): Used for fundamental data types (likesc_signal). Under the hood, they hook directly into the Evaluate-Update paradigm. When you write to a signal,sc_prim_channel::request_update()pushes thethispointer intosc_simcontext::m_update_list. Later, during the Update Phase, the scheduler loops over this list and calls the purely virtualupdate()method. They cannot have structural hierarchy (child modules) orSC_THREADs. - Hierarchical Channels (
sc_channel): Modules that implement an interface (as shown in the example above). They derive directly fromsc_moduleand can contain internal processes (SC_THREAD), ports, and child modules. They do not automatically hook into the delta-cycle update list.
Binding Policies
The LRM enforces strict binding constraints during the elaboration phase.
Under the Hood: When you call cpu.regs.bind(...), the port does not resolve the pointer immediately. It stores a sc_bind_info struct in a deferred queue. When sc_start() is called, sc_simcontext::elaborate() resolves all these proxy chains.
sc_port can accept a binding policy template argument:
SC_ONE_OR_MORE_BOUND: The port must be bound at least once (default).SC_ZERO_OR_MORE_BOUND: The port may remain unbound (useful for optional interrupts). If accessed without binding, a fatal error is thrown.SC_ALL_BOUND: All elements of a multi-port array must be bound.
If a connection executes behavior, model it as an interface and channel. If a connection crosses structural hierarchy, expose it cleanly with an export.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections