Chapter 11: Advanced Core Semantics

Events and Notification Semantics

Immediate, delta, and timed event notifications, cancellation, event lists, and how the kernel schedules waiting processes.

Events and Notification Semantics

An sc_core::sc_event is the fundamental synchronization primitive in SystemC. It is not a stored boolean flag or a queue. It is a scheduling object used by the kernel to make waiting processes runnable at the correct time.

This distinction is heavily emphasized in the IEEE 1666 LRM: Events have no memory. If an event is notified before a process explicitly waits on it, the process does not magically remember that notification. The notification is lost.

The Three Forms of Notification

The SystemC LRM models three distinct notification semantics using the overloaded notify() method:

  1. Immediate Notification: ev.notify() Wakes up sensitive processes in the current evaluation phase. Processes that are suspended on this event are moved to the runnable queue immediately.

  2. Delta Notification: ev.notify(sc_core::SC_ZERO_TIME) Schedules the event for the next delta cycle at the current simulation time. This is strictly required to model zero-delay hardware semantics, avoiding race conditions and combinational loops.

  3. Timed Notification: ev.notify(10, sc_core::SC_NS) Schedules the event to trigger at a specific future simulation time.

The Notification Override Rule

A core LRM rule specifies what happens when an event is notified multiple times before it actually triggers:

  • An immediate notification cancels any pending delta or timed notifications for that event.
  • A delta notification cancels any pending timed notifications.
  • If multiple timed notifications are requested, the one that occurs earliest takes precedence (earlier notifications override later ones).

Event Lists (AND/OR)

Processes can dynamically wait on combinations of events using event lists:

wait(rx_event | timeout_event); // OR list: Resumes when ANY event occurs
wait(a_event & b_event);        // AND list: Resumes when ALL events have occurred

An AND list resumes only after all member events have occurred since the wait was armed. If a_event fires, the kernel remembers it for this wait expression, and only resumes the thread once b_event also fires.

Cancellation

Pending delta and timed notifications can be actively canceled before they fire:

retry_event.cancel();

Cancellation is useful for modeling hardware watchdog timers, retries, and interrupt coalescing. (Immediate notifications cannot be canceled because they take effect instantly).

Complete Example: Notification Semantics and Time

The following complete, compilable sc_main demonstrates immediate vs. delta notifications, the lack of memory in events, and how to safely pair events with state variables.

#include <systemc>
#include <iostream>
#include <queue>
 
SC_MODULE(EventDemo) {
    sc_core::sc_event data_ready_evt;
    std::queue<int>   fifo;
 
    SC_CTOR(EventDemo) {
        SC_THREAD(producer);
        SC_THREAD(consumer);
    }
 
    void producer() {
        wait(10, sc_core::SC_NS);
        
        // Push data to state
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Pushing 42\n";
        fifo.push(42);
 
        // DELTA notification: Allows consumer to wake up safely in the next 
        // delta cycle, preventing read/write race conditions.
        data_ready_evt.notify(sc_core::SC_ZERO_TIME);
 
        wait(10, sc_core::SC_NS);
 
        // Immediate notification demonstration.
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Pushing 99\n";
        fifo.push(99);
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Immediate Notify...\n";
        data_ready_evt.notify();
        std::cout << "@ " << sc_core::sc_time_stamp() << " [Producer] Finished immediate notify.\n";
    }
 
    void consumer() {
        while (true) {
            // Wait for the event
            wait(data_ready_evt);
            
            // Events have no memory! Always check the state (the FIFO) 
            // after waking up to see what actually happened.
            while (!fifo.empty()) {
                int val = fifo.front();
                fifo.pop();
                std::cout << "@ " << sc_core::sc_time_stamp() 
                          << " [Consumer] Read " << val << " from FIFO.\n";
            }
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    EventDemo demo("demo");
 
    std::cout << "Starting simulation...\n";
    sc_core::sc_start(50, sc_core::SC_NS);
    std::cout << "Simulation finished.\n";
 
    return 0;
}

Explanation of the Execution

When run, the output looks like this:

Starting simulation...
@ 10 ns [Producer] Pushing 42
@ 10 ns [Consumer] Read 42 from FIFO.
@ 20 ns [Producer] Pushing 99
@ 20 ns [Producer] Immediate Notify...
@ 20 ns [Consumer] Read 99 from FIFO.
@ 20 ns [Producer] Finished immediate notify.
Simulation finished.

Notice the crucial difference at 20ns: With immediate notification, the consumer is awakened and executed immediately, preempting the producer. The producer's "Finished immediate notify." prints after the consumer has finished reading! If a delta notification was used instead, the producer would finish its thread execution entirely for that cycle before the consumer ran.

Always prefer delta notifications when communicating between processes to ensure deterministic evaluation order!

Comments and Corrections