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:
- Processes evaluate and request channel updates.
- Primitive channels update.
- Events from those updates wake dependent processes.
- 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