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:
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.
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:
- It iterates through the
m_pre_write_callbacksvector. - If any pre-write callback returns
false, the iteration immediately halts. The parameter aborts the update, leaves the underlyingm_valueunchanged, and invokesSC_REPORT_ERROR(which usually throws a C++ exception unless configured otherwise). - If all pre-write callbacks return
true, the parameter executesm_value = new_value. - Finally, it iterates through the
m_post_write_callbacksvector, 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