Chapter 13: Modeling Best Practices

Modeling Best Practices: API Docs and Doxygen

How to comment SystemC modules, sockets, registers, CCI parameters, callbacks, and examples so generated docs help real users.

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: API Docs and Doxygen

A SystemC Virtual Platform is a software product. Like any software library, if the APIs are not documented, they are unusable. In SystemC, the "APIs" are your module constructors, TLM sockets, CCI parameters, and memory-mapped register contracts.

The industry standard for C++ documentation is Doxygen. Doxygen comments should explain contracts and abstractions, not just repeat the C++ syntax. Furthermore, Accellera provides programmatic ways to expose this metadata directly into the simulation kernel.

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.

What to Document and Expose to the Kernel

When distributing a SystemC IP block, the following elements MUST be documented, and where possible, registered with the Accellera kernel APIs:

  • Module Abstraction Level: What does it model? (RTL, AT, LT/VP). What is intentionally left out?
  • TLM Sockets: Which protocols do they support? Do they support DMI? What is the expected bus width?
  • Registers: Base offsets, bitfields, reset values, and side-effects of reads/writes (e.g., "Reading this register clears the interrupt").
  • CCI Parameters (cci_param): Name, type, default value, and mutability rules. When instantiating cci_param, you should also use cci_param::set_description() so that tools querying the cci_broker_if can extract the Doxygen string at runtime via the JSON-based cci_value AST.
  • Report Message Types (msg_type): The string IDs used in SC_REPORT_ERROR. You should document these so integrators can configure the sc_report_handler to suppress or escalate specific warnings.

Doxygen Commenting Style

Use the standard Doxygen /** ... */ syntax.

Module and Abstraction Comment

/**
 * @class Uart
 * @brief Memory-mapped UART model for VP firmware bring-up.
 *
 * Models TX/RX FIFOs, status flags, and interrupt generation.
 * @note Bit-level serial waveform timing is intentionally abstracted.
 * Data is transferred instantaneously when the TX FIFO drains.
 */
class Uart : public sc_core::sc_module {

TLM Socket Comment

/**
 * @brief Target socket receiving memory-mapped register transactions.
 * 
 * Supports standard TLM-2.0 b_transport. DMI is NOT supported for 
 * memory-mapped peripheral registers. Expected payload width is 32 bits.
 */
tlm_utils::simple_target_socket<Uart> target_socket{"target_socket"};

CCI Parameter Comment (with Kernel Registration)

/**
 * @brief Approximate per-byte transmit delay.
 *
 * Mutable during simulation. Changing this affects future bytes only.
 * If set to SC_ZERO_TIME, the UART operates in zero-delay mode.
 */
cci::cci_param<sc_core::sc_time> tx_delay{"tx_delay", sc_core::sc_time(1, sc_core::SC_US)};
 
// Inside SC_CTOR, push the documentation to the CCI Broker:
// tx_delay.set_description("Approximate per-byte transmit delay. Mutable.");

Complete Example: A Fully Documented IP Block

Here is a complete sc_main demonstrates how a professionally documented SystemC IP block should look. It includes Doxygen groupings, parameter documentation, and programmatic registration.

#include <systemc>
#include <cci_configuration>
#include <iostream>
 
/**
 * @defgroup vp_timer Timer IP Block
 * @brief Abstract timer model for Loosely Timed (LT) Virtual Platforms.
 * @{
 */
 
/**
 * @class VpTimer
 * @brief A 32-bit countdown timer with interrupt generation.
 * 
 * This model uses SystemC SC_THREADs to abstract away clock cycles. 
 * It calculates the exact future time an interrupt should fire and waits 
 * for that duration, maximizing simulation speed.
 */
SC_MODULE(VpTimer) {
    /**
     * @brief Interrupt output signal.
     * Active HIGH. Level-triggered.
     */
    sc_core::sc_out<bool> irq_out{"irq_out"};
 
    /**
     * @brief Frequency of the timer.
     * Accessible via cci_broker_if.
     */
    cci::cci_param<int> frequency_hz{"frequency_hz", 1000000};
 
    /**
     * @name Register Offsets
     * Memory map offsets relative to the module base address.
     * @{
     */
    static constexpr uint32_t REG_CTRL  = 0x00; ///< Control register. Bit 0: Enable.
    static constexpr uint32_t REG_LIMIT = 0x04; ///< Value to countdown from.
    static constexpr uint32_t REG_ACK   = 0x08; ///< Write any value to clear IRQ.
    /** @} */
 
    /**
     * @brief Constructs the VpTimer.
     * @param name The SystemC hierarchical name.
     */
    SC_CTOR(VpTimer) {
        // Register the documentation string with the Accellera CCI broker
        frequency_hz.set_description("Clock frequency of the timer in Hz.");
        
        SC_THREAD(timer_process);
    }
 
private:
    void timer_process() {
        wait(10, sc_core::SC_NS); // Dummy logic for compilation
        irq_out.write(true);
    }
};
 
/** @} */ // End vp_timer group
 
int sc_main(int argc, char* argv[]) {
    // Instantiate the documented IP block
    sc_core::sc_signal<bool> irq_sig{"irq_sig"};
    VpTimer timer("my_timer");
    timer.irq_out(irq_sig);
 
    // Query the CCI Broker to demonstrate runtime metadata extraction
    cci::cci_broker_handle broker = cci::cci_get_broker();
    cci::cci_param_handle param = broker.get_param_handle("my_timer.frequency_hz");
    
    std::cout << "Runtime Param Description: " << param.get_description() << "\n";
 
    std::cout << "Starting Simulation of Documented IP...\n";
    sc_core::sc_start(1, sc_core::SC_US);
    
    return 0;
}

Explanation of the Execution

Runtime Param Description: Clock frequency of the timer in Hz.
Starting Simulation of Documented IP...

While running this code executes the simulation logic, running the doxygen tool against this source file will generate a professional HTML manual.

By combining static Doxygen comments with runtime cci_broker_if metadata (via set_description()), your models become fully introspectable. A firmware engineer can read the generated HTML to find that REG_ACK = 0x08, while an automated VP configuration tool can query the cci_broker_if at runtime to generate a GUI tooltip for the frequency_hz parameter.

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