Chapter 0: Start Here

C++ Prerequisites

What you absolutely must know about C++ before attempting to learn SystemC.

How to Read This Lesson

Start here slowly. The goal is not to memorize APIs yet; it is to build the mental model you will reuse in every later SystemC design review.

C++ Prerequisites for SystemC

Before diving into SystemC, it is crucial to understand that SystemC is not a new language. It is entirely built as a C++ class library. If you are coming from Verilog or VHDL, you might be tempted to treat SystemC like a hardware description language. However, if you do not understand the underlying C++, you will quickly become frustrated when the compiler throws massive template errors.

This page outlines exactly what C++ concepts you must master before proceeding.

Source and LRM Trail

For this foundation lesson, keep three references close: Docs/LRMs/SystemC_LRM_1666-2023.pdf for portable semantics, .codex-src/systemc/src/sysc/kernel for kernel behavior, and .codex-src/systemc/src/sysc/datatypes for bit-accurate C++ types. When this lesson mentions a macro or type, the useful habit is to ask which C++ class the macro eventually creates.

1. Object-Oriented Programming (OOP)

SystemC modules (sc_module) are just C++ classes. You need to be deeply comfortable with:

  • Classes and Structs: Creating them, member variables, and member functions.
  • Constructors: SystemC relies heavily on constructors (SC_CTOR) to register processes and bind ports.
  • Inheritance: sc_module inherits from sc_object. You will frequently use public inheritance when defining custom interfaces.
  • Polymorphism and Virtual Functions: In TLM (Transaction Level Modeling), you will define purely virtual interfaces and implement them in your target modules.

2. Pointers and References

Hardware ports are essentially safe pointers to signals.

  • Pass-by-Reference (&): Passing large transaction payloads efficiently.
  • Pointers (*): Dynamically allocating modules, or passing pointers to arrays.
  • Smart Pointers: In advanced TLM and SCV (SystemC Verification Library), you will use scv_smart_ptr to handle memory automatically and avoid segmentation faults.

3. C++ Templates

SystemC ports, signals, and FIFOs are templated classes. When you write sc_in<bool>, you are instantiating an sc_in template for the boolean type.

You must understand:

  • How to pass basic types to templates (sc_signal<int>).
  • How to pass user-defined structs to templates (sc_signal<MyStruct>).
  • Operator Overloading: SystemC overloads the << operator for sensitivity lists and port binding, and the = operator for reading/writing signals. It is not bit-shifting; it is C++ operator overloading.

4. The Standard Template Library (STL)

You will not write your own linked lists. You will use the STL.

  • std::vector for dynamic arrays of ports or modules.
  • std::string for dynamic module naming.
  • std::cout and std::endl for simulation logging.

Summary

If any of these concepts are entirely foreign to you, please pause and read through a comprehensive C++ programming guide. SystemC is incredibly powerful, but its power comes from leveraging advanced C++ paradigms to mimic hardware concurrency.

Once you are comfortable with C++, proceed to the next tutorial to install the Accellera SystemC library.


Let's Connect This to the Standard and the Source

To get genuinely comfortable with SystemC, you must peek behind the curtain of its macros and template wrappers. SystemC is strictly governed by the IEEE 1666-2023 Language Reference Manual (LRM) and implemented via the Accellera Proof-of-Concept (PoC) source code. This section demystifies the C++ mechanisms that power the SystemC kernel.

The True Identity of SC_MODULE and SC_CTOR (IEEE 1666 Section 5.2)

When you define a module using the SC_MODULE macro, you are declaring a C++ struct that inherits from the core base class sc_core::sc_module.

In the Accellera source code (sysc/kernel/sc_module.h), SC_MODULE is defined as:

#define SC_MODULE(user_module_name) struct user_module_name : ::sc_core::sc_module

Because it expands to a struct, all members are public by default unless explicitly marked private or protected. This allows quick access to ports and signals from testbenches, though encapsulating internal state using class access modifiers is highly recommended.

Furthermore, sc_module inherits from sc_core::sc_object (IEEE 1666 Section 5.1). sc_object is the base class for almost every structural element in SystemC. It provides runtime identification, name hierarchies, and attributes.

The SC_CTOR(name) macro expands to define a constructor that takes an sc_core::sc_module_name argument:

#define SC_CTOR(user_module_name) \
    typedef user_module_name SC_CURRENT_USER_MODULE; \
    user_module_name( ::sc_core::sc_module_name )

