Chapter 2: Core Modeling

Modules, Hierarchy, and Elaboration

How SC_MODULE, constructors, object names, and hierarchy registration shape the simulation.

How to Read This Lesson

Keep one question in mind: when does this code run as ordinary C++, and when is the simulation kernel in charge? That split explains most beginner bugs.

SC_MODULE is a convenience macro around a C++ class derived from sc_module. It exists because SystemC needs more than C++ object construction. The kernel also needs names, hierarchy, and process registration.

#include <systemc>
using namespace sc_core;
 
SC_MODULE(Producer) {
  sc_out<int> out{"out"};
 
  SC_CTOR(Producer) {
    SC_THREAD(run);
  }
 
  void run() {
    for (int i = 0; i != 4; ++i) {
      out.write(i);
      wait(10, SC_NS);
    }
  }
};
 
SC_MODULE(Consumer) {
  sc_in<int> in{"in"};
 
  SC_CTOR(Consumer) {
    SC_METHOD(process);
    sensitive << in;
    dont_initialize();
  }
 
  void process() {
    std::cout << "Consumer received: " << in.read() 
              << " at " << sc_time_stamp() << std::endl;
  }
};
 
SC_MODULE(Top) {
  sc_signal<int> data{"data"};
  Producer producer{"producer"};
  Consumer consumer{"consumer"};
 
  SC_CTOR(Top) {
    producer.out(data);
    consumer.in(data);
  }
};
 
int sc_main(int, char*[]) {
  Top top{"top"};
  sc_start(100, SC_NS);
  return 0;
}

The macro form is popular because it is compact. You can also write explicit C++ classes derived from sc_module, which is useful when templates or inheritance become more important than brevity.

Source and LRM Trail

Read this topic against Docs/LRMs/SystemC_LRM_1666-2023.pdf for process, event, time, reset, and report semantics. In source, follow .codex-src/systemc/src/sysc/kernel/sc_simcontext.cpp, sc_process.*, sc_event.*, sc_wait.*, sc_reset.*, and .codex-src/systemc/src/sysc/utils/sc_report_handler.cpp.

Names Matter

Every SystemC object has a name in the hierarchy. These names show up in reports, traces, and errors. A module created as Producer producer{"producer"} becomes part of the object tree. A port named out becomes producer.out.

Good names are not cosmetic. They are your debugging map.

Elaboration Is the Build Step Inside the Executable

During elaboration, the kernel discovers the object hierarchy and checks structural rules. This is where port binding errors often appear. For example, an unbound port may not fail at C++ compile time, because the compiler only sees objects and function calls. The SystemC kernel detects whether the model graph is valid.

Child Modules

Hierarchy is just composition:

Top modules construct their children and manage connections. (Note: The definitions of Producer, Consumer, and Top are already included in the full example above.)

Construct child modules before binding them. This style keeps topology in the parent constructor, where readers expect to find it.

Source-Code Angle

In the reference implementation, module construction participates in a global simulation context. The context keeps track of the current hierarchy scope while constructors run. That is how a child object can learn where it belongs without you manually passing a full hierarchical path into every port, channel, and module.

The big idea: SystemC uses ordinary C++ construction, but overlays a hierarchy-tracking discipline on top of it.


Let's Connect This to the Standard and the Source

The process of building the hierarchical tree of modules is called Elaboration. To write advanced SystemC, especially code that uses dynamic C++ generation, templates, or complex module hierarchies, you must understand the rules defined in IEEE 1666-2023 Section 4.1: Elaboration.

The SC_HAS_PROCESS Macro (IEEE 1666 Section 5.2)

The SC_CTOR macro used above forces your constructor to take exactly one argument: sc_core::sc_module_name. What if you want to pass configuration arguments to your module? What if you are using C++ templates? SC_CTOR breaks down in these scenarios.

The LRM provides SC_HAS_PROCESS as the robust alternative. When you write an explicit class inheriting from sc_module, you must invoke SC_HAS_PROCESS inside the class definition, which tells the compiler to set up the necessary typedefs for process registration macros (SC_METHOD, SC_THREAD).

