Delta Cycles in SystemC — What Happens Inside One Timestep
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:
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.
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:
- current value — what readers see
- new value — where writes go during evaluate
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:
- Evaluate — run all runnable processes; writes to signals go to new value
- Update — flush pending writes; fire value-changed events if value changed
- If any processes became runnable from step 2 → go back to step 1 (another delta cycle, same timestamp)
- 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.
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_value → current_value. The signal then reads as 10.
<=). 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
- A delta cycle is one evaluate + one update pass. It consumes zero simulation time.
- During evaluate, all runnable processes execute. Signal writes go to a new value buffer.
- During update, buffers are flushed: new value → current value. Changed signals fire events.
- Multiple delta cycles can occur at the same timestamp until the system reaches a stable state.
notify(SC_ZERO_TIME)fires during update — after evaluate — so all processes have reached theirwait().sc_signal.read()returns the current value — writes are only visible after the update phase.- Use
sc_delta_count()to inspect delta activity during debugging.