Chapter 13: Modeling Best Practices

Modeling Best Practices: Source and Standard Traceability

How to connect model behavior to LRM rules, Accellera implementation files, examples, tests, and project documentation.

How to Read This Lesson

This best-practice lesson is written for code reviews. Use it to decide what should be portable standard behavior, what is an implementation detail, and what needs a project rule.

Modeling Best Practices: Source and Standard Traceability

For senior engineers, "because it works in my simulator" is not enough. SystemC models must outlive the specific compiler and simulator version they were written against.

Important model behavior should be explicitly traceable to a standard rule (IEEE 1666 LRM), an implementation detail (Accellera PoC GitHub repositories), a specific test, or a documented internal project policy.

Source and LRM Trail

Best-practice lessons should be traceable. Use Docs/LRMs/SystemC_LRM_1666-2023.pdf, the domain LRMs for AMS/CCI/UVM when relevant, .codex-src/systemc, .codex-src/cci, .codex-src/uvm-systemc, and .codex-src/systemc-common-practices. Mark what is portable, what is source insight, and what is project policy.

The Four Traceability Levels

When commenting code or discussing architectural decisions, classify the justification into one of these four levels:

  1. Standard: The behavior is mandated by the IEEE 1666 LRM. (e.g., "Ports must be bound before end_of_elaboration"). This code is 100% portable across all commercial simulators.
  2. Implementation: The behavior is derived from reading the Accellera open-source codebase (e.g., sysc/kernel/sc_simcontext.cpp). (e.g., "The Accellera kernel uses sc_pq (a priority queue) for m_timed_events, making future event insertion O(log N). However, it uses a simple std::vector for m_update_list, making request_update() extremely fast"). This code is mostly portable, but relies on implementation details that an aggressive commercial simulator might change.
  3. Project Policy: The behavior is a team convention. (e.g., "All our virtual platforms use native uint32_t for memory rather than sc_bv<32> for performance").
  4. Tool Workaround: The behavior exists to bypass a bug in a specific tool or compiler. These should always be marked with a TODO or issue tracker link so they can be deleted later.

Example: Signal Update Semantics

Standard Traceability:

IEEE 1666 Section 6.4: The sc_signal::write() method shall submit an update request. The new value shall not be visible to readers until the update phase of the current delta cycle.

Kernel Source Traceability:

In sysc/communication/sc_signal.cpp, write() stores the value in m_new_val and calls request_update(). The kernel pushes this pointer to sc_simcontext::m_update_list. Later, sc_simcontext::crunch() iterates the list and calls the virtual update() method, which finally commits m_new_val to m_cur_val and notifies m_value_changed_event.

Project Policy Translation:

To prevent non-deterministic combinatorial loops, no IP block in this project shall attempt to read an sc_signal in the same evaluation phase it was written.

Complete Example: Documenting Traceable Code

Here is a complete sc_main demonstrates how to write a SystemC module where every architectural decision is documented with its traceability level. This is the gold standard for industrial Virtual Platforms.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
 
SC_MODULE(TraceableIP) {
    // [Project Policy]: Sockets must use snake_case and end with '_socket'
    tlm_utils::simple_target_socket<TraceableIP> target_socket{"target_socket"};
 
    // [Standard]: SC_HAS_PROCESS is required when not using SC_CTOR. 
    // Reference: IEEE 1666 Section 5.2.7
    SC_HAS_PROCESS(TraceableIP);
 
    TraceableIP(const sc_core::sc_module_name& name) : sc_core::sc_module(name) {
        target_socket.register_b_transport(this, &TraceableIP::b_transport);
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // [Standard]: Target MUST set response status before returning.
        // Reference: IEEE 1666 Section 14.12
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
 
        // [Implementation]: We accumulate delay locally to leverage the 
        // Accellera TLM quantum keeper optimization, reducing context switches.
        // Under the hood, this avoids pushing an sc_event_timed to the kernel's m_timed_events queue.
        delay += sc_core::sc_time(10, sc_core::SC_NS);
 
        // [Tool Workaround]: Simulator X crashes if we print payload pointers directly,
        // so we cast to void* first. (Ticket: #1234)
        std::cout << "[TraceableIP] Handled payload at ptr: " 
                  << static_cast<void*>(&trans) << "\n";
    }
};
 
int sc_main(int argc, char* argv[]) {
    // [Project Policy]: All top-level signals must be explicitly named
    sc_core::sc_signal<bool> rst_n{"rst_n"};
 
    TraceableIP ip("ip_inst");
 
    // Dummy test payload
    tlm::tlm_generic_payload trans;
    sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
    trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
 
    ip.target_socket->b_transport(trans, delay);
 
    return 0;
}

Why Traceability Matters

If a commercial simulator vendor updates their engine and your simulation suddenly hangs, traceability saves weeks of debugging.

If your comments cite the LRM (Level 1), you can file a bug against the vendor. If your comments rely on an Accellera implementation detail (Level 2), you know immediately that you wrote non-portable code and need to fix your architecture. If you used a Tool Workaround (Level 4), you know you can likely delete the workaround now that the tool was updated.

Deep Dive: Accellera Source for sc_signal and update()

The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.

The write() Implementation

When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:

template<class T>
inline void sc_signal<T>::write(const T& value_) {
    if( !(m_new_val == value_) ) {
        m_new_val = value_;
        this->request_update(); // Inherited from sc_prim_channel
    }
}

The request_update() call appends the channel to sc_simcontext::m_update_list.

The update() Phase

After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:

template<class T>
inline void sc_signal<T>::update() {
    if( !(m_new_val == m_cur_val) ) {
        m_cur_val = m_new_val;
        m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
    }
}

This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.

Comments and Corrections