class ConfigurableConsumer : public sc_core::sc_module {
public:
    sc_in<int> in{"in"};
    int threshold;
 
    SC_HAS_PROCESS(ConfigurableConsumer);
 
    ConfigurableConsumer(sc_core::sc_module_name name, int thresh)
        : sc_core::sc_module(name), threshold(thresh) 
    {
        SC_METHOD(process);
        sensitive << in;
        dont_initialize();
    }
 
    void process() { /* ... */ }
};

By explicitly deriving from sc_module and taking sc_module_name in the constructor, you regain complete C++ freedom to pass any number of parameters. Notice that you must pass the name argument up to the sc_module base constructor sc_core::sc_module(name).

Elaboration Callbacks (IEEE 1666 Section 4.4)

The LRM specifies a rigid lifecycle for all sc_module, sc_port, sc_export, and sc_prim_channel objects. You can override four virtual functions on sc_module to execute code at precise moments during the simulation lifecycle.

In sysc/kernel/sc_simcontext.cpp, when sc_start() is called, the kernel halts elaboration and transitions into simulation. Before time actually advances, the kernel invokes these callbacks globally across the entire object hierarchy:

  1. virtual void before_end_of_elaboration();

    • Called from the top-level modules down to the leaf modules.
    • You are still allowed to instantiate sub-modules and ports here. This is extremely useful for modules that need to dynamically allocate an array of ports based on configuration parameters discovered during early elaboration.
  2. virtual void end_of_elaboration();

    • Called from the top-level modules down to the leaf modules.
    • At this stage, the hierarchy is frozen. You cannot instantiate new modules, ports, or processes.
    • However, port bindings are complete. If you need to inspect what a port is bound to (e.g., getting the size of a multi-port using my_port.size()), this is the first safe place to do so. Doing it inside the constructor will fail because bindings happen after construction.
  3. virtual void start_of_simulation();

    • Called from the leaf modules up to the top-level modules (bottom-up).
    • This happens right before the Initialization Phase (Time 0). It is often used to open VCD trace files or initialize debug logs.
  4. virtual void end_of_simulation();

    • Called when sc_stop() is invoked or the sc_start() duration expires. Used to flush files, print summary reports, or release memory.

The Port Binding Registry and Deferred Binding

Why can't you query a port's binding inside the module constructor?

In sysc/communication/sc_port.cpp, port binding is deferred. When you call producer.out(data), the sc_port object simply records the pointer to the data signal in an internal list. It does not immediately resolve the hierarchical connections.

When sc_start() is executed, the kernel calls complete_binding() on the sc_simcontext. The kernel queries the sc_port_registry and walks the binding graph. It resolves port-to-port bindings down to the final port-to-interface binding. If a port traverses multiple layers of hierarchy, the kernel optimizes this by storing a direct pointer to the underlying interface in the top-level port, eliminating runtime overhead.

If a port is unbound, the kernel checks the sc_port_policy. If the policy requires binding, it throws a fatal sc_report exception right here, terminating the program before simulation even starts.

Prohibition on Dynamic Elaboration (IEEE 1666 Section 4.1.3)

A common mistake made by software engineers transitioning to SystemC is attempting to dynamically create modules or processes inside an SC_THREAD while the simulation is running (e.g., trying to spawn a new Consumer object dynamically).

The LRM strictly prohibits this. The structural hierarchy (modules, ports, channels) and static processes (SC_METHOD, SC_THREAD, SC_CTHREAD) can only be created during Elaboration. Once end_of_elaboration() finishes, the kernel optimizes its scheduling data structures (the sc_runnable queues). Attempting to call sc_module constructors during simulation execution will trigger a fatal SC_REPORT_FATAL exception from the Accellera kernel, explicitly stating "illegal action during simulation".

If dynamic concurrency is needed, you must use Dynamic Processes (sc_spawn), which are covered in the advanced core chapters (IEEE 1666 Section 5.5).

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