CCI Bridge: Guidelines and Integration
Practical CCI modeler guidelines for parameter ownership, descriptions, callbacks, custom value types, and platform integration.
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 Bridge: Guidelines and Integration
The IEEE 1666.1 SystemC CCI standard is highly flexible, but its effectiveness depends heavily on models following consistent engineering guidelines. When developing Virtual Platforms or intellectual property (IP) blocks using CCI, adhering to standardized practices ensures that your models can be easily integrated, queried, and managed by top-level configuration tools.
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?
1. Keep Parameters Owned by the Hierarchy
Parameters should not be floating in global scope. They must be declared as part of the sc_core::sc_module that owns the corresponding behavior. This ensures that parameter hierarchical names (e.g., top.memory.size) logically match the model structure.
Avoid creating a giant, monolithic "configuration struct" passed down from the top level, as this defeats the distributed, decentralized nature of CCI parameters.
2. Always Provide Descriptions
Descriptions are not optional in serious, industrial platforms. Tools can extract parameter descriptions via cci_param::get_description() to generate user-facing configuration GUIs, auto-generate markdown documentation, or display help in a command-line interface. Future maintainers need to know the semantic intent of a parameter.
3. Prefer Typed Access Internally
While CCI brokers deal heavily in variant types (cci::cci_value), IP models should use strongly typed access internally for safety and performance:
// Preferred within the module:
uint64_t bytes = memory_size.get_value();Untyped access (set_cci_value(), get_cci_value()) should be strictly reserved for generic tools, JSON parsers, and infrastructure components that do not know the schema at compile time.
4. Validate Early
Validate configuration values as early as possible—typically in end_of_elaboration() or start_of_simulation()—before the simulation behavior begins depending on them. Examples of critical validations include:
- Memory size must be a power of two.
- Timing latency must be strictly greater than zero.
- Address ranges must not overlap with another target.
- A boolean feature flag combination must be legally supported by the hardware model.
5. Be Conservative with Custom Value Types
CCI can support user-defined value types, but using them requires registering custom type traits and serialization functions. If a configuration can be expressed using a simple map, list, integer, string, or boolean, strongly prefer the common built-in types. This maximizes compatibility with external tools (like JSON bridges) that only understand standard primitives.
Under the Hood: cci_utils::consuming_broker and Preset Tracking
When following the guidelines above, checking for typos via get_unconsumed_preset_values() is a critical step. In the Accellera cci repository, the cci_utils::consuming_broker implements this behavior by maintaining two primary std::map data structures:
std::map<std::string, cci_value> m_presets: Stores all preset values injected into the simulation before the modules are constructed.std::vector<std::string> m_consumed_presets: Tracks which keys fromm_presetswere actually queried during the initialization ofcci_paramobjects.
When a module constructs a cci_param (e.g., top.mem.size_bytes), the parameter asks the active cci_broker_if for its initial value. Inside consuming_broker::get_preset_cci_value(const std::string& name), the broker searches m_presets. If found, the name is pushed into m_consumed_presets.
Later, when your integration layer calls get_unconsumed_preset_values(), the broker internally computes the set difference between the keys in m_presets and the elements in m_consumed_presets, returning a filtered map of typos, orphaned parameters, or version mismatches. This explicit tracking mechanism prevents silent configuration failures.
Complete Integration Pattern Example
A platform-level configuration flow in a real Virtual Platform often looks like this:
- Register the global broker.
- Load configuration file or command-line values (simulated here via presets).
- Construct the module hierarchy (parameters consume the presets).
- Validate the parameter set (e.g., check for unconsumed presets or invalid values).
- Start the simulation.
The following complete, end-to-end sc_main demonstrates this exact integration pattern:
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <vector>
// 1. Parameter Ownership: Parameters belong to the module
class MemoryModel : public sc_core::sc_module {
public:
// 2. Descriptions: Always provided
cci::cci_param<uint64_t> size_bytes;
cci::cci_param<uint32_t> latency_ns;
SC_HAS_PROCESS(MemoryModel);
MemoryModel(sc_core::sc_module_name name)
: sc_core::sc_module(name)
, size_bytes("size_bytes", 1024 * 1024, "Memory size in bytes (must be power of two)")
, latency_ns("latency_ns", 10, "Access latency in nanoseconds (must be > 0)")
{
}
// 4. Validate Early: Check parameters before simulation runs
void end_of_elaboration() override {
uint64_t size = size_bytes.get_value(); // 3. Typed access
uint32_t latency = latency_ns.get_value();
if (latency == 0) {
SC_REPORT_FATAL("MemoryModel", "Latency must be strictly greater than 0.");
}
// Simple power-of-two check
if (size == 0 || (size & (size - 1)) != 0) {
SC_REPORT_FATAL("MemoryModel", "Memory size must be a power of two.");
}
std::cout << name() << " initialized successfully with size "
<< size << " bytes and latency " << latency << " ns.\n";
}
};
class PlatformTop : public sc_core::sc_module {
public:
MemoryModel mem;
PlatformTop(sc_core::sc_module_name name)
: sc_core::sc_module(name)
, mem("mem") // Instantiates "top.mem.size_bytes" etc.
{}
};
int sc_main(int argc, char* argv[]) {
// Step A: Register broker
cci::cci_register_broker(new cci_utils::consuming_broker("Platform_Broker"));
cci::cci_broker_handle broker = cci::cci_get_broker();
// Step B: Load configuration (e.g., from command line or JSON)
// We simulate an external tool setting presets here.
broker.set_preset_cci_value("top.mem.size_bytes", cci::cci_value(2048));
broker.set_preset_cci_value("top.mem.latency_ns", cci::cci_value(5));
// We deliberately create a typo to demonstrate validation
broker.set_preset_cci_value("top.mem.latancy_ns", cci::cci_value(20));
// Step C: Construct modules
PlatformTop top("top");
// Step D: Validate parameter set at the platform level (check unconsumed presets)
auto unconsumed = broker.get_unconsumed_preset_values();
if (!unconsumed.empty()) {
std::cout << "\n--- WARNING: Configuration Typos Detected ---\n";
for (const auto& preset : unconsumed) {
std::cout << "Unconsumed preset: " << preset.first << "\n";
}
std::cout << "---------------------------------------------\n\n";
}
// Step E: Start simulation (triggers end_of_elaboration validation internally)
sc_core::sc_start();
return 0;
}Following these guidelines ensures that your models remain highly reusable, safely configurable, and easily integrated into larger SystemC Virtual Platforms leveraging the IEEE 1666.1 standard.
Comments and Corrections