Chapter 9: SystemC CCI

CCI Parameter Handles

Safely accessing and manipulating CCI parameters from outside their owning modules using parameter handles.

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 Parameter Handles

In SystemC, modules encapsulate their internal state. According to standard C++ object-oriented practices, a cci::cci_param is typically declared as a private or protected member of an sc_core::sc_module.

If it is private, how does a top-level configuration script, a GUI tool, or a testbench modify its value? Passing bare pointers to parameters breaks encapsulation and introduces dangerous dangling-pointer risks if the module is destroyed dynamically.

The IEEE 1666.1 CCI API solves this using Parameter Handles.

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?

What is a Parameter Handle?

A parameter handle acts as a safe, managed proxy to the underlying parameter instance. It provides almost all the functionality of the parameter itself (reading, writing, querying metadata, registering callbacks), but its lifecycle is explicitly decoupled from the parameter object.

If the parameter is destroyed, all handles to it are immediately marked invalid. Attempting to use an invalid handle will trigger a safe CCI error instead of a fatal C++ segmentation fault.

Getting a Handle from the Broker

To interact with a parameter you do not own, you query the broker using the parameter's string-based hierarchical name. The broker returns a cci_param_untyped_handle. If the parameter does not exist, the handle is simply returned in an invalid state.

Typed vs Untyped Handles

  • Untyped Handles (cci_param_untyped_handle): Excellent for generic tools (JSON parsers, GUIs) that operate purely on cci_value variants.
  • Typed Handles (cci_param_typed_handle<T>): If your testbench knows the C++ data type of the parameter, you can ask for a typed handle. This bypasses variant conversions, provides compile-time safety, and allows for native C++ assignment.

Under the Hood: Handles and RTTI Casting

In the Accellera CCI implementation, a parameter handle is essentially a smart-pointer wrapper around a raw cci_param_if*. When a parameter is destroyed, the central broker actively nullifies or invalidates its internal registry entries, allowing handles to securely report is_valid() == false rather than dereferencing dangling memory.

When you request a typed handle via broker.get_param_handle<T>("path"), the broker first looks up the untyped cci_param_if* by string name. To convert it into a typed handle, the C++ library performs a dynamic_cast< cci_param_typed<T>* >(param_if).

If the user requests <int> but the underlying parameter was instantiated as <bool>, the dynamic_cast safely fails (returns nullptr), and the broker explicitly returns an invalid handle. This Run-Time Type Information (RTTI) boundary guarantees that memory corruption cannot occur from mismatched type interpretations across generic APIs.

Complete Example: Modifying Private Parameters via Handles

The following complete example demonstrates a module with private parameters and a top-level configuration function that safely manipulates those parameters using both typed and untyped handles.

#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <vector>
 
// 1. IP Block with strictly private configuration
class CacheIP : public sc_core::sc_module {
private:
    cci::cci_param<int> cache_size;
    cci::cci_param<bool> write_through;
 
public:
    SC_HAS_PROCESS(CacheIP);
    CacheIP(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , cache_size("cache_size", 256, "Size in KB")
        , write_through("write_through", false, "Write policy")
    {}
 
    void print_state() {
        std::cout << "[CacheIP] " << name() << " State:\n"
                  << "  size = " << cache_size.get_value() << " KB\n"
                  << "  write_through = " << (write_through.get_value() ? "ON" : "OFF") << "\n";
    }
};
 
// 2. An external configuration function (simulating a testbench or Python script bridge)
void run_external_configuration() {
    std::cout << "\n--- External Configuration Phase ---\n";
    cci::cci_broker_handle broker = cci::cci_get_broker();
 
    // A. Request an UNTYPED handle (e.g. if we were a generic JSON tool)
    cci::cci_param_untyped_handle h_wt = broker.get_param_handle("top.l1_cache.write_through");
    
    if (h_wt.is_valid()) {
        // Must use cci_value for untyped writes
        h_wt.set_cci_value(cci::cci_value(true));
        std::cout << "Successfully updated 'write_through' via untyped handle.\n";
    } else {
        std::cout << "Parameter 'write_through' not found.\n";
    }
 
    // B. Request a TYPED handle (e.g. if we are a C++ testbench that knows the type)
    cci::cci_param_typed_handle<int> h_size = broker.get_param_handle<int>("top.l1_cache.cache_size");
    
    if (h_size.is_valid()) {
        // Native C++ assignment works directly on typed handles!
        h_size = 1024;
        std::cout << "Successfully updated 'cache_size' to " << h_size.get_value() 
                  << " via typed handle.\n";
    } else {
        std::cout << "Parameter 'cache_size' missing or type mismatch.\n";
    }
}
 
// 3. Bulk Retrieval Example
void dump_all_parameters() {
    std::cout << "\n--- Global Parameter Dump ---\n";
    cci::cci_broker_handle broker = cci::cci_get_broker();
    
    // Retrieve ALL parameter handles in the system
    std::vector<cci::cci_param_untyped_handle> all_params = broker.get_param_handles();
    
    for (auto handle : all_params) {
        std::cout << "Param: " << handle.name() 
                  << " = " << handle.get_cci_value().to_json() << "\n";
    }
}
 
int sc_main(int argc, char* argv[]) {
    // Register the broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
 
    // Instantiate the module wrapper
    struct Top : public sc_core::sc_module {
        CacheIP l1_cache;
        Top(sc_core::sc_module_name n) : sc_core::sc_module(n), l1_cache("l1_cache") {}
    };
    Top top("top");
 
    // Print initial state
    std::cout << "Before Configuration:\n";
    top.l1_cache.print_state();
 
    // Manipulate private parameters using handles
    run_external_configuration();
    
    // Dump everything
    dump_all_parameters();
 
    // Print final state to prove it worked
    std::cout << "\nAfter Configuration:\n";
    top.l1_cache.print_state();
 
    return 0;
}

[!WARNING] Generating a handle for every single parameter in a massive SoC via get_param_handles() can be computationally expensive and consume memory. If you only need to inspect a subset of parameters, you should use the predicate-filtering overloaded version of get_param_handles() provided by the LRM.

Best Practices for the Modeler

To effectively utilize the CCI standard in your virtual platforms:

  1. Keep Parameters Private: Declare cci_param instances as private or protected in your sc_module. Force external modification to legitimately go through the broker and handles.
  2. Prefer Typed Handles in C++: In hardcoded testbenches, use cci_param_typed_handle<T> to avoid cci_value variant-conversion overhead and strictly catch type mismatches at compile time.
  3. Always Check Validity: A typo in a hierarchical string means get_param_handle("...") will return an invalid handle. Always check .is_valid() before dereferencing or setting a value to prevent exceptions.

Comments and Corrections