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.

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.

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.
  • 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.
  • Report IDs (msg_type): The exact strings used in SC_REPORT_WARNING and SC_REPORT_ERROR.
  • 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?

Complete Example: Designing a Contract-Safe IP Block

This 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
            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.
                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".

Comments and Corrections