Chapter 9: SystemC CCI

CCI Parameter Callbacks

How to use CCI parameter callbacks to monitor and validate configuration changes dynamically.

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 Callbacks

Configuration parameters in a SystemC SoC model rarely exist in isolation. Changing a base address parameter might require the model to re-map its memory sockets. Changing a clock speed might require re-calculating internal delay quantums.

To support dynamic behavior driven by configuration changes, the IEEE 1666.1 CCI API provides a comprehensive Callback system.

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?

The Four Callback Phases

Callables can be registered against four distinct stages of parameter access:

  1. register_pre_read_callback: Invoked before the parameter's value is actually read. This can be used to lazily evaluate or update a parameter right before an external tool inspects it.
  2. register_post_read_callback: Invoked after the value is read, just before it is returned to the caller.
  3. register_pre_write_callback: Invoked before the new value is written to the parameter. This callback acts as a validator. If the callback returns false, the write is rejected and a cci_set_param_failure exception is thrown.
  4. register_post_write_callback: Invoked after the parameter successfully updates its value. Used for side-effects (e.g., updating internal state).

Typed vs Untyped Callbacks

If you know the underlying data type of the parameter, you can register a typed callback. The callback function receives an event object (e.g., cci::cci_param_write_event<T>) containing typed references to the values.

If you are writing a generic tool (like a logger or GUI) that does not know the parameter's type, you can register an untyped callback. It receives cci::cci_param_write_event<void>, and provides the old and new values as cci::cci_value variants.

Under the Hood: Callback Execution Vectors

In the Accellera implementation, callbacks are structurally maintained as std::vector lists of functors (often backed by std::function or SFINAE-bound member function pointers) inside the cci_param_impl base class.

When set_value() is called, the parameter executes the following sequence:

  1. It iterates through the m_pre_write_callbacks vector.
  2. If any pre-write callback returns false, the iteration immediately halts. The parameter aborts the update, leaves the underlying m_value unchanged, and invokes SC_REPORT_ERROR (which usually throws a C++ exception unless configured otherwise).
  3. If all pre-write callbacks return true, the parameter executes m_value = new_value.
  4. Finally, it iterates through the m_post_write_callbacks vector, passing the event payload.

Because set_value() blocks until all callbacks return, you must avoid calling wait() inside a callback. Callbacks execute sequentially in the context of the thread that initiated the set_value().

Complete Example: Validation and Side-Effects

The following complete, compilable example demonstrates how a Timer peripheral uses a pre-write callback to reject invalid clock frequencies, and a post-write callback to recalculate its internal delay tick rate. It also shows a global untyped callback used for generic logging.

#include <systemc>
#include <cci_configuration>
#include <iostream>
 
// A generic untyped logger for any parameter modification
void global_logger_callback(const cci::cci_param_write_event<void>& ev) {
    std::cout << "[Logger] Parameter '" << ev.param_handle.name() 
              << "' changed from " << ev.old_value.to_json() 
              << " to " << ev.new_value.to_json() 
              << " (Originator: " << ev.originator.name() << ")\n";
}
 
class TimerPeripheral : public sc_core::sc_module {
public:
    cci::cci_param<int> frequency_hz;
    sc_core::sc_time tick_period;
 
    SC_HAS_PROCESS(TimerPeripheral);
    TimerPeripheral(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , frequency_hz("frequency_hz", 1000) 
    {
        // 1. Register Typed Pre-Write Callback (Validator)
        // Returns true if valid, false to reject the write.
        frequency_hz.register_pre_write_callback(
            &TimerPeripheral::validate_frequency, this
        );
 
        // 2. Register Typed Post-Write Callback (Side-effects)
        // Using a C++11 Lambda for brevity.
        frequency_hz.register_post_write_callback(
            [this](const cci::cci_param_write_event<int>& ev) {
                this->recalculate_period(ev.new_value);
            }
        );
 
        // Initial calculation
        recalculate_period(frequency_hz.get_value());
        
        SC_THREAD(timer_thread);
    }
 
private:
    bool validate_frequency(const cci::cci_param_write_event<int>& ev) {
        if (ev.new_value <= 0) {
            std::cerr << "[Timer] Error: Frequency must be > 0. Rejected value: " 
                      << ev.new_value << "\n";
            return false; // Reject
        }
        return true; // Accept
    }
 
    void recalculate_period(int freq) {
        tick_period = sc_core::sc_time(1.0 / freq, sc_core::SC_SEC);
        std::cout << "[Timer] Tick period updated to " << tick_period << "\n";
    }
 
    void timer_thread() {
        while(true) {
            wait(tick_period);
            // Timer tick logic would go here
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Register global broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();
 
    // Instantiate Module
    TimerPeripheral timer("timer");
 
    // 3. Register Untyped Callback dynamically
    // We request a handle to the parameter and register the generic logger
    cci::cci_param_untyped_handle untyped_h = broker.get_param_handle("timer.frequency_hz");
    if (untyped_h.is_valid()) {
        untyped_h.register_post_write_callback(&global_logger_callback);
    }
 
    // Start simulation
    sc_core::sc_start(1, sc_core::SC_MS); // Advance 1ms
 
    std::cout << "\n--- Triggering Valid Change ---\n";
    // This will trigger both the post-write lambda and the global logger
    timer.frequency_hz.set_value(2000); 
 
    sc_core::sc_start(1, sc_core::SC_MS); // Advance another 1ms
 
    std::cout << "\n--- Triggering Invalid Change ---\n";
    // This will be rejected by the pre-write callback, throwing an exception
    try {
        timer.frequency_hz.set_value(-500); 
    } catch (const std::exception& e) {
        std::cout << "Caught CCI Exception: " << e.what() << "\n";
    }
 
    return 0;
}

Callback Lifecycle and Memory Management

When you register a callback, it returns a handle (e.g., cci::cci_callback_untyped_handle). You can use this handle to explicitly unregister the callback later if you no longer wish to monitor the parameter.

However, the CCI standard guarantees safe destruction. If the underlying parameter is destroyed (for example, its owning module is deleted dynamically, or the simulation ends), all callbacks are safely and automatically invalidated. You do not need to manually unregister callbacks in module destructors.

In the next tutorial, we will take a deeper dive into Parameter Handles and how external tools can safely manipulate parameters they do not own.

Comments and Corrections