This forces the constructor to accept an sc_module_name object. When sc_module_name is constructed, it pushes the object's name onto the hierarchy stack managed by the global sc_simcontext (sysc/kernel/sc_simcontext.cpp). This string-based object hierarchy allows the kernel to traverse the tree, find objects by name using sc_find_object(), and report hierarchical paths via name().

The Global Simulation Context (sc_simcontext)

Behind every SystemC simulation runs the sc_simcontext singleton. According to IEEE 1666 Section 4, elaboration and simulation execution are managed globally. The sc_get_curr_simcontext() function retrieves this global state manager.

The sc_simcontext maintains:

  1. The Object Hierarchy Stack: To build the parent-child relationships of modules, ports, and signals during elaboration.
  2. The Runqueue (Scheduler): Maintaining the sets of runnable threads and methods (sysc/kernel/sc_runnable.cpp).
  3. The Time Wheel: Handling delayed event notifications and time progression.

When you dynamically allocate a module using new, the underlying C++ calls the sc_module constructor, which interacts with sc_simcontext to register the new module into the current hierarchy context.

Virtual Inheritance and Interfaces (IEEE 1666 Section 5.11)

SystemC relies deeply on C++ multiple inheritance and pure virtual functions to implement Interfaces and Channels. An interface in SystemC (sc_interface) is a C++ class containing pure virtual methods. For instance, the sc_signal_in_if<T> interface declares the read() and get_event() methods:

template <class T>
class sc_signal_in_if : virtual public sc_interface {
public:
    virtual const T& read() const = 0;
    virtual const sc_event& value_changed_event() const = 0;
    // ...
};

Channels like sc_signal<T> inherit from these interfaces and provide the concrete implementations. When a port sc_in<T> is bound to a signal, the port simply stores a C++ interface pointer (sc_interface*) to the channel. Method calls on the port are routed through virtual dispatch (the C++ vtable) to the channel's implementation.

C++ Templates and User-Defined Types (IEEE 1666 Section 6.11)

SystemC heavily leverages C++ templates for structural reusability. sc_signal<T>, sc_in<T>, and sc_fifo<T> are all templates. When creating custom data types (structs or classes) to pass through a templated signal (sc_signal<MyType>), the LRM imposes strict C++ operator overloading requirements on your type.

If you inspect sysc/communication/sc_signal.cpp, you will see that when a signal updates its value, it compares the new value with the old value to determine if an event should be triggered. Therefore, your custom type T MUST define:

  1. bool operator==(const T&, const T&): Used by sc_signal to check if the value actually changed. If it returns false, no value_changed_event is fired.
  2. T& operator=(const T&): To copy the value into the signal's internal buffers.
  3. inline friend void sc_trace(sc_trace_file*, const T&, const std::string&): If you intend to dump the signal to a VCD file.
  4. ostream& operator<<(ostream&, const T&): For printing and logging purposes.

If these are missing, the C++ compiler will generate deeply nested template error messages that often confuse beginners.

sc_vector and Dynamic Allocation

While raw pointers and arrays (sc_module* my_modules[10];) are valid C++, IEEE 1666-2011 introduced sc_vector<T> to manage arrays of SystemC objects. sc_vector behaves much like std::vector, but it handles the complex name generation required by sc_simcontext automatically. It ensures that elements inside sc_vector get valid hierarchical SystemC names like my_vector_0, my_vector_1, etc.

C++ Exception Handling in the Kernel

The Accellera kernel uses C++ exceptions to manage errors and process aborts. When a fatal error occurs, or when you call SC_REPORT_FATAL, the sc_report_handler evaluates the action configuration and potentially throws an sc_core::sc_report object (which inherits from std::exception).

Furthermore, methods like kill() and reset() on threads use C++ exception unwinding. The kernel throws a special internal exception (e.g., sc_unwind_exception) into the running process thread, forcing it to instantly unwind its call stack and terminate. This requires careful use of RAII (Resource Acquisition Is Initialization) to avoid memory leaks when threads are abruptly killed by the scheduler.

Deep Dive: Accellera Source for sc_signal and update()

The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.

The write() Implementation

When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:

template<class T>
inline void sc_signal<T>::write(const T& value_) {
    if( !(m_new_val == value_) ) {
        m_new_val = value_;
        this->request_update(); // Inherited from sc_prim_channel
    }
}

The request_update() call appends the channel to sc_simcontext::m_update_list.

The update() Phase

After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:

template<class T>
inline void sc_signal<T>::update() {
    if( !(m_new_val == m_cur_val) ) {
        m_cur_val = m_new_val;
        m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
    }
}

This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.

Comments and Corrections