Introduction to SystemC
What SystemC is, where it fits, and the mental model behind C++ hardware simulation.
How to Read This Lesson
Read this like a conversation between normal C++ and the SystemC kernel. Whenever something looks like magic, ask: what C++ object did that macro or constructor register?
SystemC is a C++ class library and simulation kernel for modeling systems whose behavior is naturally concurrent: processors, buses, accelerators, interconnects, memories, peripherals, firmware-visible registers, and virtual platforms.
It is not a replacement syntax for Verilog or VHDL. It is C++ used with a hardware-oriented library. The library gives you modules, ports, signals, events, time, processes, and transaction-level modeling. Your compiler still sees C++, but the SystemC kernel sees a network of objects that can be elaborated, scheduled, and simulated.
Source and LRM Trail
Portable behavior comes from Docs/LRMs/SystemC_LRM_1666-2023.pdf, especially the clauses around modules, hierarchy, elaboration, and simulation startup. The Accellera implementation path to inspect is .codex-src/systemc/src/sysc/kernel: start with sc_module, sc_object, sc_module_name, sc_simcontext, and the process classes.
Why SystemC Exists
Hardware teams often need answers before RTL exists:
- Does this architecture have enough memory bandwidth?
- Can firmware boot before the chip is built?
- Which DMA shape gives the best latency?
- How much timing detail is needed for a performance question?
- Can a testbench drive a model at transaction level before signal-level detail is ready?
SystemC fills that space by letting you model at different abstraction levels. You can write a cycle-accurate block, a loosely timed bus model, or a fast functional model in the same language.
The Core Mental Model
A SystemC executable has two lives. First, normal C++ constructs objects. Then the SystemC kernel elaborates those objects into a simulation hierarchy and runs registered processes.
#include <systemc>
using namespace sc_core;
SC_MODULE(Hello) {
SC_CTOR(Hello) {
SC_METHOD(say_hello);
}
void say_hello() {
std::cout << "hello at " << sc_time_stamp() << "\n";
}
};
int sc_main(int, char*[]) {
Hello top{"top"};
sc_start();
return 0;
}The important part is not the greeting. It is the registration. SC_METHOD(say_hello) tells the kernel that a member function is a process. sc_start() hands control to the scheduler. From that point on, time, events, and process readiness decide what runs.
SystemC Is Useful Because It Is Layered
At the low level, you can model signals, clocks, and sensitivity like RTL. At the system level, you can model a memory transaction as a function call with a delay. Between those extremes, you can decide how much timing detail is worth paying for.
That is the design tradeoff this site keeps returning to: use the simplest model that answers the engineering question, then add detail where the question demands it.
What This Course Covers
This site is organized like a long-form tutorial:
- C++ setup and the shape of a SystemC program
- Modules, hierarchy, constructors, and elaboration
- Processes, sensitivity, events, waits, and delta cycles
- Ports, interfaces, exports, channels, and binding
- Signals, resolved signals, clocks, and writer policies
- TLM-2.0 payloads, sockets, timing, and protocol phases
- Source-code reading: scheduler, signals, ports, exports, sockets, and process control
- Practical patterns for virtual platforms and deployable documentation
The source-code chapters point to the official Accellera reference implementation at github.com/accellera-official/systemc. You do not need to memorize every private member. The goal is to recognize the architecture behind the public API.
Let's Connect This to the Standard and the Source
To understand this properly what SystemC is, we must look at how the IEEE 1666-2023 Language Reference Manual (LRM) defines its architecture and how the Accellera kernel implements it. SystemC is conceptually split into two phases: Elaboration and Simulation Execution.
The sc_main Wrapper and sc_elab_and_sim (IEEE 1666 Section 4.3)
Why do you write int sc_main(int argc, char* argv[]) instead of a standard main?
According to the LRM, the SystemC application entry point is sc_main. In the Accellera kernel, the actual standard int main(int argc, char* argv[]) is pre-compiled into the systemc library (specifically in sysc/kernel/sc_main_main.cpp).
When you execute your program, the standard C++ main runs first and immediately calls sc_core::sc_elab_and_sim(). The purpose of sc_elab_and_sim() is to bootstrap the simulation environment:
- It initializes the
sc_simcontextsingleton. - It processes command-line arguments to handle SystemC-specific flags.
- It sets up top-level exception handlers (catching
std::exceptionandsc_report). - It calls the user-provided
sc_main().
If you prefer to provide your own main() (for instance, when integrating SystemC into a larger C++ software framework like Google Test or an external simulator), you can compile the library without its main or invoke the sc_elab_and_sim routine directly, though bypassing the official wrapper requires careful manual initialization to avoid violating LRM semantics.
Elaboration Phase (IEEE 1666 Section 4.1)
Elaboration is the phase where the structural hierarchy of the hardware model is built. The LRM strictly defines that elaboration consists of the execution of the C++ constructors for the modules, ports, channels, and other SystemC objects.
During Elaboration, the sc_simcontext (sysc/kernel/sc_simcontext.cpp) maintains an active pointer to the currently constructing module. When you instantiate an sc_module and invoke its SC_CTOR:
- The kernel pushes the object's string name onto the hierarchical naming stack.
- The processes (like
SC_METHODandSC_THREAD) are registered with thesc_simcontext. The kernel stores C++ member function pointers to these processes. - Ports are instantiated and binding rules (
port(channel)orport.bind(channel)) are queued. Note that complete binding resolution does not happen immediately upon thebind()call; it is deferred until the end of elaboration.
The Simulation Execution Phase and sc_start (IEEE 1666 Section 4.2)
When sc_main invokes sc_start(), elaboration ends and the simulation engine takes over. The LRM specifies exactly what happens during the transition:
- End of Elaboration Callbacks: The kernel iterates over every
sc_objectin the hierarchy and invokesend_of_elaboration(). This is your last chance to dynamically allocate internal buffers or check if all ports are bound. - Port Binding Resolution: The kernel resolves all port-to-interface and port-to-export bindings. If an
sc_portis left unbound and does not have a default interface, the kernel throws a fatalsc_reportexception (sysc/communication/sc_port.cpp). - Initialization Phase: The scheduler executes every runnable process (unless disabled by
dont_initialize()). This guarantees that all signals reach a stable initial state. - The Event Loop (Evaluate and Update): The scheduler enters the main simulation loop, driven by the Delta Cycle and time progression.
The Delta Cycle and Concurrency (IEEE 1666 Section 4.2.1.2)
SystemC mimics parallel hardware execution using a uniprocessor C++ application. It achieves this via cooperative multitasking and the Delta Cycle (conceptually identical to VHDL/Verilog delta cycles).
In sysc/kernel/sc_simcontext.cpp, the event loop is structured as:
- Evaluate Phase: Run all processes in the "runnable" queue one by one until the queue is empty. Processes read current values and schedule updates (e.g.,
my_sig.write(1)). - Update Phase: If any channels (like
sc_signal) requested an update, the kernel loops over them and invokes their virtualupdate()methods. The signal changes its internal value and triggers itsvalue_changed_event. - Delta Notification Phase: The triggered events wake up any processes sensitive to them, adding them to the runnable queue. If the queue is not empty, a new Delta Cycle begins (go back to step 1) without advancing physical simulation time.
- Time Advancement: If the runnable queue is completely empty, and there are no more pending delta updates, the scheduler looks at the Time Wheel. It advances the simulation time (
sc_time_stamp()) to the nearest pending timed event and wakes up the corresponding processes.
This careful separation between reading values (Evaluate) and writing values (Update) is what makes SystemC deterministic and prevents race conditions, fulfilling its role as a hardware description and simulation language.
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:
- 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_interfaceimplementation. - 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"); } - It resolves the final interface pointer and stores it directly inside the port's
m_interfacepointer 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