Chapter 13: Modeling Best Practices

Modeling Best Practices: Public API Contracts

How to define stable contracts for reusable SystemC models: sockets, ports, registers, parameters, reports, traces, and ownership.

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: Public API Contracts

A SystemC model used by other teams has an API even if nobody calls it a library. If you change a parameter name, a report ID, or a TLM socket behavior, you break the build or the simulations of downstream users.

To write industrial-grade Virtual Platform (VP) models, you must define and stabilize your Public API Contracts. Let us examine how these contracts map directly to Accellera kernel structures.

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 Contract Surfaces

In a professional SystemC IP block, the following elements constitute the public contract and must not be broken or changed without version bumping:

  • Module Hierarchy Names: Do not use sc_gen_unique_name() for top-level objects.
    • Kernel Reality: The kernel maintains a global hash table in sc_object_manager mapping hierarchical string paths to sc_object*. Automated configuration tools (via CCI) rely on these paths. Generating unique names dynamically breaks reproducible configurations.
  • Ports and Exports: Ensure the data types and interface types are completely stable.
  • TLM Sockets: Which payload extensions are required? What byte enables are supported?
  • Register Map: Base offsets, bitfields, and reset behaviors.
  • CCI Parameters: Hierarchical paths, metadata, default values, and mutability.
    • Kernel Reality: These are registered in the singleton cci_broker_if. If a parameter name changes, top-level JSON configurations loaded by the broker will fail to apply.
  • Report IDs (msg_type): The exact strings used in SC_REPORT_WARNING and SC_REPORT_ERROR.
    • Kernel Reality: The sc_report_handler maintains a rule map comparing the exact msg_type string to decide whether to SC_DISPLAY, SC_LOG, or SC_STOP. Breaking the string breaks the user's simulation filters.
  • DMI / Debug Transport: Whether the model safely supports backdoor memory access without side-effects.

Socket Contracts

For each TLM socket, your Doxygen or Markdown documentation must answer:

  • What happens if get_byte_enable_ptr() != nullptr?
  • Are TLM_IGNORE_COMMAND payloads handled gracefully?
  • What is the expected streaming width?
  • If it is an AT (Approximately Timed) target, which protocol phases does it actively use?
  • Memory Management: For b_transport and nb_transport, does the target adhere to tlm_mm_interface reference counting? For transport_dbg, does it safely bypass acquire() and release()? (Debug transactions do not use the memory pool).

Complete Example: Designing a Contract-Safe IP Block

Here is a complete sc_main demonstrates an IP block that treats its external surface as a strict contract, validating inputs safely and exposing a clean, documented hierarchy.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
 
// ---------------------------------------------------------
// IP BLOCK WITH STRICT CONTRACTS
// ---------------------------------------------------------
class ContractSafeIP : public sc_core::sc_module {
public:
    // Contract 1: Stable Socket Name
    tlm_utils::simple_target_socket<ContractSafeIP> target_socket{"target_socket"};
 
    // Contract 2: Stable Port Name
    sc_core::sc_out<bool> interrupt_out{"interrupt_out"};
 
    // Contract 3: Documented Register Map
    static constexpr uint64_t REG_STATUS = 0x00;
    static constexpr uint64_t REG_DATA   = 0x04;
 
    SC_HAS_PROCESS(ContractSafeIP);
    
    // Contract 4: Predictable Constructor
    ContractSafeIP(const sc_core::sc_module_name& name) 
        : sc_core::sc_module(name), internal_data(0) {
        
        // Register standard blocking transport callback
        target_socket.register_b_transport(this, &ContractSafeIP::b_transport);
        
        // Register standard debug transport callback (No side-effects!)
        target_socket.register_transport_dbg(this, &ContractSafeIP::transport_dbg);
    }
 
private:
    uint32_t internal_data;
 
    // --- Blocking Transport (Functional Behavior) ---
    void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
        tlm::tlm_command cmd = trans.get_command();
        uint64_t         adr = trans.get_address();
        unsigned int     len = trans.get_data_length();
        unsigned char*   byt = trans.get_byte_enable_ptr();
 
        // Contract Enforcement: No byte enables supported
        if (byt != nullptr) {
            trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
            return;
        }
 
        // Contract Enforcement: Must be 32-bit access
        if (len != 4) {
            trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
            return;
        }
 
        // Functional side-effects happen here
        if (cmd == tlm::TLM_WRITE_COMMAND && adr == REG_DATA) {
            memcpy(&internal_data, trans.get_data_ptr(), 4);
            interrupt_out.write(true); // Side-effect!
            
            // Contract 5: Stable Report ID.
            // The sc_report_handler uses this exact string to filter output.
            SC_REPORT_INFO("IP_BLOCK/WRITE", "Data register updated, interrupt asserted.");
        } 
        else if (cmd == tlm::TLM_READ_COMMAND && adr == REG_DATA) {
            memcpy(trans.get_data_ptr(), &internal_data, 4);
            interrupt_out.write(false); // Side-effect!
        }
        else {
            trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
            return;
        }
 
        trans.set_response_status(tlm::TLM_OK_RESPONSE);
        delay += sc_core::sc_time(10, sc_core::SC_NS);
    }
 
    // --- Debug Transport (No Side Effects!) ---
    unsigned int transport_dbg(tlm::tlm_generic_payload& trans) {
        if (trans.get_command() == tlm::TLM_READ_COMMAND && trans.get_address() == REG_DATA) {
            if (trans.get_data_length() >= 4) {
                memcpy(trans.get_data_ptr(), &internal_data, 4);
                // Notice: We do NOT clear the interrupt here! Debug is invisible.
                // We also do not call trans.acquire() or release() for debug payloads.
                return 4;
            }
        }
        return 0;
    }
};
 
// ---------------------------------------------------------
// TESTBENCH
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
    sc_core::sc_signal<bool> irq{"irq"};
    ContractSafeIP ip_inst("ip_inst");
    ip_inst.interrupt_out(irq);
 
    // Dummy payload for testing
    tlm::tlm_generic_payload trans;
    sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
    uint32_t data = 0xABCD;
 
    trans.set_command(tlm::TLM_WRITE_COMMAND);
    trans.set_address(ContractSafeIP::REG_DATA);
    trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
    trans.set_data_length(4);
    trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
 
    std::cout << "Sending TLM Write...\n";
    ip_inst.target_socket->b_transport(trans, delay);
 
    if (trans.is_response_ok()) {
        std::cout << "Write Successful. IRQ State: " << irq.read() << "\n";
    }
 
    return 0;
}

Explanation of the Execution

Sending TLM Write...
Info: (I804) /IEEE_Std_1666/main: IP_BLOCK/WRITE: Data register updated, interrupt asserted.
Write Successful. IRQ State: 1

By explicitly checking byte enables, length, and addresses, the IP block guarantees it will not crash with a segmentation fault if an integrator misuses it. By separating b_transport from transport_dbg, it allows software debuggers (GDB connected to the VP) to inspect memory without accidentally triggering hardware state machines.

This level of rigor is what transforms "academic SystemC" into "industrial SystemC".

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