Chapter 9: SystemC CCI

CCI Parameter Callbacks

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

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.

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.

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