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 callwait().SC_THREAD: can suspend withwait()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 supportwait(). Under the hood, the Accellera kernel uses a coroutine library. On Linux/Windows, it typically uses QuickThreads (src/sysc/qt/) or POSIX fibers. Whenwait()is called, the coroutine context is saved, and execution yields back to the SystemC scheduler. When ansc_event::notify()is called, the kernel pushes the event intosc_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 them_runnablelist.
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