Events and Sensitivity in SystemC — How Processes Know When to Run
The previous article covered SC_THREAD vs SC_METHOD — what the two process types are and how they differ. But we skipped over a foundational question: how does a process know when to wake up in the first place?
The answer is events and sensitivity. These are the signalling mechanisms that drive every process in a SystemC simulation. Get them wrong — even by one line — and you get deadlocks, missed wakeups, or processes that fire at the wrong time. This article explains how they work from the ground up.
1. What Is an sc_event?
An sc_event is a pure notification — it has no value and no duration. It fires at a single instant in simulation time and then disappears. There is no way to read it or check it like a boolean. The only two things you can do with an sc_event are:
- Cause it — by calling
notify() - Wait for it — by calling
wait(event)ornext_trigger(event)
notify() is called, the event is silently lost — no error, no trace.
Declaring an event is straightforward:
sc_event e_ready; sc_event e_ack, e_error; // multiple on one line
2. Three Ways to Notify
The notify() method has three distinct forms, and choosing the wrong one is one of the most common sources of simulation bugs.
// 1. Immediate — fires right now, this delta cycle e_ready.notify(); // 2. Delta delay — fires after all current runnable processes finish e_ready.notify(SC_ZERO_TIME); // 3. Timed — fires at a future simulation time e_ready.notify(10, SC_NS); e_ready.notify(sc_time(5, SC_US));
Immediate notify()
Calling notify() with no arguments moves any processes currently waiting for this event into the runnable set immediately — within the same delta cycle. It's the fastest form but the most dangerous, for reasons we'll cover in Section 4.
Delta delay — notify(SC_ZERO_TIME)
SC_ZERO_TIME is simply sc_time(0, SC_SEC). This schedules the notification for the next delta cycle — after all currently runnable processes have finished executing. This is the safest general-purpose form and avoids most missed-event bugs.
Timed notify()
Schedules the event at a future simulation time. One important constraint: an sc_event can only have one outstanding scheduled notification at a time. If you call notify() multiple times, only the nearest scheduled time survives:
e_action.notify(10, SC_NS); e_action.notify(5, SC_NS); // this wins — 5 ns is nearer e_action.notify(15, SC_NS); // ignored — 5 ns still nearer
Cancelling a scheduled event
A pending timed or delta notification can be cancelled. Immediate notifications cannot — they happen the instant they are called.
e_action.cancel(); // cancels any pending notification
3. Dynamic Sensitivity — SC_THREAD and wait()
An SC_THREAD uses wait() to suspend itself and specify exactly what it is waiting for. When that condition is met, the kernel resumes the thread at the line immediately after the wait() call. This is called dynamic sensitivity — the sensitivity changes every time the process runs.
// Full wait() syntax for SC_THREAD wait(time); // suspend for a duration wait(10, SC_NS); // convenience form wait(event); // wait for one specific event wait(e1 | e2); // wake on either event wait(e1 & e2); // wake when both have fired wait(10, SC_NS, event); // whichever comes first: timeout or event wait(10, SC_NS, e1 | e2); // timeout or any of these events wait(); // use static sensitivity list (see Section 5)
When you use the OR form (e1 | e2), you cannot tell which event fired — sc_event has no value to read. If you need to know, use a flag set by the notifying process.
sc_event e_ack, e_bus_error; sc_time t_MAX(100, SC_NS); wait(t_MAX, e_ack | e_bus_error); if (sc_time_stamp() - start_time >= t_MAX) { // timeout — no ACK received } else { // either e_ack or e_bus_error fired }
4. The Missed Event Problem
This is one of the most misunderstood issues in SystemC, and it causes deadlocks that are nearly impossible to debug by inspection.
Consider three threads: A_thread fires an immediate event, while B_thread and C_thread are supposed to catch it.
SC_MODULE(missed_event_demo) {
SC_CTOR(missed_event_demo) {
SC_THREAD(A_thread);
SC_THREAD(B_thread);
SC_THREAD(C_thread);
}
sc_event e_go;
void A_thread() {
e_go.notify(); // immediate! fires right now
}
void B_thread() {
wait(e_go); // may never return
}
void C_thread() {
wait(e_go); // may never return
}
};
If the simulator happens to run A_thread first (which it is allowed to do — process ordering is not guaranteed), it fires e_go immediately. Since B_thread and C_thread haven't reached their wait() yet, they miss it. The event disappears. Both threads then reach wait(e_go) and block forever — a silent deadlock.
notify(), you are betting on ordering — and you will lose eventually.
The fix: use notify(SC_ZERO_TIME)
Delta-delayed notification fires only after all currently runnable processes have finished executing. By that time, B_thread and C_thread will have reached their wait() calls and be watching. No event gets missed.
void A_thread() { e_go.notify(SC_ZERO_TIME); // fires next delta — safe }
As a general rule: prefer notify(SC_ZERO_TIME) over notify() unless you have a specific reason to require immediate wake-up within the same delta cycle.
5. Dynamic Sensitivity — SC_METHOD and next_trigger()
SC_METHOD processes cannot call wait(). Instead, they use next_trigger() to tell the scheduler what should cause the next invocation. This is also dynamic sensitivity — it changes on every run of the method.
// Full next_trigger() syntax for SC_METHOD next_trigger(time); next_trigger(10, SC_NS); // convenience form next_trigger(event); next_trigger(e1 | e2); // any of these next_trigger(e1 & e2); // all of these next_trigger(10, SC_NS, event); // timeout or event next_trigger(); // restore static sensitivity
Each call to next_trigger() overrides the previous one. Only the last next_trigger() executed before the method returns takes effect.
next_trigger() or rely on static sensitivity. If neither exists, the method will never be called again — it silently stops running.
Here's a bus monitor that dynamically changes what it waits for based on state:
SC_MODULE(bus_monitor) {
sc_in<bool> clk, valid, ready;
bool m_waiting_for_ready = false;
sc_event e_valid_seen;
SC_CTOR(bus_monitor) {
SC_METHOD(monitor_method);
sensitive << clk.pos(); // static: trigger on rising clock
}
void monitor_method() {
if (!m_waiting_for_ready) {
if (valid.read()) {
m_waiting_for_ready = true;
e_valid_seen.notify(SC_ZERO_TIME);
next_trigger(ready.posedge_event()); // now wait for ready
}
// else: fall through, static sensitivity resumes next clock
} else {
m_waiting_for_ready = false;
next_trigger(); // back to static (clk.pos)
}
}
};
6. Static Sensitivity
Dynamic sensitivity requires you to re-specify the trigger on every execution. For processes that always react to the same set of signals — like RTL clock-edge logic or combinational monitors — that's repetitive. Static sensitivity lets you declare the sensitivity list once, in the constructor, and never touch it again.
SC_MODULE(edge_detector) {
sc_in<bool> clk, reset, data_valid;
SC_CTOR(edge_detector) {
SC_METHOD(detect);
sensitive << clk.pos() // rising clock edge
<< reset; // any change on reset
SC_THREAD(sequencer);
sensitive << data_valid.pos(); // thread uses static list via wait()
}
void detect() {
// runs on every clk rising edge or reset change
// no next_trigger() needed — static list auto-re-triggers
}
void sequencer() {
while (true) {
wait(); // suspends, resumes when data_valid rises
// do work...
}
}
};
For SC_THREAD, wait() with no arguments suspends the thread and uses the static sensitivity list as its wakeup condition. For SC_METHOD, simply not calling next_trigger() causes the static list to be used automatically.
dont_initialize()
By default, all processes are placed in the runnable set once at the start of simulation — before any events fire. For most processes this is fine. But for an attendant method that should only run when requested, an initialization run makes no sense (it would execute with no valid request).
Use dont_initialize() to suppress the initial run. It must follow the process registration and requires a static sensitivity list — otherwise there is nothing to trigger the process ever again.
SC_METHOD(attendant_method); sensitive << e_request; dont_initialize(); // skip the first automatic run
7. Static vs Dynamic — Side by Side
| Property | Static Sensitivity | Dynamic Sensitivity |
|---|---|---|
| Where set | Constructor (elaboration) | At runtime inside process |
| Can change | No — fixed for simulation | Yes — every invocation |
| SC_THREAD syntax | sensitive << e; … wait(); |
wait(event) |
| SC_METHOD syntax | sensitive << e; (no next_trigger needed) |
next_trigger(event) |
| Best for | RTL clock/reset, combinational logic, monitors | Protocol FSMs, conditional waits, state machines |
| Override | Can be overridden by next_trigger(event) |
Restored with next_trigger() |
8. Bonus: sc_event_queue
A standard sc_event can only hold one pending notification at a time — a nearer schedule always cancels the previous one. This means if events arrive faster than they are consumed, you lose them.
sc_event_queue solves this. It queues multiple scheduled events, even for the same simulation time, and delivers each one in a separate delta cycle evaluation.
sc_event_queue eq("eq"); eq.notify(10, SC_NS); // queued — not replaced eq.notify(10, SC_NS); // also queued — both will fire eq.notify(15, SC_NS); // also queued eq.cancel_all(); // cancel() replaced by cancel_all()
sc_event_queue has a measurable performance cost. Events affect a huge portion of simulation execution time. Use it only when you genuinely need to queue multiple firings — most models only need sc_event.
9. Decision Guide
| Situation | Use This |
|---|---|
| Process always reacts to the same signal | Static sensitivity (sensitive <<) |
| Process waits for different things each time | Dynamic: wait(event) or next_trigger(event) |
| Signalling to a process that may not be ready yet | notify(SC_ZERO_TIME) — never immediate |
| Protocol handshake with a timeout | wait(timeout, e_ack | e_error) |
| Method should not run at simulation start | Add dont_initialize() after registration |
| Multiple events at same time need separate handling | sc_event_queue |
| Method needs to change sensitivity mid-simulation | next_trigger(event) → next_trigger() to restore |
Summary
sc_eventis a pure notification — no value, no duration. You must be watching before it fires.- Three notification types: immediate (dangerous), delta delay (safe default), timed (future time).
- SC_THREAD uses
wait(event)for dynamic sensitivity;wait()for static. - SC_METHOD uses
next_trigger(event)for dynamic; omit it to fall back to static. - Static sensitivity is declared once in the constructor with
sensitive <<. - Immediate
notify()causes missed events when process order is unpredictable — prefernotify(SC_ZERO_TIME). - Use
dont_initialize()to suppress the automatic first run of a process. - Use
sc_event_queueonly when you need to queue multiple firings of the same event.