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_managerlinks every childsc_objectinto a dynamically managed tree viam_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_objectderivatives must be allocated vianewor be class members.
- Kernel Reason: The
- Elaboration Phase: Ports and exports are fully bound before
end_of_elaboration. - Method Restrictions:
SC_METHODprocesses must never callwait().- Kernel Reason:
SC_METHODprocesses are executed directly bysc_simcontext::crunch()on the OS's primary call stack. They do not have an allocated coroutine stack (QuickThreads or pthreads). If you callwait(), the kernel catches this viasc_get_curr_process_handle()->process_kind()and throwsSC_ID_WAIT_NOT_ALLOWED_because there is no context to yield.
- Kernel Reason:
- Thread Yielding:
SC_THREADprocesses must contain await()statement inside their infinite loop (otherwise the simulation hangs forever). - Writer Policies: If
SC_MANY_WRITERSis used on a signal, is it justified?- Kernel Reason: In
sc_signal::write(), the kernel aggressively throws an exception if multiple distinct processes callwrite()in the same evaluation phase, ensuring deterministic behavior. Disabling this bypasses hardware realism checks.
- Kernel Reason: In
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_payloadmemory management (tlm_mm_interface)?- Kernel Reason: Allocating payloads via
newis incredibly slow. The TLM-2.0 standard requires initiators to acquire payloads from a memory pool, callacquire(), pass them to targets, and upon return, callrelease(). The target must not retain pointers toget_data_ptr()after the transaction ends without acquiring a reference lock.
- Kernel Reason: Allocating payloads via
- Unsupported Commands: If a target doesn't support
TLM_WRITE_COMMAND, does it correctly returnTLM_COMMAND_ERROR_RESPONSEinstead 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_dbgguarantee 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_REPORTmessages use a hierarchicalmsg_type(e.g.,"/VENDOR/IP/ERROR")? Do error messages include context (Address, value, internal state)?- Kernel Reason:
sc_report_handleruses string matching to selectively suppress, log, or stop simulation based on thesemsg_typestrings. Generic strings like "Error" break the ability to filter.
- Kernel Reason:
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