LRM Bridge: Core Classes and Process Control
sc_module, sc_module_name, sc_spawn, sc_process_handle, reset, sensitivity, wait, next_trigger, and process control.
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.
The SystemC core classes define how structural models are instantiated and how behavioral processes become schedulable and controllable. Here we dive into the Accellera kernel source code to show how the hierarchy and scheduler control these primitives.
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.
sc_module and sc_module_name
sc_module gives a model its structural hierarchy. The class sc_module_name must be the first parameter in the constructor to support string-based naming during elaboration.
Under the Hood:
sc_module derives from sc_object. When you instantiate an sc_module_name variable, its constructor pushes itself onto a global stack managed by sc_simcontext. When sc_module is constructed immediately after, it pops that name off the stack and registers the instance as a child of the currently active sc_module scope. This creates the hierarchical object tree sc_simcontext::m_object_manager, which is the backbone of hierarchical trace names, CCI paths, and UVM-SystemC factory lookups.
Process Macros
The IEEE 1666 standard defines three main process macros.
Under the Hood: They all expand into standard C++ member functions registered via a helper class (e.g., sc_process_b hierarchy).
SC_METHOD: Creates ansc_method_processwhich executes to completion viasemantics()without suspension.SC_THREAD: Creates ansc_thread_processwhich allocates a coroutine stack (viaqt_allocateormakecontext) and can suspend usingwait().SC_CTHREAD: A specialized thread (sc_cthread_process) sensitive to a single clock edge, often used for High-Level Synthesis (HLS).
Complete Process Control & Sensitivity Example
This end-to-end example demonstrates static sensitivity, dynamic sensitivity (next_trigger), reset signaling, and dynamic process control via sc_process_handle.
#include <systemc>
SC_MODULE(ProcessControlDemo) {
sc_core::sc_in<bool> clk;
sc_core::sc_signal<bool> reset{"reset"};
sc_core::sc_event dynamic_event;
// Process handle to control a thread externally
sc_core::sc_process_handle thread_handle;
SC_CTOR(ProcessControlDemo) {
// 1. SC_METHOD with dynamic sensitivity
SC_METHOD(one_shot_method);
dont_initialize();
// 2. SC_THREAD with static sensitivity and asynchronous reset
SC_THREAD(worker_thread);
sensitive << clk.pos();
async_reset_signal_is(reset, true); // Reset is active-high
// Capture the handle of the most recently created process (worker_thread)
thread_handle = sc_core::sc_get_current_process_handle();
// 3. SC_THREAD to control the worker and trigger events
SC_THREAD(controller_thread);
}
void one_shot_method() {
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Method] Fired due to dynamic_event." << std::endl;
// Dynamic sensitivity: trigger again ONLY on the next occurrence
next_trigger(dynamic_event);
}
void worker_thread() {
while(true) {
// Check for reset condition
if (sc_core::sc_process_handle::is_unwinding()) {
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Worker] Thread reset triggered!" << std::endl;
// Perform reset logic here (clear queues, reset state machines)
wait(); // Suspend and wait for reset to clear
}
wait(); // Wait for static sensitivity (clk.pos)
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Worker] Doing work..." << std::endl;
}
}
void controller_thread() {
wait(15, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Triggering method." << std::endl;
dynamic_event.notify();
wait(20, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Asserting reset." << std::endl;
reset.write(true);
wait(20, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] De-asserting reset." << std::endl;
reset.write(false);
wait(20, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Suspending worker via handle." << std::endl;
thread_handle.suspend();
wait(20, sc_core::SC_NS);
std::cout << "@" << sc_core::sc_time_stamp() << " [Controller] Resuming worker via handle." << std::endl;
thread_handle.resume();
}
};
int sc_main(int argc, char* argv[]) {
sc_core::sc_clock clk("clk", 10, sc_core::SC_NS);
ProcessControlDemo demo("demo");
demo.clk(clk);
sc_core::sc_start(120, sc_core::SC_NS);
return 0;
}Sensitivity
Static sensitivity connects a process to events before simulation starts using sensitive <<. The kernel maintains a static list of events (sc_event_list) within sc_process_b.
Dynamic sensitivity is expressed by wait() in threads or next_trigger() in methods, which temporarily overrides the static list for the next scheduler wakeup by placing an event in m_trigger_event.
Use static sensitivity for stable, physical hardware structures (like a clock pin on a D-Flip-Flop). Use dynamic sensitivity when modeling software, TLM state machines, or protocols where the next wakeup event heavily depends on the current runtime state.
Process Handles
sc_process_handle gives programmatic access to a process object (which wraps sc_process_b*). Depending on the simulation phase and process state, a model can suspend(), resume(), disable(), enable(), kill(), or sync with a process.
Under the Hood: Calling suspend() sets m_state = ps_suspended on the underlying sc_process_b and removes the process from the m_runnable scheduler queue if it was pending.
Reset
SystemC provides standard reset semantics via reset_signal_is (synchronous) and async_reset_signal_is (asynchronous).
Under the Hood: When an asynchronous reset signal asserts, the scheduler calls trigger_reset() on the sc_process_b. If the thread is executing, it literally throws a C++ exception: throw sc_unwind_exception(). The process thread stack unwinds until it hits the kernel's catch block, re-evaluates is_unwinding(), and resets its execution pointer to the start of the while loop.
sc_spawn
sc_core::sc_spawn allows dynamic process creation at runtime.
Under the Hood: sc_spawn creates an sc_spawn_options object, packages the functor/lambda, and instantiates an sc_thread_process or sc_method_process dynamically using sc_simcontext::create_thread_process(). While permitted by the LRM, dynamically spawning threads during simulation breaks static elaboration, making debugging and static analysis tools (like structural visualizers) fail. Use sc_spawn predominantly for testbenches and UVM sequence generation, not for hardware RTL modeling.
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