Chapter 5: Source Internals

Source Deep Dive: sc_signal Update Internals

How writes, current values, pending values, update requests, and value-change events work.

sc_signal<T> is one of the best source-reading targets because it demonstrates the SystemC kernel contract for primitive channels, specifically LRM Section 6.15.

The user simply writes sig.write(next_value);, but hardware-like behavior requires more than assigning a C++ variable.

The Two-Value Model and Primitive Channels

A signal has a current value and a pending value. To understand exactly how the SystemC simulation context manages this, we can write a complete, compilable primitive channel that implements the LRM's request_update() and update() semantics without hiding behind the built-in sc_signal.

#include <systemc>
 
using namespace sc_core;
 
// 1. The Interface
template <typename T>
struct custom_signal_if : virtual public sc_interface {
  virtual const T& read() const = 0;
  virtual void write(const T&) = 0;
  virtual const sc_event& default_event() const = 0;
};
 
// 2. The Primitive Channel mimicking sc_signal
template <typename T>
class SignalInternalsDemo : public sc_prim_channel, public custom_signal_if<T> {
private:
  T m_current_value;
  T m_new_value;
  sc_event m_value_changed;
 
public:
  explicit SignalInternalsDemo(const char* name) 
    : sc_prim_channel(name), m_current_value(T()), m_new_value(T()) {}
 
  const T& read() const override {
    return m_current_value;
  }
 
  void write(const T& value) override {
    // Check if the value is actually changing
    if (value != m_new_value) {
      m_new_value = value;
      // This tells the simcontext to push this channel into the update queue
      request_update();
    }
  }
 
  const sc_event& default_event() const override {
    return m_value_changed;
  }
 
protected:
  // 3. The Update Callback (Called by the Kernel)
  void update() override {
    if (m_current_value != m_new_value) {
      m_current_value = m_new_value;
      // Notify sensitive processes in the next delta cycle
      m_value_changed.notify(SC_ZERO_TIME);
    }
  }
};
 
// 4. Test Module
SC_MODULE(SignalTest) {
  sc_port<custom_signal_if<int>> port{"port"};
 
  SC_CTOR(SignalTest) {
    SC_THREAD(writer_thread);
    SC_METHOD(reader_method);
    sensitive << port; // Binds to default_event()
    dont_initialize();
  }
 
  void writer_thread() {
    std::cout << "[Time: " << sc_time_stamp() << "] Writing 42\n";
    port->write(42);
    
    // Read immediately? It will be the OLD value.
    std::cout << "[Time: " << sc_time_stamp() << "] Immediate read: " << port->read() << "\n";
    
    wait(SC_ZERO_TIME); // Advance to next delta
    
    // Read after update phase
    std::cout << "[Time: " << sc_time_stamp() << "] Read after delta: " << port->read() << "\n";
  }
 
  void reader_method() {
    std::cout << "[Time: " << sc_time_stamp() << "] Reader method triggered. New value: " << port->read() << "\n";
  }
};
 
int sc_main(int argc, char* argv[]) {
  SignalInternalsDemo<int> sig("sig");
  SignalTest test("test");
  test.port(sig);
 
  sc_start(10, SC_NS);
  return 0;
}

The real implementation handles more details (writer policies, tracing, reset integration), but the visible behavior comes exactly from this sequence:

  1. process writes signal
  2. signal requests update
  3. scheduler finishes evaluate phase
  4. kernel calls update() on the channel
  5. signal commits pending value and notifies value-change event
  6. sensitive processes become runnable in a later delta

Writer Checks and Resolved Signals

Signals enforce writer policies (LRM Section 6.15.3). If two processes drive one signal unexpectedly, the implementation reports an error. This is not just politeness; multiple writers can make a hardware model ambiguous.

Resolved signal types (sc_resolved, sc_rv) exist for multi-driver logic. They apply resolution rules to determine the final value. Use resolved signals for real multi-driver semantics (tri-state buses), not to silence accidental multiple-driver bugs.

Debugging Signal Internals

If a signal value appears late:

  • the writer may have only set the pending value
  • the update phase may not have run yet
  • the reader may be sensitive to the value-change event in the next delta

Once you understand the pending/current split and the evaluate-update cycle, most signal timing questions become explainable.

Comments and Corrections