Chapter 13: Modeling Best Practices

Modeling Best Practices: Expert Code Review Checklist

A senior SystemC review checklist for kernel semantics, TLM correctness, CCI configuration, AMS boundaries, UVM usage, and 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: Expert Code Review Checklist

When reviewing production SystemC code, "it compiles and simulates" is not enough. SystemC's flexibility means poor architectural choices might simulate correctly today but deadlock tomorrow under a different kernel scheduler order, or cripple the performance of an entire SoC integration.

Use this LRM-compliant checklist to evaluate IP blocks before they are merged into a shared library. We will include the Accellera kernel source rationale for why these rules exist.

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.

1. Core Kernel Semantics

Rule: Ensure predictable scheduling and memory safety.

  • Hierarchy Lifetimes: No sc_object (ports, modules, signals) is created as a local stack variable inside a constructor.
    • Kernel Reason: The sc_object_manager links every child sc_object into a dynamically managed tree via m_hierarchy_curr. If an object is allocated on the stack, it is destroyed when the constructor exits, leaving a dangling pointer in the kernel's object tree. sc_object derivatives must be allocated via new or be class members.
  • Elaboration Phase: Ports and exports are fully bound before end_of_elaboration.
  • Method Restrictions: SC_METHOD processes must never call wait().
    • Kernel Reason: SC_METHOD processes are executed directly by sc_simcontext::crunch() on the OS's primary call stack. They do not have an allocated coroutine stack (QuickThreads or pthreads). If you call wait(), the kernel catches this via sc_get_curr_process_handle()->process_kind() and throws SC_ID_WAIT_NOT_ALLOWED_ because there is no context to yield.
  • Thread Yielding: SC_THREAD processes must contain a wait() statement inside their infinite loop (otherwise the simulation hangs forever).
  • Writer Policies: If SC_MANY_WRITERS is used on a signal, is it justified?
    • Kernel Reason: In sc_signal::write(), the kernel aggressively throws an exception if multiple distinct processes call write() in the same evaluation phase, ensuring deterministic behavior. Disabling this bypasses hardware realism checks.

2. TLM-2.0 Correctness

Rule: Ensure strict adherence to the TLM-2.0 protocol phases.

  • Response Status: Every target socket MUST set a response status (set_response_status()) before returning from a blocking transport call.
  • Memory Management: Does the module safely handle tlm_generic_payload memory management (tlm_mm_interface)?
    • Kernel Reason: Allocating payloads via new is incredibly slow. The TLM-2.0 standard requires initiators to acquire payloads from a memory pool, call acquire(), pass them to targets, and upon return, call release(). The target must not retain pointers to get_data_ptr() after the transaction ends without acquiring a reference lock.
  • Unsupported Commands: If a target doesn't support TLM_WRITE_COMMAND, does it correctly return TLM_COMMAND_ERROR_RESPONSE instead of silently ignoring it?
  • Data Length: Does the target validate get_data_length() against its internal register sizes to prevent buffer overflows (segfaults)?
  • Debug Transport: Does transport_dbg guarantee zero side-effects? (It must never advance time, clear interrupts, or pop FIFOs).

3. Configuration & Virtual Platform Quality

Rule: Ensure the IP is reusable and configurable by a top-level architect.

  • Parameters: Are magic numbers replaced by CCI-compliant parameters (cci::cci_param)?
  • Abstraction: Is the timing abstraction clear? (e.g., "This timer fires accurately, but register read delays are assumed to be 0").
  • Reporting: Do SC_REPORT messages use a hierarchical msg_type (e.g., "/VENDOR/IP/ERROR")? Do error messages include context (Address, value, internal state)?
    • Kernel Reason: sc_report_handler uses string matching to selectively suppress, log, or stop simulation based on these msg_type strings. Generic strings like "Error" break the ability to filter.

Complete Example: The Reviewer's Sandbox

Here is a complete sc_main acts as a "Reviewer's Sandbox", demonstrating a module that passes the checklist (Good IP) and a module that fails several checks (Bad IP).

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
 
// ---------------------------------------------------------
// BAD IP: Fails the Code Review
// ---------------------------------------------------------
SC_MODULE(BadIP) {
    tlm_utils::simple_target_socket<BadIP> socket{"socket"};
    
    SC_CTOR(BadIP) {
        socket.register_b_transport(this, &BadIP::b_transport);
        // Fails Checklist: SC_METHOD calling wait()! 
        // Kernel will throw an SC_ID_WAIT_NOT_ALLOWED_ exception.
        // SC_METHOD(bad_method);
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        // Fails Checklist: Ignores command type!
        // Fails Checklist: Ignores byte enables!
        // Fails Checklist: NEVER SETS RESPONSE STATUS! (Initiator will panic)
        std::cout << "[BadIP] Received transaction.\n";
    }
};
 
// ---------------------------------------------------------
// GOOD IP: Passes the Code Review
// ---------------------------------------------------------
SC_MODULE(GoodIP) {
    tlm_utils::simple_target_socket<GoodIP> socket{"socket"};
    uint32_t internal_reg;
 
    SC_CTOR(GoodIP) : internal_reg(0) {
        socket.register_b_transport(this, &GoodIP::b_transport);
    }
 
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        unsigned char*   ptr = trans.get_data_ptr();
        unsigned int     len = trans.get_data_length();
        unsigned char*   byt = trans.get_byte_enable_ptr();
 
        // Passes Checklist: Validates Byte Enables
        if (byt != nullptr) {
            trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
            return;
        }
 
        // Passes Checklist: Validates Data Length
        if (len != 4) {
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }
 
        // Passes Checklist: Handles Commands properly
        if (cmd == tlm::TLM_READ_COMMAND) {
            memcpy(ptr, &internal_reg, 4);
        } else if (cmd == tlm::TLM_WRITE_COMMAND) {
            memcpy(&internal_reg, ptr, 4);
        }
 
        // Passes Checklist: Sets Successful Response Status
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        
        // Annotate abstract delay
        delay += sc_core::sc_time(10, sc_core::SC_NS);
        
        std::cout << "[GoodIP] Successfully processed " 
                  << (cmd == tlm::TLM_READ_COMMAND ? "READ" : "WRITE") << ".\n";
    }
};
 
int sc_main(int argc, char* argv[]) {
    GoodIP good("good_ip");
    BadIP bad("bad_ip");
 
    // We don't simulate here as the sockets are unconnected, 
    // but this code compiles and demonstrates the architectural differences.
    std::cout << "Code Review Sandbox Compiled Successfully.\n";
    
    return 0;
}

Explanation of the Execution

The BadIP module is a prime example of code that might compile but violates the TLM-2.0 standard. If an initiator sends a payload to BadIP, the initiator's check of trans.get_response_status() will show TLM_INCOMPLETE_RESPONSE, causing the simulation to abort or fail a verification check.

The GoodIP module defensively checks inputs, handles the payload safely, annotates timing accurately, and guarantees a response status is set. This is the quality expected of production virtual platform IP.

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