sc_signal and the Evaluate-Update Mechanism
If you've read the delta cycles article, you know that signal writes are deferred to the update phase. Now let's go deeper: why does sc_signal work this way, what does it actually look like in code, and what are the traps that catch engineers every day?
This article covers the full sc_signal<T> API — reads, writes, events, edge detection, the single-writer rule, and the dangerous implicit operators you should avoid.
1. Why sc_signal Exists — The Shift Register Problem
Consider a 4-stage hardware shift register. On every clock edge, each stage copies its value from the previous stage. In software, the order of assignments is everything:
// Software — order matters
Q4 = Q3;
Q3 = Q2;
Q2 = Q1;
Q1 = DATA;
If you flip the order, data shifts wrong. But in hardware, all four registers capture simultaneously — there is no ordering. In a SystemC simulation with four concurrent processes (one per register), the execution order is not guaranteed by the standard. Whichever process runs first will read a mix of old and new values, and the result depends on implementation.
The solution is the evaluate-update mechanism: writes are buffered during evaluate, and all updates are flushed simultaneously during the update phase. Every process reads the current (pre-update) value regardless of when it runs. Order stops mattering.
// SystemC — order doesn't matter — all read pre-update values void reg4_process() { Q4_sig.write(Q3_sig.read()); } void reg3_process() { Q3_sig.write(Q2_sig.read()); } void reg2_process() { Q2_sig.write(Q1_sig.read()); } void reg1_process() { Q1_sig.write(DATA.read()); }
2. Basic Syntax
// Declaration sc_signal<int> count_sig; sc_signal<bool> ready_sig; sc_signal<string> msg_sig; // Write (goes to new_value buffer — deferred) count_sig.write(42); // Read (returns current_value — always the pre-update value) int val = count_sig.read(); // Wait for any value change wait(count_sig.value_changed_event()); // Static sensitivity — trigger method on any change sensitive << count_sig; // Check if signal changed in the immediately previous delta if (count_sig.event()) { /* changed last delta */ }
3. The Read Lag — Step by Step
This example shows exactly how the delta cycle delay manifests. Watch carefully as each wait(SC_ZERO_TIME) advances one delta:
int count = 0; sc_signal<int> count_sig; // initial value: 0 // ── Delta 1 ────────────────────────────────── count_sig.write(10); // new_value = 10, current still 0 count = 11; // regular int: immediate cout << count << endl; // → 11 (immediate) cout << count_sig << endl; // → 0 (still old!) wait(SC_ZERO_TIME); // trigger update: current ← 10 // ── Delta 2 ────────────────────────────────── count_sig.write(count = 20); // new_value = 20, current still 10 cout << count << endl; // → 20 (immediate) cout << count_sig << endl; // → 10 (last delta's write) wait(SC_ZERO_TIME); // trigger update: current ← 20 // ── Delta 3 ────────────────────────────────── cout << count_sig << endl; // → 20 (now visible)
wait(SC_ZERO_TIME) causes one update cycle, making the previous write visible. This is expected — not a bug.
4. Events — Knowing When a Signal Changes
sc_signal<T> provides three event-related methods. For a deeper look at how events and sensitivity work across all process types, see Events and Sensitivity in SystemC.
| Method | Returns | Use Case |
|---|---|---|
value_changed_event() |
Reference to sc_event |
Wait for any change in signal value |
default_event() |
Alias for value_changed_event() |
Used by sensitive << sig under the hood |
event() |
bool |
True if signal changed in the immediately previous delta cycle |
// Dynamic — wait in an SC_THREAD wait(ready_sig.value_changed_event()); // Static — SC_METHOD retriggers on any change SC_METHOD(on_change); sensitive << ready_sig; // same as << ready_sig.default_event() // Inside on_change() — which signal triggered me? void on_change() { if (ready_sig.event()) { /* ready_sig changed this delta */ } if (valid_sig.event()) { /* valid_sig changed this delta */ } }
event() only returns true for the delta cycle immediately following the change — it resets on the next delta. Use it inside an SC_METHOD triggered by the signal, not after an arbitrary number of waits.
5. Edge Detection — posedge and negedge
For sc_signal<bool> and sc_signal<sc_logic> only, SystemC adds edge-detection methods that are essential for RTL-style clock modeling:
sc_signal<bool> clk; // Events wait(clk.posedge_event()); // wait for 0→1 transition wait(clk.negedge_event()); // wait for 1→0 transition // Static sensitivity sensitive << clk.pos(); // shorthand for posedge_event() sensitive << clk.neg(); // shorthand for negedge_event() // Check inside a method if (clk.posedge()) { /* rising edge occurred last delta */ } if (clk.negedge()) { /* falling edge occurred last delta */ }
These don't exist on sc_signal<int> or other generic types — only on the boolean and logic specializations. sc_buffer<T> does not support posedge/negedge either.
6. The Single-Writer Rule
Only one process may write to a given sc_signal during a single delta cycle. If two processes both write to the same signal in the same evaluate phase, the behaviour is undefined. The OSCI simulator will flag this as a runtime error only if the DEBUG_SYSTEMC macro is defined at compile time.
// BAD — two processes both write to sig in the same delta void process_A() { sig.write(1); } // ← which one wins? void process_B() { sig.write(0); } // ← undefined behaviour
-DDEBUG_SYSTEMC during development. Without it, multiple-writer violations are silently ignored and produce non-deterministic results. Remove it only after you've verified correctness and need the extra simulation speed.
If your design genuinely needs multiple drivers — for example, a tri-state bus — use sc_signal_resolved or sc_signal_rv<N>, which support multiple writers and apply a built-in wired-AND/wired-OR resolution table.
7. sc_buffer — The Difference
sc_signal is just one type of channel in SystemC's hierarchy — see Evolution of Channels in SystemC for the full picture. Within that family, sc_buffer<T> is almost identical to sc_signal<T> with one key difference:
| Behaviour | sc_signal<T> | sc_buffer<T> |
|---|---|---|
| Fires value_changed_event | Only when the value actually changes | Every time write() is called, even with same value |
| posedge / negedge | Supported (bool/sc_logic only) | Not supported |
| Use case | Hardware signals, clocks, data lines | Trigger channels where re-assertion matters |
If you write the same value to an sc_signal twice in a row, no event fires the second time. With sc_buffer, the event always fires on write — useful when the act of writing is itself a meaningful trigger, regardless of value.
8. The Dangerous Implicit Operators
SystemC overloads the assignment and implicit conversion operators on sc_signal, which allows this syntax:
// Implicit — looks like a regular variable sig = 42; // calls write() — DANGEROUS int x = sig; // calls read() — mildly dangerous
Why dangerous? Consider this seemingly innocent code:
// r is sc_signal<int>, x=3, y=4, r=0 on entry r = x; // write(3) → new_value=3, current=0 if (r != 0 && r != 1) r = r * r; // read()=0! 0*0=0 → write(0) if (y != 0) r = r + y*y; // read()=0, 0+16=16 → write(16) cout << sqrt(r) << endl; // → 0 (current still 0!)
Expected result: sqrt(3² + 4²) = 5. Actual result: 0. Every read returns the current value (0), not the value just written. The code looks like normal arithmetic but behaves completely differently because r is a signal.
.write() and .read() calls. Use a naming convention like a _sig suffix so signal variables are immediately obvious in code review. Never mix signal reads and writes in arithmetic expressions.
9. sc_signal Full API Reference
| Method | Description |
|---|---|
write(val) | Write to new_value buffer (deferred) |
read() | Read current_value (pre-update) |
value_changed_event() | Event fired when value changes after update |
default_event() | Alias for value_changed_event() |
event() | True if changed in immediately previous delta |
posedge_event() | Event on 0→1 transition (bool/sc_logic only) |
negedge_event() | Event on 1→0 transition (bool/sc_logic only) |
pos() | Shorthand for posedge_event() in sensitive list |
neg() | Shorthand for negedge_event() in sensitive list |
posedge() | True if rising edge in immediately previous delta |
negedge() | True if falling edge in immediately previous delta |
Summary
sc_signal<T>models hardware signals with deferred writes — it is not a variable.- Writes go to a new value buffer; reads return the current value. They sync during the update phase.
- Reads after a write in the same evaluate pass return the old value — always.
- Use
value_changed_event()to wait for changes;event()to check if change happened last delta. posedge_event()/negedge_event()only available onsc_signal<bool>andsc_signal<sc_logic>.- Only one process may write a signal per delta cycle — enforce with
-DDEBUG_SYSTEMC. sc_buffer<T>fires events on every write, even same-value — useful when write itself is the trigger.- Avoid implicit
=operators on signals — always use explicit.write()and.read().