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:
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.register_post_read_callback: Invoked after the value is read, just before it is returned to the caller.register_pre_write_callback: Invoked before the new value is written to the parameter. This callback acts as a validator. If the callback returnsfalse, the write is rejected and acci_set_param_failureexception is thrown.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