Chapter 9: SystemC CCI

CCI Brokers and Architecture

Deep dive into SystemC CCI Brokers, handling parameter registration, global vs local brokers, and preset values.

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 Brokers and Architecture

The SystemC Configuration, Control and Inspection (CCI) standard (IEEE 1666.1) introduces a powerful, standardized mechanism for configuring models. At the heart of this architecture lies the relationship between Parameters and Brokers.

If you think of a parameter as a typed variable bound to a hierarchical name, the broker is the centralized registry that manages its existence, access control, and initial configuration. It is responsible for bridging the gap between the internal SystemC module hierarchy and external tools or top-level configuration scripts.

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 Broker?

A broker (implementing cci::cci_broker_if) is an object that aggregates parameters. It provides container behaviors such as finding parameters by name, enumerating all known parameters, and managing preset values.

Rather than interacting with the raw broker interface, applications and models use a cci::cci_broker_handle. This handle acts as a proxy, safely routing requests to the underlying broker while injecting cci::cci_originator information that represents the caller.

Global vs Local Brokers

CCI defines a hierarchy of brokers that typically mirrors the sc_core::sc_module hierarchy. The LRM defines three primary layers of broker interaction:

  1. Global Broker: The ultimate fallback. There is exactly one global broker in the simulation, and it sits at the top level, above any sc_module hierarchy. It must be registered before any parameters or local brokers are created. If you fail to register a global broker before instantiating a parameter, the CCI library will enforce a default behavior, but explicit registration is strongly recommended.
  2. Local Brokers: Modules can define their own "local" brokers to hide or manage parameters private to a sub-assembly. By registering a broker to a specific module, all parameters instantiated within that module (and its children) will default to communicating with the local broker.
  3. Automatic Broker Resolution: When a parameter is created inside an sc_module, it registers with the "automatic" broker. The CCI runtime finds this broker by walking up the sc_core::sc_object hierarchy until a local broker is found. If none is found, the global broker takes responsibility.

C++ Source Code Implementation Details: cci_broker_if and cci_originator

Under the hood, every broker must implement the pure virtual interface cci_broker_if. This interface defines the core contract for parameter management:

  • add_param(cci_param_if*) and remove_param(cci_param_if*): The runtime hooks where parameters inject themselves into the broker's maps upon construction and destruction.
  • get_param_handle(name): Returns a type-erased cci_param_handle referencing the underlying cci_param_if.
  • get_preset_cci_value(name): Retrieves the cci_value JSON-like AST payload.

When using a cci_broker_handle, the handle secretly injects a cci_originator into every call routed to the cci_broker_if. The originator is a critical security and traceability mechanism in CCI. In the accellera-official/cci repository, when a cci_originator is constructed without arguments, it inspects the active simulation context via sc_core::sc_get_current_process_handle(). If a process is currently executing, the originator captures the hierarchical name of the process. If it is constructed during elaboration (e.g., in a module constructor), it captures the module's name. The broker uses this originator to determine if a specific entity has the permissions to lock or mutate a parameter.

Initializing Parameters with Presets

One of the most important jobs of a broker is to manage preset values. A preset allows an external tool, testbench, or top-level script to define the value of a parameter before the parameter even exists.

When a parameter is finally constructed by its owning module, it queries its broker. If the broker has a preset value for that parameter's hierarchical name, the parameter adopts the preset value instead of its hardcoded constructor default.

Unconsumed Presets

A common configuration error is a typo in the hierarchical name of a preset value. Because presets are stored in the broker until claimed by a newly constructed parameter, a preset with a typo will simply sit in the broker, unconsumed, while the target parameter silently falls back to its default value.

Brokers provide a way to detect this via get_unconsumed_preset_values(). This is typically checked at the end_of_elaboration() or start_of_simulation() phase to catch typos before runtime.

Complete Example: Brokers and Presets

The following complete, compilable example demonstrates how to set up the global broker, apply preset values, instantiate a module with parameters, and check for unconsumed presets (e.g., due to typos).

#include <systemc>
#include <cci_configuration>
#include <iostream>
 
// A simple module that defines CCI parameters
class MySubsystem : public sc_core::sc_module {
public:
    // CCI Parameters
    cci::cci_param<int> buffer_size;
    cci::cci_param<bool> enable_logging;
 
    SC_HAS_PROCESS(MySubsystem);
    MySubsystem(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        // Initialize parameters with default values
        , buffer_size("buffer_size", 256, "Size of the internal buffer")
        , enable_logging("enable_logging", false, "Enable verbose logging") 
    {
        SC_METHOD(print_config);
    }
 
    void print_config() {
        std::cout << "[MySubsystem] " << name() << " Configuration:\n"
                  << "  buffer_size = " << buffer_size.get_value() << "\n"
                  << "  enable_logging = " << (enable_logging.get_value() ? "true" : "false") 
                  << std::endl;
    }
};
 
int sc_main(int argc, char* argv[]) {
    // 1. Register the global broker before any modules or parameters are created.
    // cci_utils::consuming_broker is the standard implementation provided by the library.
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    
    // 2. Obtain a handle to the global broker
    cci::cci_broker_handle global_broker = cci::cci_get_broker();
 
    // 3. Set preset values for parameters that do not exist yet.
    // The hierarchical path includes the module instance name ("subsystem").
    global_broker.set_preset_cci_value("subsystem.buffer_size", cci::cci_value(1024));
    
    // We intentionally introduce a typo here to demonstrate unconsumed presets:
    global_broker.set_preset_cci_value("subsystem.enable_loging", cci::cci_value(true)); // Typo!
 
    // 4. Instantiate the module. 
    // It will consume the "subsystem.buffer_size" preset automatically.
    MySubsystem subsystem("subsystem"); 
 
    // 5. Check for unconsumed presets before starting simulation.
    // This is a crucial step for catching typos in configuration files or scripts.
    std::vector<std::pair<std::string, cci::cci_value>> unconsumed = 
        global_broker.get_unconsumed_preset_values();
        
    for (const auto& preset : unconsumed) {
        SC_REPORT_WARNING("CCI", 
            ("Unconsumed preset value detected for parameter path: " + preset.first).c_str());
    }
 
    // 6. Start simulation
    sc_core::sc_start(1, sc_core::SC_NS);
 
    return 0;
}

Explanation of the Flow

  1. cci_register_broker: Installs a consuming_broker as the global fallback.
  2. set_preset_cci_value: The broker records that if a parameter named "subsystem.buffer_size" is ever created, it should take the value 1024 instead of its hardcoded default.
  3. Instantiation: MySubsystem is instantiated. When buffer_size is constructed, it queries the broker, finds the preset, and sets its initial value to 1024.
  4. Typo Detection: The preset "subsystem.enable_loging" contains a typo. The parameter enable_logging does not match, so it uses its default (false). The typo is caught by calling get_unconsumed_preset_values() and explicitly issuing an SC_REPORT_WARNING.

In the next tutorial, we will look closer at the cci_param class itself and how parameters manage mutability and originators.

Comments and Corrections