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)
Key Observation The signal always lags one delta behind the regular variable. Each 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.

MethodReturnsUse 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 */ }
}
Note on event() 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
Build Tip Always define -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:

Behavioursc_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.

Best Practice Always use explicit .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

MethodDescription
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


Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.