Chapter 9: SystemC CCI

CCI Mutability, Locking, and Lifecycle

Elaboration-only parameters, simulation-time updates, locks, callbacks, destruction, and safe configuration windows.

How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

CCI Mutability, Locking, and Lifecycle

Not every parameter should be changeable at any time during simulation. The IEEE 1666.1 CCI standard gives you a specific vocabulary and API to enforce mutability rules, protecting model invariants from unsafe runtime modifications.

Source and LRM Trail

For CCI, start with Docs/LRMs/SystemC_CCI_1_0_LRM.pdf. Then inspect .codex-src/cci/configuration/src/cci for cci_param, cci_broker_if, cci_value, originators, callbacks, and the consuming broker. The practical question is always: who owns this value, when may it change, and how can tools inspect it?

Mutability Categories

A parameter can be intended for elaboration-time configuration (structural) or simulation-time control (dynamic).

Structural Parameters (Elaboration-Time only):

  • RAM size, address widths, or array dimensions.
  • If these change after the sc_core::sc_start() phase begins, memory reallocation might corrupt pointers, or TLM sockets might become invalid.

Dynamic Parameters (Simulation-Time):

  • Trace enables, log verbosity levels.
  • Throttling delays or error-injection flags.

Enforcing Immutability and Locks

If you do not have a robust mechanism to handle a parameter changing dynamically, you must lock it.

  1. CCI_IMMUTABLE_PARAM: Passed as a template argument during instantiation, making the parameter permanently unchangeable after its initial constructor/preset evaluation.
  2. .lock(void* pwd): Dynamically locks the parameter at runtime. The parameter cannot be modified unless the caller provides the exact same password pointer (pwd) to .unlock(void* pwd).
  3. .is_locked(): Allows tools to query if a parameter is currently accepting changes.

Complete Example: Lifecycle and Safe Configuration Windows

A robust Virtual Platform often implements a "Safe Configuration Window." During this window (typically end_of_elaboration), the top-level module verifies configurations and locks down all structural parameters.

#include <systemc>
#include <cci_configuration>
#include <iostream>
 
class MemoryController : public sc_core::sc_module {
public:
    // Structural parameter: must be locked before simulation starts.
    cci::cci_param<int> memory_size;
    
    // Dynamic parameter: can be changed during simulation.
    cci::cci_param<bool> enable_debug;
 
    SC_HAS_PROCESS(MemoryController);
    MemoryController(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , memory_size("memory_size", 1024)
        , enable_debug("enable_debug", false)
    {
        SC_THREAD(run_controller);
    }
 
    void run_controller() {
        while(true) {
            wait(10, sc_core::SC_NS);
            if (enable_debug.get_value()) {
                std::cout << "[MemCtrl] Debug tick at " << sc_core::sc_time_stamp() << "\n";
            }
        }
    }
};
 
class PlatformTop : public sc_core::sc_module {
public:
    MemoryController mem_ctrl;
 
    PlatformTop(sc_core::sc_module_name name) : sc_core::sc_module(name), mem_ctrl("mem_ctrl") {}
 
    // Safe Configuration Window: Lock structural parameters before simulation
    void end_of_elaboration() override {
        std::cout << "--- End of Elaboration: Locking Structural Parameters ---\n";
        
        // We lock 'memory_size' using 'this' as the password pointer.
        mem_ctrl.memory_size.lock(this);
        
        if (mem_ctrl.memory_size.is_locked()) {
            std::cout << "Success: " << mem_ctrl.memory_size.name() << " is locked.\n";
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    // 1. Setup Broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();
 
    // 2. Set Presets
    broker.set_preset_cci_value("top.mem_ctrl.memory_size", cci::cci_value(2048));
 
    // 3. Instantiate Platform
    PlatformTop top("top");
 
    // 4. Start simulation (Triggers end_of_elaboration, locking memory_size)
    sc_core::sc_start(15, sc_core::SC_NS);
 
    std::cout << "\n--- Attempting Runtime Configuration ---\n";
    
    // 5. Modifying an unlocked dynamic parameter (Succeeds)
    std::cout << "Enabling debug...\n";
    top.mem_ctrl.enable_debug.set_value(true);
 
    // 6. Attempting to modify a locked structural parameter (Fails)
    try {
        std::cout << "Attempting to change memory_size...\n";
        // This will throw cci_set_param_failure because it is locked!
        top.mem_ctrl.memory_size.set_value(4096);
    } catch (const std::exception& e) {
        std::cout << "Caught Expected Exception: " << e.what() << "\n";
    }
 
    // 7. Modifying a locked parameter with the CORRECT password (Succeeds)
    // (Usually this is only done internally by the owner, but shown here for completeness)
    top.mem_ctrl.memory_size.unlock(&top);
    top.mem_ctrl.memory_size.set_value(4096);
    top.mem_ctrl.memory_size.lock(&top);
    std::cout << "Successfully updated memory_size using correct password.\n\n";
 
    sc_core::sc_start(20, sc_core::SC_NS);
 
    return 0;
}

Destruction Lifecycle

What happens to a parameter when its owning sc_module is dynamically destroyed? The CCI standard dictates that when a cci_param is destroyed:

  1. It unregisters itself from its managing broker.
  2. It immediately invalidates all outstanding cci_param_handle proxies.
  3. It safely invalidates all registered callbacks.

This lifecycle management guarantees that a long-running external tool (like a GUI) will not crash via segmentation fault if it attempts to query a handle for a hardware block that was hot-unplugged and deleted from the simulation.

Under the Hood: Safe Handle Invalidation

In C++, ensuring a proxy object does not dereference a dangling pointer after the target is destroyed is a classic problem, typically solved using std::weak_ptr and std::shared_ptr. However, because CCI mandates strict performance and binary compatibility, the Accellera reference implementation achieves this manually.

When a cci_param executes its destructor, it calls m_broker->remove_param(this). The broker actively locates the parameter in its internal m_params registry and erases it. More importantly, the internal cci_param_impl object maintains a list of all active cci_param_handle proxies that currently point to it. During destruction, the parameter iterates through this list, explicitly reaching into each handle and setting its internal m_param_impl pointer to nullptr.

Because a handle always checks if (m_param_impl != nullptr) inside is_valid() before forwarding method calls, this active-invalidation architecture guarantees that an external GUI trying to access a hot-unplugged parameter will gracefully receive an invalid response, rather than triggering a fatal C++ segmentation fault.

Comments and Corrections