Chapter 7: SystemC 1666-2023 LRM

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.

Source and LRM Trail

This chapter is the LRM bridge. The primary reference is Docs/LRMs/SystemC_LRM_1666-2023.pdf; the secondary reference is .codex-src/systemc. Read the LRM first for the rule, then read the source to understand why the rule produces the behavior you see in a debugger.

The Four Pillars of Communication

  1. 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.
  2. Channel (sc_channel or sc_prim_channel): A module that implements the interface. It contains the actual state and logic.
  3. sc_port: An outward-facing connection on a module that requires an interface to function. Under the hood, sc_port is just a proxy object that internally holds an array of sc_interface* pointers.
  4. 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 internal sc_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 (like sc_signal). Under the hood, they hook directly into the Evaluate-Update paradigm. When you write to a signal, sc_prim_channel::request_update() pushes the this pointer into sc_simcontext::m_update_list. Later, during the Update Phase, the scheduler loops over this list and calls the purely virtual update() method. They cannot have structural hierarchy (child modules) or SC_THREADs.
  • Hierarchical Channels (sc_channel): Modules that implement an interface (as shown in the example above). They derive directly from sc_module and 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.

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:

  1. 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_interface implementation.
  2. 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"); }
  3. It resolves the final interface pointer and stores it directly inside the port's m_interface pointer 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