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:

Core Rule To observe an event, a process must already be waiting for it before it fires. If no process is watching when 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.

Timeout pattern The timeout form is extremely useful in protocol verification. If the ACK doesn't arrive within the budget, you take the error path — without any external timer infrastructure.
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.

The Rule Process execution order is not guaranteed. Never write code that depends on a specific process running first. If you use immediate 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.

Critical Every execution path through an SC_METHOD must either call 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()
Performance Note 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