Chapter 3: Communication

Signals, Clocks, and Primitive Channels

How sc_signal stores values, delays updates, notifies readers, and models hardware-like behavior.

How to Read This Lesson

Think of this chapter as wiring discipline. Ports, exports, interfaces, and channels are not decorative; they are how the model states its contract before time starts moving.

sc_signal<T> is the everyday channel for value communication. It implements signal interfaces, stores a current value, accepts writes, and notifies readers when the value changes.

Source and LRM Trail

The standard contract lives in Docs/LRMs/SystemC_LRM_1666-2023.pdf around interfaces, ports, exports, primitive channels, hierarchical channels, and predefined channels. The implementation trail is .codex-src/systemc/src/sysc/communication: sc_port, sc_export, sc_interface, sc_prim_channel, sc_signal, sc_fifo, and the writer policy helpers.

Read and Write

#include <systemc>
using namespace sc_core;
 
SC_MODULE(Writer) {
  sc_out<int> out{"out"};
  SC_CTOR(Writer) { SC_THREAD(run); }
  void run() {
    out.write(42);
    wait(10, SC_NS);
  }
};
 
int sc_main(int, char*[]) {
  sc_signal<int> data{"data"};
  Writer w("writer");
  w.out(data);
 
  data.write(42);
  int old_or_new = data.read();
  
  sc_start(20, SC_NS);
  return 0;
}

The tricky part is timing. A write does not necessarily become visible immediately to all other processes. The signal requests an update from the kernel. During the update phase, the current value changes and value-change events are notified.

Why Delayed Update Exists

Hardware does not usually behave like a sequence of software assignments. If two processes evaluate during the same simulated moment, the final state should not depend on an arbitrary function-call order.

Delayed update gives SystemC a hardware-like discipline:

  1. Processes evaluate and request channel updates.
  2. Primitive channels update.
  3. Events from those updates wake dependent processes.
  4. More delta cycles run if necessary.

This is the evaluate-update rhythm behind many SystemC semantics.

Clocks

sc_clock is a predefined channel that toggles over time:

#include <systemc>
using namespace sc_core;
 
SC_MODULE(ClockedModule) {
  sc_in<bool> clk{"clk"};
  SC_CTOR(ClockedModule) {
    SC_METHOD(tick);
    sensitive << clk.pos();
    dont_initialize();
  }
  void tick() {
    std::cout << "Tick at " << sc_time_stamp() << std::endl;
  }
};
 
int sc_main(int, char*[]) {
  sc_clock clk{"clk", 10, SC_NS};
  ClockedModule mod("mod");
  mod.clk(clk);
  sc_start(30, SC_NS);
  return 0;
}

dont_initialize() prevents the method from running once at time zero before the first triggering edge.

Writer Policies

Signals can enforce writer rules. A common bug is accidentally driving the same signal from multiple processes. SystemC has writer-policy machinery to detect or allow different cases, depending on the signal type and configuration.

Resolved signals exist for cases such as tri-state or multi-driver logic, but you should not use them to hide accidental architecture problems. If a signal has multiple writers, make that a conscious design choice.

Primitive Channels

Signals are primitive channels. They participate directly in the kernel update phase through request_update() and an update() callback. That source-code shape explains why writing a signal from a process is not the same as assigning a C++ variable.

The public API is small. The behavior comes from how the channel cooperates with the scheduler.

Under the Hood: Evaluate and Update Phases in sc_signal

sc_signal<T> inherits from sc_prim_channel. This is crucial for the Evaluate-Update paradigm. Inside sc_signal, there are two member variables representing state: m_cur_val (current value) and m_new_val (next value). When a process writes to a signal (sig.write(val)), m_new_val is updated, and the channel calls request_update() (sysc/kernel/sc_simcontext.cpp). This registers the signal in the scheduler's m_update_list. During the Evaluate phase, all runnable processes execute. During the Update phase, the scheduler loops over m_update_list and calls update() on each primitive channel. sc_signal::update() assigns m_cur_val = m_new_val and, if the value changed, fires the value_changed_event, which schedules sensitive processes for the next delta cycle.

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