Chapter 2: Core Modeling

Processes, Events, and Time

SC_METHOD, SC_THREAD, sensitivity, wait(), sc_event, and the meaning of delta cycles.

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.

Processes are the executable behavior inside a SystemC model. Modules provide structure. Channels provide communication. Processes provide activity.

SystemC has two process styles you will use constantly:

  • SC_METHOD: runs to completion and cannot call wait().
  • SC_THREAD: can suspend with wait() and resume later.

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.

SC_METHOD

Use SC_METHOD for combinational behavior or small reactions to events:

#include <systemc>
using namespace sc_core;
 
SC_MODULE(AndGate) {
  sc_in<bool> a{"a"};
  sc_in<bool> b{"b"};
  sc_out<bool> y{"y"};
 
  void comb() {
    y.write(a.read() && b.read());
  }
 
  SC_CTOR(AndGate) {
    SC_METHOD(comb);
    sensitive << a << b;
  }
};
 
int sc_main(int, char*[]) {
  sc_signal<bool> sig_a, sig_b, sig_y;
  AndGate and_gate("and_gate");
  and_gate.a(sig_a);
  and_gate.b(sig_b);
  and_gate.y(sig_y);
 
  sc_start(1, SC_NS);
  return 0;
}

The method runs when an event in its sensitivity list occurs. It should finish quickly because it cannot yield.

SC_THREAD

Use SC_THREAD when behavior has an internal timeline:

#include <systemc>
using namespace sc_core;
 
SC_MODULE(Timer) {
  sc_event done;
 
  SC_CTOR(Timer) {
    SC_THREAD(run);
  }
 
  void run() {
    wait(100, SC_NS);
    done.notify();
    std::cout << "Timer done at " << sc_time_stamp() << std::endl;
  }
};
 
int sc_main(int, char*[]) {
  Timer timer("timer");
  sc_start(200, SC_NS);
  return 0;
}

The call to wait() suspends the thread process. The simulation kernel saves enough process state to resume it when the wait condition is satisfied.

Events Do Not Store History

An sc_event is not a queue of messages. It is a notification mechanism. If nobody is waiting when an immediate event is notified, the event is missed.

That makes this pattern important:

#include <systemc>
using namespace sc_core;
 
SC_MODULE(Consumer) {
  sc_event producer_done;
 
  SC_CTOR(Consumer) {
    SC_THREAD(consumer_thread);
  }
 
  void consumer_thread() {
    while (true) {
      wait(producer_done);
      consume_result();
    }
  }
 
  void consume_result() {
    std::cout << "Result consumed at " << sc_time_stamp() << std::endl;
  }
};
 
int sc_main(int, char*[]) {
  Consumer cons("cons");
  cons.producer_done.notify(10, SC_NS);
  sc_start(20, SC_NS);
  return 0;
}

The process arms the wait first, then reacts.

Delta Cycles

A delta cycle is a zero-time scheduling step. It lets the kernel settle chains of events without advancing simulation time. Signal writes use this idea: a process writes a new value, the channel schedules an update, and dependent processes wake in a later delta cycle.

Delta cycles are why SystemC can avoid many order-dependent bugs. Processes can run in a deterministic simulation order while still modeling hardware-like simultaneous updates.

Practical Debug Rule

When behavior looks one step late, ask which phase you are observing:

  • Did a method write a signal but the update has not happened yet?
  • Did an event notify in the same delta or the next delta?
  • Is a thread waiting on a value change or on a timed delay?

Most SystemC timing surprises become ordinary once you separate time advancement from delta-cycle settling.

Under the Hood: sc_process, sc_event, and QuickThreads

When you register a process using SC_METHOD or SC_THREAD, SystemC allocates a process object inheriting from sc_process_b (defined in sysc/kernel/sc_process.h).

  • sc_method_process: A simple C++ function pointer. The scheduler calls it, and it must run to completion.
  • sc_thread_process: Requires its own execution stack to support wait(). Under the hood, the Accellera kernel uses a coroutine library. On Linux/Windows, it typically uses QuickThreads (src/sysc/qt/) or POSIX fibers. When wait() is called, the coroutine context is saved, and execution yields back to the SystemC scheduler. When an sc_event::notify() is called, the kernel pushes the event into sc_simcontext::m_event_list. At the end of the delta cycle, the scheduler wakes up all processes statically or dynamically sensitive to that event by moving them into the m_runnable list.

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