You run a simulation. Two processes share a signal. One writes to it, the other reads it immediately after — and gets the old value. No bug in your logic. No race condition in the traditional sense. Just the simulator doing exactly what it's supposed to do: enforcing the delta cycle.

Delta cycles are the invisible heartbeat of every SystemC simulation. Once you understand them, a whole class of confusing simulation behaviors stops being mysterious. This article explains the full evaluate-update loop, where SC_ZERO_TIME fits in, and why sc_signal reads lag by one cycle.

1. The Simplified Simulation Engine (First Pass)

In the previous articles we used a simplified model of the scheduler: processes run during evaluate, time advances when the runnable set is empty. That model is correct — but incomplete. It's missing the update phase.

Here is the full simulation loop:

Elaborate
Initialize
Evaluate
Update
← delta cycle →
Advance Time
Cleanup

One complete Evaluate → Update pair is called a delta cycle. Multiple delta cycles can occur at the same simulation timestamp — time does not advance during a delta cycle. Only when the evaluate-update loop has fully settled does the scheduler advance to the next scheduled time.

Definition A delta cycle is one pass through the evaluate phase followed by one pass through the update phase. It consumes zero simulation time. Any number of delta cycles may occur at a single timestamp before time advances.

2. What Happens During Evaluate

During evaluate, the kernel picks runnable processes one at a time and executes each until it either wait()s or returns. Order of selection is unspecified — any process from the runnable set may run first. A given simulator implementation must be deterministic (same run = same order), but the standard doesn't mandate which order.

A key insight: all statements inside a process execute in zero simulated time. There is no simulated time cost to computing, branching, writing to a variable, or writing to a signal. Everything within one evaluate pass happens at the same timestamp.

// All of these execute at the same simulation timestamp
void my_process() {
  int x = compute_something();    // 0 simulated time
  sig_out.write(x);              // 0 simulated time
  if (x > 10) do_something_else(); // 0 simulated time
  wait(10, SC_NS);               // NOW time advances 10 ns
}

Once all runnable processes have yielded (via wait() or return), evaluate ends and the kernel enters update.


3. What Happens During Update

The update phase exists to solve a fundamental problem in concurrent simulation: if two processes both write and read the same signal in the same evaluate pass, what value should the reader see? Whichever process runs first? That would make the result dependent on process ordering — non-deterministic by design.

The solution: writes are deferred. Every evaluate-update channel (like sc_signal) maintains two storage locations:

Evaluate phase
sig.write(42) → stores 42 in new_value
sig.read() → returns current_value (old!)
Update phase
new_value → copied to current_value
value_changed_event fired if value changed

During update, the kernel calls the update() method of every channel that called request_update() during evaluate. The channel copies the new value into the current value and, if the value actually changed, fires a value_changed_event — which may put new processes into the runnable set, triggering another delta cycle at the same timestamp.


4. The Full Delta Cycle Loop

Here is the complete picture of what happens at a single timestamp:

  1. Evaluate — run all runnable processes; writes to signals go to new value
  2. Update — flush pending writes; fire value-changed events if value changed
  3. If any processes became runnable from step 2 → go back to step 1 (another delta cycle, same timestamp)
  4. If no processes are runnable → advance to the next scheduled time

Each pass through steps 1–2 is one delta cycle. A timestamp can have many delta cycles if signals keep changing and waking new processes.

Delta cycle storms If a chain of signals keeps triggering new events, the simulation can spin through hundreds of delta cycles at the same timestamp. This is usually a modelling bug — a combinational loop with no break condition. The simulation won't hang (the kernel detects no-progress), but it will be slow and confusing to debug.

5. Where SC_ZERO_TIME Fits In

In the previous article we used notify(SC_ZERO_TIME) as the safe alternative to immediate notify(). Now we can explain exactly why it's safe:

Notification When it fires Risk
notify() Immediately — mid-evaluate, right now Processes not yet waiting will miss it
notify(SC_ZERO_TIME) At the start of the next evaluate phase (after this update) All processes reach wait() before it fires — safe
notify(10, SC_NS) After time advances 10 ns Safe — all processes have long suspended by then

SC_ZERO_TIME doesn't advance simulation time by zero — it schedules the notification to fire during the update phase of the current delta cycle, which means processes see it at the start of the next evaluate. By then every process has had a chance to reach its wait(). No missed events.


6. The sc_signal Read Lag — Explained

This is the most common source of confusion for developers new to SystemC (especially those coming from pure software backgrounds). Consider:

sc_signal<int> count_sig;
int count;

// Inside a process, at time 0:
count_sig.write(10);   // write goes to new_value
count = 11;            // regular var: updates immediately

cout << count     << endl;  // prints: 11  ✓
cout << count_sig << endl;  // prints: 0   ← still old value!

wait(SC_ZERO_TIME);         // let update phase run

cout << count_sig << endl;  // prints: 10  ✓ now visible

The signal read returns 0 (the initial value) even though we just wrote 10 to it. This is not a bug — it's the evaluate-update paradigm working correctly. The write went to new_value. The read returns current_value. They become equal only after the update phase runs.

The call to wait(SC_ZERO_TIME) yields control to the kernel, which runs the update phase, which copies new_valuecurrent_value. The signal then reads as 10.

HDL Analogy This is identical to VHDL's signal assignment semantics and Verilog's non-blocking assignment (<=). Hardware description languages have always worked this way — the update is deferred to the end of the simulation cycle to model concurrent hardware behaviour faithfully.

7. Observing Delta Cycles with sc_delta_count()

SystemC provides a built-in function to inspect the current delta count — useful for debugging complex timing issues:

// Returns total delta cycles elapsed since simulation start
sc_dt::uint64 d = sc_delta_count();
cout << "Delta: " << d << " @ " << sc_time_stamp() << endl;

This counter increments with every delta cycle regardless of simulated time. If you see it climbing rapidly at a single timestamp, you likely have a combinational feedback loop or an unexpected event chain.


8. Practical Rules

Situation What to do
Read a signal you just wrote to wait(SC_ZERO_TIME) first — the write won't be visible until after update
Notify an event that other processes are waiting for Use notify(SC_ZERO_TIME) — fires safely after all processes yield
Multiple processes write to the same signal in the same delta Only the last write survives — earlier writes are overwritten before update
Check if a signal changed in the previous delta sig.event() returns true if the signal fired in the immediately previous delta
Simulation spinning at one timestamp Check sc_delta_count() — you likely have a feedback loop

Summary