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 inSC_REPORT_WARNINGandSC_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_COMMANDpayloads 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