This document describes the Simics C++ Device API, which is a C++ layer built on top of the Simics C API. See the Model Builder User's Guide for more information about the C API. The C++ Device API is also used when integrating SystemC models into the Simics framework. See the SystemC Library Programming Guide for more information about Simics and SystemC models.
We recommend that you use DML for writing new simulation models, but it is often necessary to port existing simulations models from a different environment to Simics. If these are written in C++ or SystemC, you can use the C++ Device API to simplify the task. For SystemC there is further support described in the SystemC Library Programming Guide.
The Simics C++ Device API is a collection of C++ functions, data types, and templates that make it easier to connect a C++ simulation model to the Simics framework. The C++ Device API is implemented as a layer on top of the Simics C API, and the detailed API documentation is found in the Simics C++ Device API Reference Manual. The source code is found in [simics]/src/devices/c++-api.
While this application note aims to cover most aspects of porting C++ device models to the Simics APIs, it is assumed that you are familiar with the Simics device modeling concepts described in the Model Builder User's Guide.
Version 2 of the Simics C++ Device API replaces the old Simics C++ Device API, which has been retroactively named as the Simics C++ Device API v1. The v1 API was developed before the C++11 standard was introduced. The v1 API also has some technical limitations; for example, no support for Simics port objects that was introduced in Simics version 6.
The v2 API utilizes C++11 and C++14 features to improve productivity. It has been designed to be easier to use and adds support for Simics port objects. To use the v2 API, your C++ compiler must support C++14, which means that the minimal GCC compiler version is 6.3 and the minimal MSVC compiler version is 2015. Later versions are typically used.
Different features have different minimal C++ language standard requirements:
| Feature | Description | Minimal C++ language standard requirement |
|---|---|---|
Register Banks | Used for modelling memory mapped device. The feature is included in a separate header file cc-modeling-api.h. See 7 for how to create and use the register banks. | C++17 |
| Other features | Included in header file cc-api.h | C++14 |
Figure 1 shows the high-level observation of the Simics C++ device API version 2. The v2 API consists of several parts. First it provides support for registration of a Simics class which connects with your C++ device. It also provides Port and Connect concept to communicate with other Simics modules. Then for inspection and checkpointing, it also includes support for Attribute. The Event concept helps to register events which are driven by the Simics scheduler. Last but not the least, a tech-preview feature to support programming registers is included.
To use the v2 API, you must set USE_CC_API = 2 in your module's Makefile and #include <simics/cc-api.h> in your C++ source files. The Simics C++ Device API is exported in the simics namespace.
The module sample-device-c++ provides source code that shows how to use the API described in this document. Use bin/project-setup --copy-module sample-device-c++ to copy the code to your Simics project for easy reference.
Before you start to connect your C++ device to the Simics API, you need to consider which the logical components of your device model are, and how they are connected to each other and the rest of the simulation environment.
In Simics, device models are implemented as separate objects that are dynamically connected to each other using the Simics configuration system, and the objects communicate using explicitly requested interfaces. This is in contrast to C++, where the objects are often aggregated at compile time or by running compiled startup code and interfaces are resolved during compilation and linking. The Model Builder User's Guide has a more complete description of the Simics object system.
Similar to C++, objects in the Simics simulator are instances of classes. However, Simics classes are not C++ classes but instead defined by Simics modules. Simics modules declare Simics classes by calling the Simics API when the module is loaded. Simics classes have attributes, interfaces, port objects, and callbacks to allocate and delete instances of the class. The C++ API provides a convenient way to express these Simics simulator concepts in C++, but there is not a 1-to-1 correspondence between C++ classes and Simics classes.
The first step is to decide how the C++ model should appear within Simics. For a simple C++ model that models a small piece of hardware, it is probably sufficient to make the entire model into a single Simics class.
But for a C++ model that consists of several components connected together, it is worth considering exposing it as several Simics classes. One reason for this is that it makes the Simics configuration more natural by creating a model that fits better with the Simics framework. And it allows for future separation of the model parts without major changes to the configurations.
Simply exposing the model as several classes will not automatically make the classes independent under the surface; they are still implemented as one conglomerate of C++ objects inside the implementation. To fully take advantage of the flexibility of the Simics configuration system, the device should be split it to discrete pieces that communicate only using Simics interfaces. By using well-known interfaces, the devices will become replaceable by newer implementations individually, and will allow experimentation in reconfiguration without having to recompile the model sources, or even having access to the source code.
This document focuses on creating Simics modules wrapping simple C++ models which define a single Simics class, but it easily extends to modules with several classes.
A Simics object attribute is used to accomplish primarily three things. The first is to specify configuration parameters when initially creating the model instance. This includes connections to other configuration objects, model parameters such as frequencies and buffer sizes, but anything is possible.
The other main purpose of attributes is to allow saving and restoring the model state to support checkpointing. This means that the complete state of the model needs to be available to the Simics configuration system as attribute values. Attributes would be read to save the state of the model, and the attributes would then be written to set the state of a new instance of the model to the same state as saved in the checkpoint.
A third use of attributes is to inspect and control the state of the model. This is usually covered by the same attributes used for checkpointing, as it is about the state of the models. Attributes are read in order to inspect the state, and attributes can also be changed (from scripts or CLI) to force changes to the state of the model during a simulation session.
Attributes should never be used to communicate simulation information between objects after instantiation. They are a model-to-simulator mechanism, not a model-to-model mechanism. Models should communicate over interfaces.
State attributes used for checkpointing and/or inspection could also be used to set the initial state of the model at setup time.
The configurable aspects of the C++ model should be available as configuration attributes. In some cases this will mean small changes to the C++ implementation.
The most common configurable parameter is probably references to other Simics configuration objects, such as interrupt targets, memory spaces, or DMA controllers. All references to other Simics simulation objects have to be provided as configuration parameters. Such references are set using object references when a Simics simulation session is set up. The model must never make any assumptions about which other objects are present in the system configuration or there names.
If the C++ model is written with compile-time configuration using the preprocessor, consider rewriting it to be dynamically reconfigurable. This will make the model more versatile, and potentially much more useful to a broader audience. The overhead of runtime checking of these kinds of configuration parameters is negligible in the vast majority of devices, especially when run in the context of full-system simulation.
For example, a parameter to decide which hardware revision to be compatible with may very well be a run-time parameter. Another option is adding configuration parameters that go outside the scope of the known hardware, by allowing buffer sizes and similar parameters to be reconfigured for experimental purposes.
To support checkpointing, the model needs to be able to collect a full description of the current model state and it needs to be able to restore the model to the checkpointed state when the Simics object is created.
To support reversible execution, the requirements are stricter. The object needs to be able to restore from a checkpointed state at any time, even when there is a previous state that must be discarded.
An existing C++ model needs to be examined to find how its simulation state is defined. If the model is not written to handle checkpointing, it may need to be updated with a way to extract and restore the state.
The model state is made available to the Simics configuration system using a number of attributes. There should be one attribute for each piece of the model state. It is a good idea to design the set of attributes for the model in a way that allows some separation of the external, checkpointable representation of the state from the implementation details. Ideally, the checkpoint format should not need to be updated when the implementation is changed, including running on a different host platform, using different C++ classes, internal representation or other structural changes. As a minimum requirement the external representation must not depend on what compiler is used, or whether the model is built for a 32-bit or 64-bit environment. Preferably it should be fully portable between platforms regardless of what CPU architecture the simulation is running on. When changes to the checkpoint format is required, Simics provides ways to still be able to read old checkpoints with updated models.
For example, if the model models a device with 16 32-bit registers with different meaning, it is preferably represented as 16 integer-valued attributes, with names that match those used in the device programming manual.
Never save a copy of the binary in-memory representation of a C++ object or struct. This is highly unportable and may break checkpoint compatibility just by recompiling the source with different compiler flags. Pointer variables obviously break in this case.
A Simics configuration consists of a number of interacting configuration objects, and the interaction between these objects is done through interfaces. The most commonly implemented interfaces are those used to simulate memory transactions between processors, devices, and memory. For more information about Simics interfaces, see the Model Builder User's Guide.
A Simics interface can be implemented either on the device model itself or on a separate port object. See Ports and Interfaces for details on how to implement a Simics interface.
A Simics C++ model can use the Simics C interface directly, but it can also use the Simics C++ interface provided by the C++ API for a more idiomatic C++ approach. There is a 1-to-1 mapping from each Simics C interface to its corresponding Simics C++ interface. For example, to use the transaction interface, include the header simics/c++/model-iface/transaction.h in your C++ source files. For a user-defined interface, a Simics C++ interface can be generated. See User-Defined Interface for instructions on creating and using a user-defined Simics C++ interface.
Due to the possible ABI (Application Binary Interface) compatibility issues, calling the Simics C++ interface directly between C++ classes is not allowed unless they are built from the same Simics module. It is recommended to use the Connect class which translates C++ method calls into C function calls as expected by the Simics API. This approach leverages the C language's stable and well-defined ABI.
A memory-mapped I/O Simics device model interacts with the memory bus using register banks. Each register bank is a separate port object. One device model can have many register banks.
The register bank processes the received transaction and passes it down to the related registers on the bank. Each register is generally used to model a hardware register. Typically some model behavior is triggered when the register is being accessed. A register can be further divided into fields on the bit level.
By using register banks, the rich Simics features are automatically enabled. The register banks can be easily inspected, traced and manipulated using Simics tools. The registers are registered as Simics attributes and automatically saved in a Simics checkpoint. See 7 for how to create and use the register banks.
For more information about how to design a model for Simics, see Model Builder User's Guide.
The first step of building a C++ simulation model for Simics is to create a Simics module. Typically, you put each Simics class in a module of its own. It is also common to have multiple closely-related classes in a single module (to simplify distribution if the classes would typically be used together anyway).
To create a Simics C++ module skeleton, you use project-setup:
See the Build Environment chapter in the Model Builder User's Guide for details on how to set up a project and creating Simics module skeletons.
When a Simics module is loaded into Simics, it should register the Simics classes within the module with the Simics core. Additionally, it can execute other initialization code for these Simics classes. For detailed instructions on registering Simics classes and running their initialization code, see Chapter 5.1.
The standard Simics memory tracking allocator is used by Simics C++ device API by default, by providing it as a custom allocator for new and delete. This is done per module. For a module to use its own custom allocators or even the default new allocator, this feature can be disabled.
To disable the feature, set the USE_CC_MEMORY_MANAGEMENT build parameter to no and rebuild the module.
A Simics class defines the properties of an object, including its attributes, interfaces, and class name. Objects are created by instantiating a Simics class and setting the required attributes.
For each Simics configuration object created, a corresponding instance of a C++ object is also created. This C++ object is an instance of a model-defined class that must inherit from the simics::ConfObject class.
The C++ class must have a constructor taking a single parameter of type ConfObjectRef which is passed on to the ConfObject constructor. The C++ class constructor is called internally when the corresponding Simics configuration object is created. The destructor of the C++ class is called internally when the corresponding Simics configuration object is deleted.
In addition of the constructor and destructor, user can override two methods finalize and objects_finalized to register additional functionality if needed. The finalize method is called when all attributes have been initialized in the object, and in all other objects that are created at the same time. This method is supposed to do any object initialization that require attribute values. The objects_finalized method is called after finalize has been called on all objects, so in this method the configuration is ready, and communication with other objects is permitted without restrictions.
#include <simics/cc-api.h>
class SampleClass : public simics::ConfObject {
public:
using ConfObject::ConfObject;
// This static method is invoked from simics::make_class
static void init_class(simics::ConfClass *cls) {
// register the class properties like attribute, port, event,
// interface and logging settings
}
};
This is the main object of the model instance, and everything goes through this. It should contain or reference anything that the model instance will need. Remember that there can be several instances of the model class, since the configuration allows the user to load multiple systems into the same simulation.
Each Simics class implemented by a module must be registered with Simics using the Simics API. In C++, registration of classes can be done in two ways.
init_local C functionOne approach is to invoke the simics::make_class function within the init_local function to register the Simics class with the Simics core.
extern "C" void init_local() {
simics::make_class<SampleClass>(
// Simics class name
"sample_device_cxx_class_with_init_class",
// short description
"sample C++ class device with init_class",
// class documentation
"This is a sample Simics device written in C++.");
}
The function parameters for simics::make_class are the name, the short_desc, and the description. It also takes an optional forth parameter kind which by default is Sim_Class_Kind_Vanilla. See the documentation of SIM_create_class for more information about these 4 parameters.
The template argument is the C++ class that should be instantiated to represent the Simics object or a port object of the Simics object. It must be derived through public inheritance from simics::ConfObject(for the Simics object) or simics::Port(for port object) as noted above.
Sometimes it is necessary to know the Simics class that is being registered. The function returns a ConfClassPtr which can be used to register attributes, interfaces, log groups and ports. In the example above, the return value is not used, and only the class is registered.
If, during registration, the class passed as template argument defines a static function init_class; that function is called during the registration. It is recommended to perform any class related registration of properties inside this static function, to improve data encapsulation. Other type of registration can be done inside init_local using the return value from make_class.
extern "C" void init_local() {
auto cls = simics::make_class<SampleClass>(
"sample_device_cxx_class_without_init_class",
"sample C++ class device without init_class",
"This is a sample Simics device written in C++.");
// use cls to register the class properties like attribute, port,
// event, interface and logging settings
}
RegisterClassWithSimicsStatic variables are initialized when the module is loaded. By defining a static variable of type RegisterClassWithSimics, you can register a Simics class automatically.
// coverity[global_init_order]
static simics::RegisterClassWithSimics<SampleClass> init {
"sample_device_cxx_class_without_init_local",
"sample C++ class device without init_local",
"This is a sample Simics device written in C++."
};
This approach only works for modules built with CMake. For modules built with GNU Make, an empty init_local function is still required.
As mentioned in Interfaces, a Simics C++ model can either use the Simics C interface directly, or use the Simics C++ interface provided by the C++ API for a more idiomatic C++ approach. For a user-defined interface, the C++ interface header can be generated from its C interface header using a helper script.
A Simics C++ model can use the Simics C interface directly. Below is an example using the Simics signal interface:
class SampleInterfaceC : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls);
bool signal_raised {false};
};
static void signal_raise(conf_object_t *obj) {
simics::from_obj<SampleInterfaceC>(obj)->signal_raised = true;
}
static void signal_lower(conf_object_t *obj) {
simics::from_obj<SampleInterfaceC>(obj)->signal_raised = false;
}
void SampleInterfaceC::init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("signal_raised", "b", "If signal is raised",
ATTR_CLS_VAR(SampleInterfaceC, signal_raised)));
static const signal_interface_t signal_interface = {
signal_raise,
signal_lower
};
SIM_REGISTER_INTERFACE(*cls, signal, &signal_interface);
}
The advantages of using the Simics C interface directly are:
The disadvantages of using the Simics C interface directly are:
SampleDeviceC class.A Simics C++ model can use the Simics C++ interfaces provided by the C++ API. The C++ device class or the port object class needs to inherit from the Simics C++ interface class and implement the required interface methods. Additionally, the interface must be registered with the ConfClass instance.
Take the Simics signal interface as an example. The C++ signal interface is declared in the simics::iface::SignalInterface class. The C++ device class needs to inherit from this class and override the two pure virtual methods.
class SampleInterface : public simics::ConfObject,
public simics::iface::SignalInterface {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls);
// simics::iface::SignalInterface
void signal_raise() override;
void signal_lower() override;
bool signal_raised {false};
};
void SampleInterface::signal_raise() {
signal_raised = true;
}
void SampleInterface::signal_lower() {
signal_raised = false;
}
The interface method declarations are straightforward. They should have the same signature as the interface methods in the Simics Reference Manual (or as shown with the Simics command api-help), except that the first C argument of type conf_object_t * is omitted, as it corresponds to the C++ class instance. The C++ override keyword ensures that the function is virtual and checked by the compiler.
The interface method is called when the Simics interface is accessed. The implementation should handle the call in a device-specific manner.
To expose the interface to other Simics configuration objects, the interface needs to be registered on the ConfClass object using the add function, similar to registering a Simics attribute:
void SampleInterface::init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("signal_raised", "b", "If signal is raised",
ATTR_CLS_VAR(SampleInterface, signal_raised)));
cls->add(simics::iface::SignalInterface::Info());
}
The only parameter is an instance of SignalInterface::Info, which provides the registry support for the signal interface.
For better data encapsulation, it is recommended to register the interface in the instance's static method init_class.
The interface info class, such as SignalInterface::Info, includes a method cstruct() that returns a pointer to a static, constant structure (simics::iface::SignalInterface::ctype). This structure contains function pointers for signal operations, which point to static functions defined in the SignalInterface class. These functions convert C interface calls to C++ interface calls, utilizing dynamic_cast for conversion. However, this may impact performance if there are numerous interface calls.
If performance is a concern, users can define a custom interface info class to redirect C interface function calls to local functions that directly invoke the C++ interface methods. Below is an example:
class SampleInterfaceWithCustomInfo : public SampleInterface {
public:
using SampleInterface::SampleInterface;
using SampleInterface::signal_raise;
using SampleInterface::signal_lower;
static void init_class(simics::ConfClass* cls);
static void signal_raise(conf_object_t *obj) {
return simics::from_obj<SampleInterfaceWithCustomInfo>(
obj)->signal_raise();
}
static void signal_lower(conf_object_t *obj) {
return simics::from_obj<SampleInterfaceWithCustomInfo>(
obj)->signal_lower();
}
class CustomSignalInfo : public simics::iface::SignalInterface::Info {
public:
const interface_t *cstruct() const override {
static constexpr simics::iface::SignalInterface::ctype funcs {
signal_raise,
signal_lower,
};
return &funcs;
}
};
};
The C++ bindings for interfaces are only available for standard Simics interfaces. To support user defined interfaces new C++ bindings in the form of C++ interface classes must be generated. These classes can either be copied and edited from existing headers, or generated by the bin/gen-cc-interface tool.
For example, sample-interface.h defines the C interface type sample_interface_t.
#if defined(__cplusplus)
extern "C" {
#endif
/* This defines a new interface type. Its corresponding C data type
will be called "sample_interface_t". */
SIM_INTERFACE(sample) {
void (*simple_method)(conf_object_t *obj, int arg);
void (*object_method)(conf_object_t *obj, conf_object_t *arg);
};
/* Use a #define like this whenever you need to use the name of the
interface type; the C compiler will then catch any typos at
compile-time. */
#define SAMPLE_INTERFACE "sample"
#if defined(__cplusplus)
}
#endif
The C interface type sample_interface_t needs to be converted to C++ interface class SampleInterface. This can be done by running the gen-cc-interface tool(for usage, see 12.1):
project> bin/gen-cc-interface modules/sample-interface/sample-interface.h
Now the c++/sample-interface.h can be included in the device model providing SampleInterface.
// This c++ file is generated from sample-interface.h by gen_cc_interface.py
#include "c++/sample-interface.h"
The same interface cannot be registered twice on a single Simics object. However, port objects can be used to expose the same interface multiple times. They can also be utilized to separate certain functionalities into distinct objects within the namespace of the parent object.
A port object is a child object that is automatically created alongside its parent. For more details about port objects, refer to the Model Builder User Guide. Port objects are intended to replace port interfaces.
A C++ configuration class that inherits from the simics::ConfObject class can be registered as a port object of another C++ configuration class. Below is an example of this use case:
// An example class derived from ConfObject, designed to be used as a port
// object for SamplePortDeviceUseConfObject
class SamplePortUseConfObject : public simics::ConfObject,
public simics::iface::SignalInterface {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::iface::SignalInterface::Info());
cls->add(simics::Attribute(
"raised", "b",
"Return if signal is raised or not",
ATTR_GETTER(SamplePortUseConfObject, raised_),
nullptr,
Sim_Attr_Pseudo));
}
// simics::iface::SignalInterface
void signal_raise() override {raised_ = true;}
void signal_lower() override {raised_ = false;}
private:
bool raised_ {false};
};
class SamplePortDeviceUseConfObject : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
auto port = simics::make_class<SamplePortUseConfObject>(
cls->name() + ".sample", "sample C++ port", "");
// Register port class with the device class
cls->add(port, "port.sample");
}
};
static simics::RegisterClassWithSimics<SamplePortDeviceUseConfObject>
// coverity[global_init_order]
init_port_use_confobject {
"sample_device_cxx_port_use_confobject",
"a C++ test device",
"No description"
};
The port class is created similarly to a device class by using the make_class function. Interfaces are added in the same way as with device classes, by invoking the add function with the relevant interface information.
Once the port class is defined, it can be registered by calling the add function on its parent class (the device class). This function requires two parameters:
While a port class can use simics::ConfObject, C++ API also provides the simics::Port template class. It provides utilities to access the parent object from the port object and the ability to retrieve the port array index. The example below demonstrates its usage:
class SamplePortDeviceUsePort : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
auto port = simics::make_class<SamplePort>(
cls->name() + ".sample", "sample C++ port", "");
port->add(simics::iface::SignalInterface::Info());
// Registers a port class with an array-like naming convention.
// Upon device creation, two port objects are instantiated with names:
// <dev_name>.port.sample[0] and <dev_name>.port.sample[1].
cls->add(port, "port.sample[2]");
cls->add(simics::Attribute("state", "i", "A value",
ATTR_CLS_VAR(SamplePortDeviceUsePort,
state)));
}
// Define a C++ port class which implements the signal interface
class SamplePort : public simics::Port<SamplePortDeviceUsePort>,
public simics::iface::SignalInterface {
public:
using Port<SamplePortDeviceUsePort>::Port;
// simics::iface::SignalInterface
void signal_raise() override;
void signal_lower() override;
};
private:
// An integer simulates the signal state, each bit represents one signal
int state {0};
};
void SamplePortDeviceUsePort::SamplePort::signal_raise() {
// method index() returns the index of the port object array
if (index() == 0) {
// method parent() returns pointer to the parent C++ class
parent()->state |= 1;
} else {
parent()->state |= 2;
}
}
void SamplePortDeviceUsePort::SamplePort::signal_lower() {
if (index() == 0) {
parent()->state &= 2;
} else {
parent()->state &= 1;
}
}
static simics::RegisterClassWithSimics<SamplePortDeviceUsePort>
// coverity[global_init_order]
init_port_use_port {
"sample_device_cxx_port_use_port",
"a C++ test device",
"No description"
};
The template class typename for simics::Port, TParent, typically represents the C++ class of the port's parent. A complete definition of TParent is necessary, so it is advisable to define the port class after the TParent class or as an inner class within TParent to ensure proper access to its private members.
Hint:
The simics::Port template defines a type alias ParentType for its template parameter TParent.
The helper method parent returns a pointer to the parent object, allowing access to the class members of its Simics parent class. Additionally, the helper method index provides the current index of this port object within the registered port array. For instance, in the example above, these methods can be used to raise different bits based on the port object's index.
It is recommended that port objects follow a designated port namespace for naming. For example, using port.sample indicates that the created port objects will be accessible as <dev_name>.port.sample.
When the name is given in an array format (e.g., port.sample[2]), it signifies that an array of port objects is being created. This allows for easy management and access to multiple port instances associated with the same device.
To call a Simics interface on another configuration object, the other object must be connected with the current object. This connection is represented by having a Connect instance that points to the other object. The instance is then used to extract the interface pointer used in the interface call.
The template based class Connect performs these two things; it takes an object reference and extracts the interfaces requested from this object. If the object passed to the Connect variable does not provide all required interfaces a Simics log message is emitted indicating which interface is missing and the set ignores the object passed.
The Connect class optionally takes a parameter of type ConnectConfig. It can be used to specify optional interfaces.
The following example shows a Connect instance named irq_dev. When passing a Simics configuration object to it, the object must implement the SimpleInterruptInterface/simple_interrupt interface and in addition optionally can implement the SignalInterface/signal interface as well.
#include <simics/cc-api.h>
#include <simics/c++/devs/signal.h>
#include <simics/c++/devs/interrupt.h>
class SampleConnect : public simics::ConfObject {
public:
using ConfObject::ConfObject;
// use the connect after all objects are finalized
void objects_finalized() override;
static void init_class(simics::ConfClass *cls);
simics::Connect<simics::iface::SimpleInterruptInterface,
simics::iface::SignalInterface> irq_dev {
simics::ConnectConfig::optional<simics::iface::SignalInterface>()
};
};
The Simics C++ interface struct is fetched through a template based function iface. The template parameter can be omitted when fetching the first interface type of the template parameter list.
void SampleConnect::objects_finalized() {
if (irq_dev) {
if (irq_dev.iface<simics::iface::SignalInterface>().get_iface()) {
irq_dev.iface<simics::iface::SignalInterface>().signal_raise();
} else {
irq_dev.iface().interrupt(0);
}
}
}
In Simics, device models are dynamically connected to each other. To facilitate this dynamic connection, the Connect instance must be registered as a Simics attribute. This allows different objects to be passed dynamically during the simulation. For more details, see the chapter Connect as attribute.
In C++ two overloaded functions must not have the same signature. When implementing interfaces that expose the same function this collision can be avoided by introducing an intermediate class that provides the implementation and then inherit from these intermediate classes as shown in the following example. Another option is to implement the interface in a port object.
extern "C" {
SIM_INTERFACE(one) {
void (*iface_fun)(conf_object_t*);
};
SIM_INTERFACE(another) {
void (*iface_fun)(conf_object_t*);
};
}
namespace simics {
namespace iface {
class OneInterface {
public:
// Function override and implemented by user
virtual void iface_fun() = 0;
};
class AnotherInterface {
public:
// Function override and implemented by user
virtual void iface_fun() = 0;
};
} // namespace iface
} // namespace simics
class ImplementOne : public simics::iface::OneInterface {
void iface_fun() override {
// This is implementation for OneInterface
}
};
class ImplementAnother : public simics::iface::AnotherInterface {
void iface_fun() override {
// This is implementation for AnotherInterface
}
};
class MethodsCollision : public simics::ConfObject,
public ImplementOne,
public ImplementAnother {
public:
explicit MethodsCollision(simics::ConfObjectRef o)
: simics::ConfObject(o) { }
};
To use the C++ device API for register banks, you must set USE_CC_MODELING_API = yes in your module's Makefile and #include <simics/cc-modeling-api.h> in your C++ source files.
Memory-mapped I/O (MMIO) devices are mapped to (associated with) an address space with an address value. The device connects the data bus to the desired device's hardware register banks. In C++, the device API for register banks makes the connection and modeling of such MMIO devices easier.
To enable the support for register banks, cc-modeling-api.h needs to be imported. This header provides several objects types similar to the DML, including bank, register and field. Bank models a hardware register bank but can also contain unmapped registers to help with the modeling. It contains a set of registers. Each register is generally used to model a hardware register. A register can be further divided into fields on the bit level. As the scope of these types represents a hierarchy, they are referred to as hierarchical objects. In addition, the C++ device class needs to be a MappableConfObject to support device level configuration. All these types are described further in the sub sections.
To model an MMIO device, the C++ device class needs to inherit from class simics::MappableConfObject instead of class simics::ConfObject. The class simics::MappableConfObject extends simics::ConfObject class with the support for device level configurations. For example, the bit order representation used for the device.
The bit fields is by default represented in the little endian bit order, i.e, bit number 0 is the least significant bit. It can be changed to big endian bit order by overriding the function big_endian_bitorder from class simics::MappableConfObject. This only affects how bits are represented but not the internal bits implementation.
An instance of the simics::MappableConfObject class maintains a one-to-one mapping between the full name of a hierarchical object and its corresponding access interface. This map is dynamically updated whenever a new hierarchical object instance is created. Hierarchical object instances can be created in various locations, such as within the constructor of base classes of the C++ device class, as class members of the C++ device class, or within the constructor body of the C++ device class.
It is permissible to create hierarchical objects with the same name multiple times; in such cases, the last created instance takes precedence based on the C++ object creation order. This feature is useful for replacing the behavior of an existing hierarchical object without modifying the original implementation, allowing for straightforward substitution. See Figure 2 for the order of the object creation flow.
Occasionally, it may be necessary to modify the behavior of the one-to-one map. For instance, you might want to prevent overwriting a hierarchical object once it has been created, even if another object with the same name is instantiated later. To accomplish this, users can utilize the write_protect_iface_maps method to control the write protect status of the interface associated with a hierarchical object.
During the simulation, the access interface for a hierarchical object can be easily retrieved from the map using its name. This is useful for cross-object access inside the device. For example, a register's access side-effect could be to update a field in some other register. If this is a common access pattern this lookup should be cached by storing the interface as a private member.
The Simics C++ API offers two approaches for modeling register banks, distinguished by how mapping and hierarchy information is provided. The first approach, referred to as "by code," involves supplying mapping information as input to class members within a class hierarchy constructed from C++ code. The second approach, known as "by data," involves providing both mapping and hierarchy information in a separate data structure.
In this approach, banks/registers/fields are modeled in C++ by declaring themselves as class members of other classes.
To simplify modeling, several C++ classes are provided. The PortBank<TBank> class facilitates the creation and addition of a bank to a port. The BankRegister<TRegister> class aids in creating and adding a register to a bank, while the RegisterField<TField> class helps create and add a field to a register. These classes use template type parameters to create different object types, defaulting to basic hierarchical object types like simics::Bank. The parameter can be any provided class, such as ReadOnlyRegister, or a user-defined class. For a complete list of provided classes, see 7.4.4.
While nesting classes for banks, registers, and fields using C++ nested class is optional, it can help reduce scope and make the code resemble a corresponding DML device.
Below is an example using this option:
class SampleBank : public simics::PortBank<> {
public:
using PortBank::PortBank;
class SampleRegister : public simics::BankRegister<> {
public:
using BankRegister::BankRegister;
class SampleField : public simics::RegisterField<> {
public:
using RegisterField::RegisterField;
// Override to print a log when being written
void write(uint64_t value, uint64_t enabled_bits) override {
SIM_LOG_INFO(3, bank_obj_ref(), 0, "Write to SampleField");
simics::Field::write(value, enabled_bits);
}
};
uint64_t read(uint64_t enabled_bits) override {
SIM_LOG_INFO(3, bank_obj_ref(), 0, "Read from SampleRegister");
return simics::Register::read(enabled_bits);
}
private:
SampleField f0 {
this, Name("f0"), Description("a sample field"),
Offset(0), BitWidth(16)
};
simics::RegisterField<> f1 {
this, Name("f1"), Description("a default field"),
Offset(16), BitWidth(16)
};
};
private:
SampleRegister r0 {
this, Name("r[0]"),
Description("A register with init value 42"),
Offset(0), ByteSize(4), InitValue(42)
};
SampleRegister r1 {
this, Name("r[1]"),
Description("A register with init value 42"),
Offset(0x10), ByteSize(4), InitValue(42)
};
};
In this option, the hierarchy of ports, banks, registers, and fields is embedded within the C++ class. Both mapping information and behavior are defined in the code during object creation, hence the term "by code." The Simics C++ API modeling extension is based on this option.
In the second approach, resource mapping information for banks, registers, and fields is provided as data using an arbitrary format. A data importer converts this data and calls the C++ device API to register the mapping, keeping resource mapping separate from behavior. This allows users to create custom generators to produce data in the required format and enables importers to read data from files, facilitating mapping changes without recompiling the model.
The API method for registering mapping is create_hierarchy_from_register_data. The first parameter is a pointer to simics::ConfClass, and the second can be either simics::bank_t or a braced-init-list of simics::bank_t. Since the API method must be invoked when the module is loaded into Simics, the most suitable place to call it is init_class. It can be invoked multiple times with different register data. The address of the bank object will be stored and used after the function call, so it must remain valid for the lifetime of the simulation.
The simics::bank_t type describes bank information, including the bank's name, description, and register information. Register information is defined using simics::register_t, which includes name, description, memory address offset, size in bytes, initialized value, and field information. Field information is defined using simics::field_t, including name, description, bit offset, and bit width.
Using the second option, a C++ model with default read/write behavior can run in a Simics simulation without user-defined behaviors. To change default behavior, users can use a standard class from the modeling library or subclass it. For a complete list of standard classes, see 7.4.4. If no customized behavior is registered, the default base class for the corresponding resource is used.
Below is an example using this option. The mapping information for SampleBankByData is registered by calling the import_data method from a data importer, which invokes create_hierarchy_from_register_data with mapping data. Each resource in the example is assigned default behavior (read and write), except for registers b[0].r[0], b[0].r[1], b[1].r[0], and b[1].r[1], defined as custom write-clear registers.
#include <simics/cc-modeling-api.h>
#include "register-as-data.h"
class SampleRegister : public simics::Register {
public:
using Register::Register;
class SampleField : public simics::Field {
public:
using Field::Field;
void write(uint64_t value, uint64_t enabled_bits) override {
SIM_LOG_INFO(3, bank_obj_ref(), 0, "Write to SampleField");
simics::Field::write(value, enabled_bits);
}
};
uint64_t read(uint64_t enabled_bits) override {
SIM_LOG_INFO(3, bank_obj_ref(), 0, "Read from SampleRegister");
return simics::Register::read(enabled_bits);
}
private:
SampleField f0 {dev_obj(), hierarchical_name() + ".f0"};
};
class DataImporter {
public:
explicit DataImporter(simics::MappableConfObject *obj)
: obj_(obj) {}
template <typename T>
static void import_data(simics::ConfClass *cls) {
simics::create_hierarchy_from_register_data<T>(cls, register_as_data);
}
private:
simics::MappableConfObject *obj_;
SampleRegister b0_r0 {obj_, "b[0].r[0]"};
SampleRegister b0_r1 {obj_, "b[0].r[1]"};
SampleRegister b1_r0 {obj_, "b[1].r[0]"};
SampleRegister b1_r1 {obj_, "b[1].r[1]"};
};
#include <simics/cc-api.h>
#include <simics/cc-modeling-api.h>
#include "data-importer.h"
class SampleBankByData : public simics::MappableConfObject,
public DataImporter {
public:
explicit SampleBankByData(simics::ConfObjectRef obj)
: MappableConfObject(obj),
DataImporter(this) {}
static void init_class(simics::ConfClass *cls) {
DataImporter::import_data<SampleBankByData>(cls);
}
};
// coverity[global_init_order]
static simics::RegisterClassWithSimics<SampleBankByData> init_bank_by_data {
"sample_device_cxx_bank_by_data",
"sample C++ device with a bank by data",
"Sample C++ device with a bank by data"
};
In Simics, the device model is instantiated before port objects, so hierarchical objects defined in the device model are instantiated first. Hierarchical objects with the same name can be instantiated multiple times, with behavior defined by the last instantiation based on C++ object instantiation order.
Default behavior hierarchical objects are instantiated during port object instantiation if no behavior is already defined for them. They are allocated on the heap memory.
A device can have multiple bank ports, each containing a single bank, which can have multiple registers. Registers are accessible to the simulated system through the Simics transaction interface and are exposed to scripting and user interfaces via the register_view, register_view_read_only, and bank_instrumentation_subscribe Simics interfaces.
The simics::BankPort template class is used to model the bank port object. It inherits from simics::Port and implements the necessary Simics bank interfaces. The template type parameter should be set to the C++ device class. If the bank port is not accessing any class member from the C++ device class, then the template type can be simics::MappableConfObject.
As outlined in the previous section, there are two approaches to modeling register banks. The simics::BankPort template class provides two constructors corresponding to each approach. For the "by-code" option, the constructor requires only a ConfObjectRef parameter, which represents the Simics configuration port object. For the "by-data" option, in addition to the required ConfObjectRef parameter, the constructor accepts a second parameter to provide bank mapping information.
The make_bank_port function facilitates the creation of a Simics port class. The port class is then registered with its parent ConfClass using the add function, typically within the init_class function of the device model class.
Simics attributes can be directly registered on the bank port, which is useful for storing the state of the bank, registers, or fields. Bank attributes are automatically saved to the Simics checkpoint by write-configuration. To register an attribute, simply add an Attribute instance (see 8 for more details).
Banks can also be organized into arrays, with each element in the bank array representing a separate configuration object in Simics. This allows for individual mapping in a memory space. For details on registering a bank array, see Port.
Instead of defining a custom BankPort class that inherits from simics::BankPort, users can directly utilize the existing simics::SimpleBankPort class to create a bank port. This class is a template class that accepts the type of the port bank as a template parameter. A port bank of the specified type is instantiated as a member of the SimpleBankPort. Additionally, extra arguments can be provided to the bank through a second template parameter. Below is an example of how to use this in code:
void SampleBankByCode::init_class(simics::ConfClass *cls) {
cls->add(simics::make_bank_port<simics::SimpleBankPort<SampleBank>>(
cls->name() + ".SampleBank", "sample bank"), "bank.b[2]");
}
There are 3 kinds of hierarchical objects: bank, register and field. These concepts are the same as the concepts used in the DML. The class hierarchy is shown in 3. The generic HierarchicalObject class serves as the abstract base class.
A hierarchical object is instantiated with a pointer to a MappableConfObject instance and a unique name. As described in the previous section, the MappableConfObject instance maintains a map from the hierarchical object's name to its access interface.
All hierarchical objects in a device should have a unique name that begins with the bank's name. The name should consist of a sequence of characters from the character set [A–Z][a–z][0–9], underscore(_), square brackets ([]) and dot (.). Square brackets should only be used to represent items in an array. A dot (.) can only be used between different hierarchical levels. For example, following names are invalid to use as a hierarchical object's name:
.bank_x
bank[1].reg2.
_x.._y
*.reg_?.+
Bank is a hierarchical object that implements the simics::BankInterface interface. Its main role is dispatching the incoming transaction access to the corresponding registers on the bank. The entry point for the transaction access is the function transaction_access.
Simics configuration objects for bank instances are named like the bank but with a bank prefix. For instance, if a device class has added a bank with declaration bank.regs[2], and a device instance is named dev in Simics, then the two banks are represented in Simics by configuration objects named dev.bank.regs[0] and dev.bank.regs[1].
A register is an object that contains an integer value. Typically, a register corresponds to a segment of consecutive locations in the address space of the bank; however, it is also possible (and often useful) to have registers that are not mapped to any address within the bank. All registers must be part of a register bank.
The following information is needed to map a register to the address space of the enclosing bank:
The default Register class provides methods for reading, writing, setting, and getting register values. These methods interact with the fields contained within the register, invoking corresponding methods on each field.
set(uint64_t value): Sets the register value by invoking the set method on each field within the register. This ensures that each field is updated according to its specific bit range.uint64_t get(): Retrieves the register value by invoking the get method on each field and combining their values. This provides a complete view of the register's current state.write(uint64_t value, uint64_t enabled_bits): Writes the specified bits of value by invoking the write method on each field. This may trigger side-effects specific to each field. The enabled_bits argument specifies which bits of the register are accessed.uint64_t read(uint64_t enabled_bits): Reads the register value by invoking the read method on each field, potentially triggering side-effects. The enabled_bits argument specifies which bits of the register are accessed.To define an array of registers, similar to a C array, specify the number of registers within square brackets, e.g., r[8]. By default, the stride of the array is the size of the register, but it can be customized, e.g., r[8 stride 4]. For multidimensional arrays, the default stride is calculated based on the size of all registers in the inner dimension. For instance, the stride of the outermost dimension for r[2][8 stride 4] is calculated as 8 * 4 = 32 bytes.
For every register, an attribute of integer type is automatically added to the containing bank. The name of the register is used as the name of the attribute; e.g., a register named r1 will get a corresponding attribute on the bank named r1. The register value is automatically saved when Simics creates a checkpoint.
An important thing to note is that registers do not have to be mapped at all. This may be useful for internal registers that are not directly accessible from software. By using an unmapped register, you can get the advantages of using register, such as automatic checkpointing and register fields. This internal register can then be used from the implementations of other registers, or other parts of the model. For simply storing state, consider using bank port attributes instead.
Please note that register_view interface and breakpoints only work on mapped registers.
To create an unmapped register, simply instantiate a register of type UnmappedRegister (or a subtype of it).
Real hardware registers often consist of multiple fields, each with a distinct meaning. For example, the lowest three bits of a register might represent a status code, the next six bits could be a set of flags, and the remaining bits might be reserved.
To facilitate this representation, a register object can contain multiple field objects, with each field corresponding to a specific bit range within the enclosing register. The value of a field is stored in the corresponding bits of the register's storage.
The default Field class provides methods for reading, writing, setting, and getting field values. Typically, one could inherit and override one or more of these methods—read, write, get, and set—to customize the behavior of the field according to specific requirements.
The default behaviour of these methods are:
set(uint64_t value): Sets the field value without triggering any side-effects. This method directly updates the field's value.uint64_t get(): Retrieves the field value without triggering any side-effects. This method directly returns the field's value.write(uint64_t value, uint64_t enabled_bits): Writes the specified bits of value, potentially triggering side-effects. It uses the get method to form the new value to be set using the set method. The enabled_bits argument specifies which bits of the field are accessed.uint64_t read(uint64_t enabled_bits): Reads the field value, potentially triggering side-effects. It uses the get method to obtain the underlying value. The enabled_bits argument specifies which bits of the field are accessed.It's important that set and get methods are guaranteed to be side-effect free. Any side effects should be handled by the read or write methods.
To define an array of fields, similar to a C array, specify the number of fields within square brackets, e.g., f[8]. By default, the stride of the array is the size of the field, but it can be customized, e.g., f[8 stride 4]. For multidimensional arrays, the default stride is calculated based on the size of all fields in the inner dimension. For instance, the stride of the outermost dimension for f[2][8 stride 4] is 8 * 4 = 32 bits.
This chapter describes the standard templates for C++ registers and fields.
Note that many standard templates have the same functionality and only differ by name or log-messages printed when writing or reading them. The name of the template helps developers to get a quick overview of the device functionality. Two such examples are the undocumented and reserved templates. Both have the same functionality. However, the undocumented template hints that something in the device documentation is unclear or missing, and the reserved template that the register or field should not be used by software.
Software reads and writes are defined as accesses using the transaction interface (write/reads to memory/io mapped device). Software reads and writes use the built-in read and write methods. Hardware access is defined as access made from within the model itself, using either set/get for side-effect free access or read/write for an access with side-effects.
The default class Bank models a bank of little endian byte order. To model a big endian byte order bank, the class BigEndianBank can be used. It does not affect the internal data representation, only matters when the data is read out and presented in some format. For example, the Bank method read returns a vector of uint8_t. The output of this method is by default a little endian byte ordered vector of bytes, while a big endian byte order bank has the order reversed.
By default, reading an address range on a bank which is not fully mapped by registers triggers unmapped_read. It prints a spec-violation log and fail the read transaction. This behavior can be altered by using class MissPatternBank. Its constructor takes an extra third parameter called miss_pattern which is used to fill the unmapped bytes in the read transaction. With this, the function unmapped_read is not triggered, thus no spec-violation log is printed and the read transaction does not fail. This parameter defaults to zero if not set.
Typically, each bank has its own memory dedicated to storing its registers. The memory is identified by a unique ID, which is the bank's name. However, in certain cases, multiple banks can share the same memory. This functionality is supported by the SharedMemoryBank class. The third parameter of the class constructor, name_of_bank_memory, is a string that serves as the unique ID for this shared memory across different banks. By specifying a common name_of_bank_memory, multiple banks can access and manipulate the shared memory collectively.
The read and write behaviour of registers and fields is in most cases controlled by class inheritance and method overriding. The read and write provided in Register and Field is virtual and can be override by an implementation in a derived class. The default implementation can still be referenced using a explicit namespace from the base class.
The following templates are provided and most of them can be applied to both registers and fields. The section use object as a combined name for registers and fields. Most of them affect either the write or read operation; if applied on a register it will disregard fields. For instance, when inheriting from the ReadUnimplRegister class on a register with fields, then the read will ignore any implementations of read overrides in fields, and return the current register value (through get). However, writes will still propagate to the fields.
| Class name | Description | Log output |
|---|---|---|
IgnoreWriteRegister, IgnoreWriteField | Writes are ignored. This template might also be useful for read-only fields inside an otherwise writable register. See the documentation for the ReadOnlyRegister template for more information. | |
Read0Register, Read0Field | Reads return 0, regardless of register/field value. Writes are unaffected by this template. | |
ReadOnlyRegister, ReadOnlyField | The object value is read-only for software, the object value can be modified by hardware. | First software write results in a spec_violation log-message on log-level 1, remaining writes on log-level 2. Fields will only log if the written value is different from the old value. If the register containing the read-only field also contains writable fields, it may be better to use the IgnoreWriteRegister template instead, since software often do not care about what gets written to a read-only field, causing unnecessary logging. |
WriteOnlyRegister, WriteOnlyField | The register/field value can be modified by software but can't be read back, reads return 0 regardless of register/field value. Writes are unaffected by this template. | For register, the first time the object is read there is a spec_violation log-message on log-level 1, remaining reads on log-level 2. For field, only logs on log-level 4. |
Write1ClearsRegister, Write1ClearsField | Software can only clear bits. This feature is often used when hardware sets bits and software clears them to acknowledge. Software write 1's to clear bits. The new object value is a bitwise AND of the old object value and the bitwise complement of the value written by software. | |
ClearOnReadRegister, ClearOnReadField | Software reads return the object value. The object value is then reset to 0 as a side-effect of the read. | |
Write1OnlyRegister, Write1OnlyField | Software can only set bits to 1. The new object value is the bitwise OR of the old object value and the value written by software. | |
Write0OnlyRegister, Write0OnlyField | Software can only set bits to 0. The new object value is the bitwise AND of the old object value and the value written by software. | |
ReadConstantRegister, ReadConstantField | Reads return a constant value. Writes are unaffected by this template. The read value is unaffected by the value of the register or field. The template is intended for registers or fields that have a stored value that is affected by writes, but where reads disregard the stored value and return a constant value. The attribute for the register will reflect the stored value, not the value that is returned by read operations. For constant registers or fields that do not store a value, use the Constant template instead. | |
ConstantRegister, ConstantField | Writes are forbidden and have no effect. The object still has backing storage, which affects the value being read. Thus, an end-user can modify the constant value by writing to the register's attribute. Such tweaks will survive a reset. Using the Constant template marks that the object is intended to stay constant, so the model should not update the register value, and not override the read method. Use the template ReadOnly if that is desired. | First write to register or field (if field value is not equal to write value) results in a spec_violation log-message on log-level 1, remaining writes on log-level 2. |
SilentConstantRegister, SilentConstantField | The object value will remain constant. Writes are ignored and do not update the object value. The end-user can tweak the constant value; any tweaks will survive a reset. By convention, the object value should not be modified by the model; if that behaviour is wanted, use the IgnoreWrite template instead. | |
ZerosRegister, ZerosField | The object value is constant 0. Software writes are forbidden and do not update the object value. | First software write to register or field (if field value is not equal to write value) results in a spec_violation log-message on log-level 1, remaining writes on log-level 2. |
OnesRegister, OnesField | The object is constant all 1's. Software writes do not update the object value. The object value is all 1's. | First software write to register or field (if field value is not equal to write value) results in a spec_violation log-message on log-level 1, remaining writes on log-level 2. |
IgnoreRegister, IgnoreField | The object's functionality is unimportant. Reads return 0. Writes are ignored. | |
ReservedRegister, ReservedField | The object is marked reserved and should not be used by software. Writes update the object value. Reads return the object value. | First software write to register or field (if field value is not equal to write value) results in a `spec-viol` log-message on log-level 2. No logs on subsequent writes. |
UnimplRegister, UnimplField | The object functionality is unimplemented. Warn when software is using the object. Writes and reads are implemented as default writes and reads. | First read from a register results in an unimplemented log-message on log-level 1, remaining reads on log-level 3. Reads from a field does not result in a log-message. First write to a register results in an unimplemented log-message on log-level 1, remaining writes on log-level 3. First write to a field (if field value is not equal to write value) results in an unimplemented log-message on log-level 1, remaining writes on log-level 3. |
ReadUnimplRegister, ReadUnimplField | The object functionality associated to a read access is unimplemented. Write access is using default implementation and can be overridden (for instance by the ReadOnly template). | First software read to a register results in an unimplemented log-message on log-level 1, remaining reads on log-level 3. Software reads to fields does not result in a log-message. |
WriteUnimplRegister, WriteUnimplField | The object functionality associated to a write access is unimplemented. Read access is using default implementation and can be overridden (for instance by the WriteOnly template). | First software write to registers results in an unimplemented log-message on log-level 1, remaining writes on log-level 3. First write to a field (if field value is not equal to write value) results in an unimplemented log-message on log-level 1, remaining writes on log-level 3. |
SilentUnimplRegister, SilentUnimplField | The object functionality is unimplemented, but do not print a lot of log-messages when reading or writing. Writes and reads are implemented as default writes and reads. | First software read to a register results in an unimplemented log-message on log-level 2, remaining reads on log-level 3. Software reads to fields does not result in a log-message. First software write to a register results in an unimplemented log-message on log-level 2, remaining writes on log-level 3. First write to a field (if field value is not equal to write value) results in an unimplemented log-message on log-level 2, remaining writes on log-level 3. |
UndocumentedRegister, UndocumentedField | The object functionality is undocumented or poorly documented. Writes and reads are implemented as default writes and reads. | First software write and read result in a spec_violation log-message on log-level 1, remaining on log-level 2. |
UnmappedRegister | The register is excluded from the address space of the containing bank. | |
DesignLimitationRegister, DesignLimitationField | The object's functionality is not in the model's scope and has been left unimplemented as a design decision. Software and hardware writes and reads are implemented as default writes and reads. Debug registers are a prime example of when to use this template. This is different from unimplemented which is intended to be implement (if required) but is a limitation in the current model. | |
AliasRegister | The register is an alias for another register. All operations are forwarded to the other register. |
Unlike previous C++ API, there is only one way to define attributes. The attribute is defined by creating an object of type Attribute and adding it to the ConfClass instance using the add function.
There are several constructors with different sets of parameters. The following parameters must be provided for all constructors: a string name, a string type and a string doc. These parameters have the same meaning as in SIM_register_attribute. The name specifies the attribute name, and must be unique for the class and stable between revisions of the model. The type is the type which describes the data type of the attribute, and should also be stable between revisions of the model to support Simics configuration scripts and checkpointing. The macro ATTR_TYPE_STR can be used to auto generate the type string from a C++ variable. The doc describes the attribute.
Get and set callbacks can be registered for the attribute through getter and setter parameters. For a C++ class member variable, AttributeAccessor can be used for the registration.
There is an optional attr parameter which is one of Sim_Attr_Required, Sim_Attr_Optional or Sim_Attr_Pseudo. This can be used to indicate that an attribute is required (Sim_Attr_Required) or that it should not be part of checkpoints (Sim_Attr_Pseudo). The default value is Sim_Attr_Optional if no explicit value is set.
A required attribute (attr set to Sim_Attr_Required) will be initialized to the value given by the configuration. For attribute of other types the initial value must be explicitly initialized by the C++ class that owns the member pointed to by the attribute. In C++, unlike DML, the variables are not zero-initialized implicitly.
Depending on how the state variable is stored in the C++ class, there are different ways to register the attribute.
class SampleAttributeClassMemberVariable : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute(
"flags", "[bb]", "Two boolean flags in an array",
ATTR_CLS_VAR(SampleAttributeClassMemberVariable,
flags)));
}
std::array<bool, 2> flags {false, false};
};
static simics::RegisterClassWithSimics<SampleAttributeClassMemberVariable>
// coverity[global_init_order]
init_class_member_variable {
"sample_device_cxx_attribute_class_member_variable",
"sample C++ attr device use cls member variable",
"Sample C++ attribute device use cls member variable"
};
This example uses ATTR_CLS_VAR(SampleAttributeClassMemberVariable, flags) to locate the flags variable, given an instance of SampleAttributeClassMemberVariable. The macro ATTR_CLS_VAR expands to an AttributeAccessor.
A public state variable member of a C++ build-in type or a C++ standard container of build-in type can be registered directly using ATTR_CLS_VAR. For example, an array of two boolean values.
If a state variable is not in public scope, indirect access is needed. It is quite common that a C++ class provides a public get and set function for its private state variable member. To register the variable as a Simics attribute, simply wrap the public get and set function pointers using macro ATTR_GETTER and ATTR_SETTER respectively and pass them as the parameters to the Attribute constructor.
class SampleAttributeClassMemberMethod : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute(
"value", "i", "A value.",
ATTR_GETTER(SampleAttributeClassMemberMethod,
get_value),
ATTR_SETTER(SampleAttributeClassMemberMethod,
set_value),
Sim_Attr_Required));
}
int get_value() const;
void set_value(const int &v);
private:
int value {0};
};
// ...
int SampleAttributeClassMemberMethod::get_value() const {
return value;
}
void SampleAttributeClassMemberMethod::set_value(const int &v) {
if (v < 256) {
value = v;
} else {
throw std::runtime_error("Too large value");
}
}
static simics::RegisterClassWithSimics<SampleAttributeClassMemberMethod>
// coverity[global_init_order]
init_class_member_method {
"sample_device_cxx_attribute_class_member_method",
"sample C++ attr device use cls member method",
"Sample C++ attribute device use cls member method"
};
By wrapping the value with functions, it is also possible to add extra checks. The getter function simply returns the value. The setter function takes a reference to a value, and can accept or reject it by throwing a runtime_error with a string message describing why it was rejected. The exception is caught in the Simics attribute setter function and converted to a proper Simics log message.
Note the use of Sim_Attr_Required when constructing the simics::Attribute. It means a value must be provided when creating the Simics object.
The getter and setter functions can be global functions as well. Then they should take the main C++ object as a function parameter.
class SampleAttributeGlobalMethod : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls);
std::string name;
size_t id {0};
};
// ...
std::pair<std::string, size_t> get_name_and_id(
const SampleAttributeGlobalMethod &obj) {
return {obj.name, obj.id};
}
void set_name_and_id(SampleAttributeGlobalMethod &obj, // NOLINT
const std::pair<std::string, size_t> &name_and_id) {
std::tie(obj.name, obj.id) = name_and_id;
}
void SampleAttributeGlobalMethod::init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("name_and_id", "[si]", "A pair of a name and id",
ATTR_GETTER(get_name_and_id),
ATTR_SETTER(set_name_and_id)));
}
static simics::RegisterClassWithSimics<SampleAttributeGlobalMethod>
// coverity[global_init_order]
init_global_method {
"sample_device_cxx_attribute_global_method",
"sample C++ attr device use global method",
"Sample C++ attribute device use global method"
};
The same macros, ATTR_GETTER and ATTR_SETTER are used to help convert getters and setters into the functions required by the Simics attribute.
If the getter is nullptr, it means that the attribute is write-only. If the setter is nullptr, it means that it is read-only. If either is nullptr, the attribute cannot be checkpointed and if attr is left out, Sim_Attr_Pseudo is automatically chosen.
The help macros ATTR_CLS_VAR, ATTR_GETTER, ATTR_SETTER and ATTR_TYPE_STR support all C++ native build-in types or STL containers that consist of them. A complete list of equivalent types supported by the help macros are listed in the following table. The pointer and the enum are supported if the underlying type is one of the types listed in the table.
| Type category | Include types |
|---|---|
| Boolean type | bool |
| Character types | char, signed char, unsigned char |
| Integer types | short int, unsigned short int, int, unsigned int, long int, unsigned long int, long long int, unsigned long long int |
| Floating-point types | float, double |
| Object type | simics::ConfObjectRef, simics::Connect |
| Container type | array, list, vector, deque, pair, map, tuple, set |
Nested STL containers composed of the types listed in the table above are also supported. An example is provided below. For these types, the type string is often too complex, so it is recommended to use the helper macro ATTR_TYPE_STR to construct the type string.
class SampleAttributeNestedStlContainer : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute(
"id_strs",
ATTR_TYPE_STR(SampleAttributeNestedStlContainer::id_strs),
"a map where each ID maps to a list of strings",
ATTR_CLS_VAR(SampleAttributeNestedStlContainer,
id_strs)));
}
std::map<int, std::vector<std::string>> id_strs;
};
For other types not listed in table above, including user defined types, there are many ways to support that. Here two of the ways are shown, first way creating custom get and set functions and implement them as shown in the example below:
// The buffer class is used as an example to show how to register
// a custom type Simics attribute. It is not a reference
// implementation of how to write a custom buffer class.
// coverity[rule_of_three_violation:FALSE]
class buffer {
public:
buffer(const unsigned char *d, size_t size) {
aux_.assign(d, d + size);
}
buffer(const buffer &other) {
aux_.assign(other.data(), other.data() + other.size());
}
buffer& operator=(const buffer &other) = delete;
virtual ~buffer() = default;
const unsigned char *data() const { return aux_.data(); }
int size() const { return aux_.size(); }
private:
std::vector<unsigned char> aux_;
};
class SampleAttributeCustomMethod : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls);
buffer get_blob() const;
void set_blob(const buffer &v);
private:
unsigned char blob_[1024] {};
};
buffer SampleAttributeCustomMethod::get_blob() const {
return buffer(blob_, sizeof blob_);
}
void SampleAttributeCustomMethod::set_blob(const buffer &v) {
if (v.size() == sizeof blob_) {
memcpy(blob_, v.data(), v.size());
} else {
throw std::runtime_error { "Wrong size of data buffer" };
}
}
namespace {
attr_value_t get_blob_helper(conf_object_t *obj) {
auto *o = simics::from_obj<SampleAttributeCustomMethod>(obj);
return SIM_make_attr_data(1024, o->get_blob().data());
}
set_error_t set_blob_helper(conf_object_t *obj, attr_value_t *val) {
auto *o = simics::from_obj<SampleAttributeCustomMethod>(obj);
try {
o->set_blob(buffer(SIM_attr_data(*val), SIM_attr_data_size(*val)));
} catch (const std::exception &e) {
SIM_LOG_INFO(1, o->obj(), 0, "%s", e.what());
return Sim_Set_Illegal_Value;
}
return Sim_Set_Ok;
}
} // namespace
void SampleAttributeCustomMethod::init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("blob", "d", "Some data",
&get_blob_helper, &set_blob_helper));
}
static simics::RegisterClassWithSimics<SampleAttributeCustomMethod>
// coverity[global_init_order]
init_custom_method {
"sample_device_cxx_attribute_custom_method",
"sample C++ attr device use custom method",
"Sample C++ attribute device use custom method"
};
Besides the above way, it is also possible to specialize the converters to/from the user defined type. An example is shown below:
struct MyType {
uint64_t ull;
std::string message;
simics::ConfObjectRef some_object;
};
class SampleAttributeSpecializedConverter : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("my_type", "[iso|n]",
"An attribute of MyType",
ATTR_CLS_VAR(
SampleAttributeSpecializedConverter,
my_type)));
}
MyType my_type;
};
// Specialize the template converter
namespace simics {
template <>
inline MyType attr_to_std<MyType>(attr_value_t src) {
MyType result;
result.ull = attr_to_std<uint64>(SIM_attr_list_item(src, 0));
result.message = attr_to_std<std::string>(SIM_attr_list_item(src, 1));
result.some_object = attr_to_std<ConfObjectRef>(SIM_attr_list_item(src, 2));
return result;
}
template <>
inline attr_value_t std_to_attr<MyType>(const MyType &src) {
attr_value_t result = SIM_alloc_attr_list(3);
SIM_attr_list_set_item(&result, 0, std_to_attr<uint64>(src.ull));
SIM_attr_list_set_item(&result, 1, std_to_attr<std::string>(src.message));
SIM_attr_list_set_item(&result, 2,
std_to_attr<ConfObjectRef>(src.some_object));
return result;
}
} // namespace simics
static simics::RegisterClassWithSimics<SampleAttributeSpecializedConverter>
// coverity[global_init_order]
init_specialized_converter {
"sample_device_cxx_attribute_specialized_converter",
"sample C++ attr device with specialized converter",
"Sample C++ attribute device with specialized converter"
};
Registering a Connect variable as an attribute is done the same way as any other variable. User can use the help macros described above. The following code will create an irq_dev attribute that uses the Connect instance shown in Connect as a value.
void SampleConnect::init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("irq_dev", "o|n",
"IRQ device",
ATTR_CLS_VAR(SampleConnect, irq_dev)));
}
The template based class ConnectToDescendant helps to set the connect attribute by default to a automatically created object. It is similar to DML's connect template init_as_subobj. To achieve this, user needs to first register a port object and pass the name of it to the CTOR of class ConnectToDescendant.
class SampleConnectToDescendant : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static constexpr const char * PORT_MEMORY_SPACE = "port.memory_space";
static void init_class(simics::ConfClass *cls);
simics::ConnectToDescendant<
simics::iface::MemorySpaceInterface> target_mem_space {
this, PORT_MEMORY_SPACE
};
};
void SampleConnectToDescendant::init_class(simics::ConfClass *cls) {
// Register the port object as default target memory space
SIM_register_port(*cls, PORT_MEMORY_SPACE,
SIM_get_class("memory-space"),
"Target memory space as descendant");
// It can also be optionally connected to other memory-space
cls->add(simics::Attribute("target_mem_space", "o|n",
"Target port to a memory space",
ATTR_CLS_VAR(SampleConnectToDescendant,
target_mem_space)));
}
The MapTargetConnect class facilitates the creation and return of a new map target. A map target can be viewed as an opaque representation of an object/interface pair which can function either as an endpoint for a memory transaction or as an address space where a memory transaction can be performed.
When an object is assigned to the MapTargetConnect attribute, it is systematically examined for the presence of specific interfaces in the following order: ram, rom, io_memory, port_space, translator, transaction_translator, transaction or memory_space. The first interface found determines the "type" of the map target. For example, if obj implements both the io_memory and the translator interface, then the created map target will direct memory transactions to the io_memory interface.
The generated map target offers convenient methods such as read, read_bytes, write, and write_bytes. Additionally, users have the option to interact directly with transactions using the issue method, providing flexibility in handling memory operations.
class SampleConnectMapTarget : public simics::ConfObject {
public:
using ConfObject::ConfObject;
static void init_class(simics::ConfClass *cls) {
cls->add(simics::Attribute("map_target", "o|n",
"Map Target",
ATTR_CLS_VAR(SampleConnectMapTarget,
map_target)));
}
simics::MapTargetConnect map_target {this->obj()};
};
To register a class attribute in Simics, you can use the ClassAttribute class. This class allows you to define attributes that are associated with a Simics class rather than a Simics object.
You need to create an instance of the ClassAttribute class, providing the attribute name, type, description, getter, setter, and attribute type. And then register it with the ConfClass using the add method.
class SampleAttributeClassAttribute : public simics::ConfObject {
public:
explicit SampleAttributeClassAttribute(simics::ConfObjectRef obj)
: ConfObject(obj) {
++instance_count_;
}
virtual ~SampleAttributeClassAttribute() {
--instance_count_;
}
static attr_value_t get_instance_count(conf_class_t *cls) {
return simics::std_to_attr<int>(instance_count_);
}
static void init_class(simics::ConfClass *cls) {
cls->add(simics::ClassAttribute("instance_count", "i",
"Instance count of the class",
get_instance_count, nullptr,
Sim_Attr_Pseudo));
}
private:
static int instance_count_;
};
int SampleAttributeClassAttribute::instance_count_ = 0;
static simics::RegisterClassWithSimics<SampleAttributeClassAttribute>
// coverity[global_init_order]
init_class_attribute {
"sample_device_cxx_attribute_class_attribute",
"sample C++ attr device with class attribute",
"Sample C++ attribute device with class attribute"
};
The AttrValue class in Simics is a RAII (Resource Acquisition Is Initialization) class designed to manage the lifetime of a attr_value_t object. This class ensures that the memory allocated for the attr_value_t object is properly freed when the AttrValue object is destroyed, preventing memory leaks and ensuring proper resource management.
It is typically constructed or assigned from a Simics API call which returns an attr_value_t object.
Logging is an essential tool for tracking the behavior of the model and diagnosing issues. While standard C++ logging mechanisms such as std::cout and std::cerr are available and could be used, leveraging the full power of Simics logging is highly recommended. Simics provides a robust logging system that offers several advantages over standard logging methods. Simics logging supports the selection of log objects, levels, and groups. For more information about Simics logging, see the Model Builder User's Guide.
For performance reasons, always use the logging macros below instead of invoking the Simics logging API directly:
SIM_LOG_<type> basic macros are provided by the Simics C API and have printf-like formatting.SIM_LOG_<type>_STR extended macros are provided by the Simics C++ API and supports string-link object. The strings can be formatted using FMT library that is also shipped with Simics.Both type of macros invoke the corresponding SIM_log_<type> functions internally but are wrapped in an additional layer that does not expand the code when log level conditions are not met.
The macros expect:
The type placeholder should be one of the following: INFO, SPEC_VIOLATION, UNIMPLEMENTED, ERROR, WARNING, or CRITICAL.
An example using C++ logging:
SIM_LOG_INFO_STR(1, obj(), GROUP_ID(Signal),
fmt::format("Lowering signal (new level: {})", level));
An example using C logging:
SIM_LOG_INFO(1, obj(), GROUP_ID(Signal), "Raising signal (new level: %d)",
level);
New log groups can be registered with the Simics class in the init_class method:
void SampleLogging::init_class(simics::ConfClass *cls) {
simics::LogGroups lg {"CTOR", "Signal"};
cls->add(lg);
NOTE: The Simics API SIM_log_register_groups can only be called once per Simics class. If you need to register log groups incrementally, such as adding more groups in an extended C++ class, it is recommended to use cls->add for registering log groups.
Event is useful to let something happen only after a certain amount of (simulated) time. By posting an event on a queue, a callback function is placed on the queue and will be called later in the simulation. The amount of time can be specified in different units, e.g, seconds, cycles or steps. Users should inherit from one of the Simics provided classes based on their desired timebase:
For the sake of clarity and simplicity, the following sections will primarily focus on the TimeEvent class as the primary example. It is important to note that both CycleEvent and StepEvent operate in an analogous manner.
Event support in the C++ API encompasses several key aspects: declaring the behavior of an event, registering the event, and defining and managing it.
The following example demonstrates how to declare, register, and manage an event using the SampleEvent class:
class SampleEvent : public simics::ConfObject {
public:
using ConfObject::ConfObject;
void finalize() override {
// Post the user_event after 1 second
user_event.post(1.0);
}
class UserTimeEvent : public simics::TimeEvent<SampleEvent> {
public:
explicit UserTimeEvent(simics::ConfObject *obj)
: TimeEvent(obj, "user_event") {}
// Callback method invoked when the event is triggered
void callback(lang_void *data = nullptr) override {
dev_->some_side_effects();
}
};
void some_side_effects() {
// Implementation of side effects goes here
}
static void init_class(simics::ConfClass *cls) {
// Registering the event with a callback
cls->add(simics::EventInfo("user_event",
EVENT_CALLBACK(SampleEvent,
user_event)));
}
// Instance of UserTimeEvent initialized with this object
UserTimeEvent user_event {this};
};
The callback for an event is defined within a user-defined event class. In this example, UserTimeEvent inherits from simics::TimeEvent, which allows it to utilize time-based delays.
Requirements for the event class:
Base Class: The base class simics::TimeEvent is a template class. The template type serves as the type for the member variable dev_, which points to the C++ device class containing this event. Thus, the template type must match the C++ device class type.
Constructor: The event class must include a constructor that accepts a pointer to a Simics object (simics::ConfObject).
Static Member Variable: The event class should have a static member variable named event_cls. This fixed name is utilized by certain macros during event registration.
Passing Static Variable: The static variable event_cls must be passed to the constructor of simics::TimeEvent. This variable points to the event_class_t used in calls to the Simics API.
Callback Override: The event class must override the callback method, which is invoked when the event is triggered.
The following methods may also be overridden:
void destroy(void *data): Called when the event is removed from the queue without being triggered.attr_value_t get_value(void *data): Converts the event data into a value suitable for saving in a configuration.void *set_value(attr_value_t value): Converts a configuration value back into event data.char *describe(void *data): Generates a human-readable description of the event for use with the print-event-queue command.To register an event, call add on the ConfClass object. An example of how to register an event is shown in init_class method above, where an instance of simics::EventInfo is constructed with registration information and passed to add.
Events are posted using the post(duration) method:
Events can be canceled before they trigger using remove(match_data):
In addition to post and remove, users can also utilize:
The after feature is a lightweight event mechanism that schedules a specified method call (the callback) to be executed at a future point with the provided arguments. This feature is particularly useful for managing timed events in simulation environments.
Here's an example demonstrating how to use the after feature in a Simics C++ device class:
// Log on sim object. Used from global function since no other Simics
// objects can be used there.
void logOnSim(const std::string &msg) {
static auto sim_obj = SIM_get_object("sim");
SIM_LOG_INFO_STR(1, sim_obj, 0, msg);
}
void twoStrsArgumentGlobalFunction(std::string s1, std::string s2) {
logOnSim("Hello, I am twoStrsArgumentGlobalFunction(" + \
s1 + ", " + s2 + ")");
}
class SampleAfter : public simics::ConfObject,
public simics::EnableAfterCall<SampleAfter> {
public:
explicit SampleAfter(simics::ConfObjectRef obj)
: ConfObject(obj), simics::EnableAfterCall<SampleAfter>(this) {
}
void cancel_after(bool trigger) {
if (trigger) {
// cancel all suspended method calls associated with
// this object
cancel_all();
}
}
void oneUint64ArgumentClassFunction(uint64_t a) {
logOnSim("Hello, I am oneUint64ArgumentClassFunction("
+ std::to_string(a) + ")");
}
void finalize() override {
if (SIM_is_restoring_state(obj())) {
return;
}
AFTER_CALL(this, 1.0, &twoStrsArgumentGlobalFunction,
std::string("abc"), std::string("def"));
AFTER_CALL(this, 2.0, &SampleAfter::oneUint64ArgumentClassFunction,
obj(), one_uint64_);
}
static void init_class(simics::ConfClass *cls);
private:
uint64_t one_uint64_ {0xdeadbeef};
};
void SampleAfter::init_class(simics::ConfClass *cls) {
// Registering functions for later after call invocation
REGISTER_AFTER_CALL(&twoStrsArgumentGlobalFunction);
REGISTER_AFTER_CALL(&SampleAfter::oneUint64ArgumentClassFunction);
// Register the after event on SampleAfter with default name "after_event"
cls->add(SampleAfter::afterEventInfo());
cls->add(simics::Attribute(
"cancel_after", "b",
"When being set, cancel all after callbacks",
nullptr,
ATTR_SETTER(SampleAfter, cancel_after),
Sim_Attr_Pseudo));
}
Sample Class:
The SampleAfter class inherits from simics::ConfObject and simics::EnableAfterCall<SampleAfter>, enabling it to utilize the after feature.
To suspend a method call, a pointer to the class must be passed as the first parameter to the AFTER_CALL macro.
Method Calls:
Two method calls are suspended in this example using the after feature:
twoStrsArgumentGlobalFunction takes two strings as arguments and logs a message indicating its invocation.oneUint64ArgumentClassFunction is a class member function which logs a message with a single uint64_t argument.The method call suspended by after can be a free global function, a static class member function, or a class member function. For class member functions, the class must either inherit from simics::ConfObject (indicating it is a Simics C++ device class) or be a Bank/Register/Field class. The method must not return any value and can accept an arbitrary number of arguments. It will be invoked at most once per after statement; it does not recur. If recurring behavior is desired, the method must invoke after to schedule another call to itself.
The function pointer for the method call must be passed to two macros: REGISTER_AFTER_CALL or REGISTER_REG_BANK_AFTER_CALL, as well as to AFTER_CALL.
After Call:
Two method calls are scheduled using the AFTER_CALL macro:
twoStrsArgumentGlobalFunction occurs after 1 second with two string arguments.SampleAfter::oneUint64ArgumentClassFunction occurs after 2 seconds with a uint64_t argument.This AFTER_CALL macro requires four parameters:
after calls.Class Initialization:
after feature is built upon an internal event mechanism. To utilize this feature, you must register the event within the Simics class using the add functioninit_class method also registers the functions for later invocation via after calls. This is required by checkpointing to be able to resume a suspended method call after loading a checkpoint. This can be done using another macro: REGISTER_AFTER_CALL, or REGISTER_REG_BANK_AFTER_CALL for methods on Bank/Register/Field classes.The file bin/gen-cc-interface (or bin/gen-cc-interface.bat on Windows) is used to generate a Simics C++ interface header which is used for a Simics C++ module from the given Simics C interface header. Required argument is the path to the C interface header file.
For instance the following line specifies that example.h should be parsed by the tool and converted to a C++ interface header file.
project> bin/gen-cc-interface modules/example-interface/example.h
The generator parses the C interface header, searches for SIM_INTERFACE and generates corresponding C++ interface class to the output C++ header file.
By default, the output C++ interface header is generated under c++ subdirectory of where the input C header file is located. This can be altered by providing an optional argument by -o with the desired output location.
By default, the following code is generated to locate the original C interface header in the parent folder:
#include "../c-header.h"
When -o is used, the desired include path must be provided by the optional argument -p.
The Simics C++ API Modeling Extension (SME) complements the Simics C++ API but is not required for modeling. The SME adds notification rules, expression logic rules & state machine modeling (using Boost SML) to create a comprehensive behavioral modeling method which can ease the implementation of complex hardware modules.
Additional information is required by the SME to facilitate consistent execution order of notifications between Register & Field rules which exist for the same field. Scaffolding provides these mechanics by introducing logic on read/write methods which determines the origin of execution and ensure consistent ordering of rules along with some additional book-keeping.
To enable SME the developer will wrap any register type with sme::reg<> or sme::field<> as appropriate. Using this approach also allows the developer to selectively add the modeling extension capability only to those registers where notification rules are useful.
WARNING: When enabling SME for any entity, the register and all fields associated must be wrapped or the code will not compile.
Register declaration example
class EXAMPLE_REGISTER : public simics::BankRegister< sme::reg< simics::Register > > { ... }
Field declaration example
class EXAMPLE_FIELD : public simics::RegisterField< sme::field< simics::Field > > { ... }
Below is a full declaration example within a PortBank. Note that any simics::Register subtype or simics::Field subtype may be utilized, the sme::reg & sme::field declarations enable scaffolding for notification rules without affecting type customization.
class SampleDevice : public simics::MappableConfObject {
public:
explicit SampleDevice( simics::ConfObjectRef o)
: simics::MappableConfObject(o) {
}
static void init_class(simics::ConfClass *cls);
};
class SampleBank : public simics::PortBank<> {
public:
using PortBank::PortBank;
class EXAMPLE_REGISTER : public simics::BankRegister< sme::reg< simics::Register > > {
public:
using BankRegister::BankRegister;
class EXAMPLE_FIELD_1 : public simics::RegisterField< sme::field< simics::Field > > {
public:
using RegisterField::RegisterField;
};
class EXAMPLE_FIELD_2 : public simics::RegisterField< sme::field< simics::Field > > {
public:
using RegisterField::RegisterField;
};
public:
EXAMPLE_FIELD_1 example_field_1 {
this, simics::Name("example_field_1"),
simics::Description("example_field_1"),
simics::Offset(0),
simics::BitWidth(16)
};
EXAMPLE_FIELD_2 example_field_2 {
this, simics::Name("example_field_2"),
simics::Description("example_field_2"),
simics::Offset(15),
simics::BitWidth(16)
};
};
EXAMPLE_REGISTER example_register {
this, simics::Name("example_register"),
simics::Description("example_register"),
simics::Offset(0x04),
simics::ByteSize(4),
simics::InitValue(0x0)
};
};
class SampleBankPort : public simics::BankPort<SampleDevice> {
public:
using BankPort<SampleDevice>::BankPort;
SampleBank bank {
this, simics::Description("Sample Bank")
};
};
There is no difference to the actual device initialization code, so this modification is to declaration code only.
Notification rules provide a means to register a callback at a certain stage of processing register/field transactions, and may include content matching in the form of masks or patterns so that the logic is declarative in nature rather than in algorithm code.
Notification rules must be registered to a particular stage which determines when the rule will be evaluated. When a notification rule resolves to true, then its associated callback is executed.
| Stage | Description |
| sme::stage::PRE_READ | Occurs prior to the actual data read taking place. Only useful in rare cases where a check is required before allowing data to be read. NOTE: should only utilize a rule Type of sme::type::NOTIFY, other rule type behavior is undefined. |
| sme::stage::POST_READ | Occurs after the read of data and will allow modification of the data to be returned. Often utilized to effect state of device when content may need to be flushed or reset. |
| sme::stage::PRE_WRITE | Occurs before the data is committed. Useful if particular modes of operation will force particular bits not to accept writes. |
| sme::stage::POST_WRITE | Occurs after data has been committed (write complete) to the register/field. Important to note control has not returned to the bus at time of execution. Most common stage used to declare notification rules. |
Notification rules must also define the Type of rule which defines how data will be evaluated to determine if the rule should execute the assigned callback. The below table provides a reasonable description and details extended parameters required for each type (when necessary).
| Type | Description |
| sme::type::NOTIFY | Executes with no logic evaluation applied. |
| sme::type::MASKED | Executes only if positive masked bits change. PARAMETERS uint64_t _mask :: a binary mask which enabled bits (1) will be evaluated. |
| sme::type::PATTERN | Executes if masked bits change from one value combination to another value combination. Useful for multi-bit changes which modify state or mode of IP. PARAMETERS uint64_t _mask :: a binary mask which enabled bits (1) will be evaluated. uint64_t _start :: value which must match existing stored value with _mask applied. uint64_t _end :: value which must match data (being written) with _mask applied. |
| sme::type::RISING_BIT | Executes when the specified bit transitions from 0 to 1. PARAMETERS uint8_t _bit :: bit to evaluate. |
| sme::type::FALLING_BIT | Executes when the specified bit transitions from 1 to 0. PARAMETERS uint8_t _bit :: bit to evaluate. |
Before creating notification rules it is common place to create a stand alone class for the behavior so that stimulation from the simulator and the behavior are separated. This is useful in cases where device/register/field frameworks are auto-generated.
class example_behavior {
protected:
SampleBank * m_bank {nullptr};
public:
void init( SampleBank * _bank) { m_bank = _bank; }
};
Behavior is implemented in the form of callbacks which the notification rules can execute. Right now only simple behavior is needed to demonstrate notification rule connectivity.
Important note: If in your callbacks you find you want the old value and compare it to the new value on a change in the register you will want sme::type::PATTERN, sme::type::RISING_BIT, sme::type::FALLING_BIT and not just a simple sme::type::NOTIFY rule.
void on_reg_pre_read() {
// If you need the value of any registers you have access to your bank and
// can scope down to any register to get it's value
uint32_t val = m_bank->example_register.get();
// If you wish to log info you can use SIM_LOG_INFO
SIM_LOG_INFO(4, this->obj(), 0,
"on_reg_pre_read: %d", val);
}
void on_reg_post_read() {
// You can of course use std::cout prints for quick debug but
// probably will want to utilize SIM_LOG_INFO for long term
// as it can be turned on/off at runtime
std::cout << "on_reg_post_read()" << std::endl;
}
void on_reg_pre_write() {
std::cout << "on_reg_pre_write()" << std::endl;
}
void on_reg_post_write() {
std::cout << "on_reg_post_write()" << std::endl;
}
on_reg_mask_change() {
std::cout << "on_reg_mask_change()" << std::endl;
}
on_reg_pattern_change() {
std::cout << "on_reg_pattern_change()" << std::endl;
}
void on_enable_field_1() {
std::cout << "on_enable_field_1()" << std::endl;
// You can do calculations with the current value of a field
uint16_t temp = m_bank->example_register.example_field_1.get() & 0x7fff;
// And then use that calculated value to set a new value in the same field/register
// Or you can set a completely different register/field
m_bank->example_register.example_field_1.set(temp);
}
void on_enable_field_2() {
std::cout << "on_enable_field_2()" << std::endl;
uint16_t temp = m_bank->example_register.example_field_2.get() & 0x7fff;
m_bank->example_register.example_field_2.set(0);
}
The behavior model can be instantiated in Simics Device or as another class which ties the two together. For this example an example model will be created.
class example_model {
public:
example_model( SampleDevice * _dev) {
dev = _dev;
bank_port = new SampleBankPort { _dev->obj() };
beh.init( &bank_port->bank)
}
virtual ~example_model() {
if( bank_port) {
delete bank_port;
bank_port = nullptr;
}
}
void bind() {
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_pre_read(); },
sme::stage::PRE_READ, sme::type::NOTIFY, "on_reg_pre_read"
);
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_post_read(); },
sme::stage::POST_READ, sme::type::NOTIFY, "on_reg_post_read"
);
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_pre_write(); },
sme::stage::PRE_WRITE, sme::type::NOTIFY, "on_reg_pre_write"
);
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_post_write(); },
sme::stage::POST_WRITE, sme::type::NOTIFY, "on_reg_post_write"
);
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_mask_change(); },
sme::stage::POST_WRITE, sme::type::MASKED, "on_reg_mask_change", 0x0ff00ff0
);
bank_port->bank.example_register.add_rule(
[this]()->void { beh.on_reg_pattern_change(); },
sme::stage::POST_WRITE, sme::type::PATTERN, "on_reg_pattern_change", 0x000f000f, 0x00030002, 0x00040005
);
bank_port->bank.example_register.example_field_1.add_rule(
[this]()->void { beh.on_enable_field_1(); },
sme::stage::POST_WRITE, sme::type::RISING_BIT, "on_enable_field_1", 15
);
bank_port->bank.example_register.example_field_2.add_rule(
[this]()->void { beh.on_enable_field_2(); },
sme::stage::POST_WRITE, sme::type::LOWERING_BIT, "on_enable_field_2", 15
);
}
protected:
SampleDevice * dev;
SampleBankPort * bank_port;
example_behavior beh;
}
The example demonstrates how additional parameters for rule types are added to the end of the list. Notice that bit based field rules are relative to the field width and not the register width.
There is also the add_user_rule() method which is similar to add_rule except it has access to the register's old value and the register's new value. This allows you to easily create rules that are based on specific value changes.
bank_port->bank.example_register.add_user_rule(
[this]( uint64_t _old, uint64_t _new)->void {
// keep in mind the new value here is what was written in,
// not what is restricted by register type
if ((_old != 0x0) || (_old == 0xdeadbeef)) {
// Do some stuff
}
},
sme::stage::PRE_WRITE, sme::type::NOTIFY, "on_reg_pre_write", 15
);
Notification rules can also be deactivated from within a behavioral callback.
bank_port->bank.example_register.deactivate_rule( sme::stage::PRE_READ, "on_reg_pre_read");
bank_port->bank.example_register.deactivate_rule( sme::stage::POST_READ, "on_reg_post_read");
bank_port->bank.example_register.deactivate_rule( sme::stage::PRE_WRITE, "on_reg_pre_write");
bank_port->bank.example_register.deactivate_rule( sme::stage::POST_WRITE, "on_reg_post_write");
And those rules can be re-activated as well, this provides a great deal of flexibility for mode based IP configuration.
bank_port->bank.example_register.activate_rule( sme::stage::POST_WRITE, "on_reg_post_write");
To help understand the notification rule techniques, this section shows a very simple side effect and converts it to notification rules. This is not the only way to do the following conversion and is just an example and is not a fully functional C++ model.
Assume we have a device named SimpleDevice and on a field write we want the following to trigger:
- If the current value is zero and the new (written) value is 1
Set 'field2' to the value of 0x0 and set an array value with index 'reg_idx' to true
- If the current value is one and the new value is zero or the current value is zero and the new value is zero
Set 'field2' to the value of 0x1 and set an array value with index 'reg_idx' to false
- If the current value is one and the new value is two or the current value is zero and the new value is two:
Just output a log message saying: "Enabling task #3 %d", reg_idx
To convert these side effects to base Simics C++ API it would look like:
void write(uint64_t value, uint64_t enabled_bits) override {
auto current_value = get();
if ((current_value == 0) && (value == 1)) {
SIM_LOG_INFO(4, bank_obj_ref(), 0, "Enabling task #1 %d", reg_idx);
field2->set(0);
some_array[reg_idx]->level = true;
}
if (((current_value == 1) && (value == 0))
|| ((current_value == 0) && (value == 0))) {
SIM_LOG_INFO(4, bank_obj_ref(), 0, "Enabling task #2 %d", reg_idx);
field2->set(1);
some_array[reg_idx]->level = false;
}
if (((current_value == 1) && (value == 2))
|| ((current_value == 0) && (value == 2))) {
SIM_LOG_INFO(4, bank_obj_ref(), 0, "Enabling task #3 %d", reg_idx);
}
Field::write(value, enabled_bits);
}
To convert these side effects to SME you would take each of the if statements and create notification rules out of them and place them into the device and define what to do on each of the callbacks.
class SimpleDevice : public simics::MappableConfObject {
public:
explicit SimpleAccessTestSimpleDevice(simics::ConfObjectRef o)
: simics::MappableConfObject(o) {
// Add register rules
// ((this.val == 0) && (value == 1))
b->bank.example_register.idx.add_rule([this]()->void {this->enable_task_1(); }, sme::stage::PRE_WRITE, sme::type::RISING_BIT, "idx_rising_bit_0", 0);
// ((this.val == 1) && (value == 0))
b->bank.example_register.idx.add_rule([this]()->void {this->enable_task_2(); }, sme::stage::PRE_WRITE, sme::type::FALLING_BIT, "idx_falling_bit_0", 0);
// nochange to bit 0 ((this.val == 0) && (value == 0))
b->bank.example_register.idx.add_rule([this]()->void {this->enable_task_2(); }, sme::stage::PRE_WRITE, sme::type::PATTERN, "idx_no_change_bit_0", 0x1, 0x0, 0x0);
// ((this.val == 1) && (value == 2))
b->bank.example_register.idx.add_rule([this]()->void {this->enable_task_3(); }, sme::stage::PRE_WRITE, sme::type::PATTERN, "idx_val_1_to_2", 0x3, 0x1, 0x2);
// ((this.val == 0) && (value == 2))
b->bank.example_register.idx.add_rule([this]()->void {this->enable_task_3(); }, sme::stage::PRE_WRITE, sme::type::RISING_BIT, "idx_rising_bit_1", 1);
};
// Device init_class
static void init_class(simics::ConfClass *cls);
// Start notify rule methods
void enable_task_1() {
uint8_t reg_idx = b->bank.example_register.idx.get();
SIM_LOG_INFO(4, this->obj(), 0, "Enabling task #1 %d", reg_idx);
b->bank.example_register.field2 = 0;
some_array[reg_idx]->level = true;
}
void enable_task_2() {
uint8_t reg_idx = b->bank.example_register.idx.get();
SIM_LOG_INFO(4, this->obj(), 0, "Enabling task #2 %d", reg_idx);
b->bank.example_register.field2 = 1;
some_array[reg_idx]->level = false;
}
void enable_toggle_edge() {
SIM_LOG_INFO(4, this->obj(), 0, "Enabling task #3 %d", b->bank.example_register.idx.get());
}
};
Expression rules provide a way to listen to register, field and (eventually) signal notification rules and then evaluate a compound expression which mirrors a HW SPEC definition. When the expression evaluates to true a callback is executed where the user only has to be concerned with coding the specific behavior/functionality. These compound expressions can also be used to walk a state machine.
Expressions will typically be declared in the MODEL definition.
sme::expression expression_example { "expression_example"};
The sensitive_to statement can bind another expression (via an expression vector) or create a notification rule and bind the notification to this expression (automatically). The parameter list determines what type of notification rule or binding will occur.
// DEFINE EXPRESSION SENSITIVITIES
// NOTIFY
expression_example.sensitive_to( bank.example_register.example_field_1, stage::POST_WRITE);
// MASK
expression_example.sensitive_to( bank.example_register.example_field_1, stage::POST_WRITE, 0x0003);
// PATTERN
expression_example.sensitive_to( bank.example_register.example_field_1, stage::POST_WRITE, 0x0030, 0x0020, 0x0010);
The expression does not use a DSL, equation parser or other complex notion. Instead a standard C boolean expression is used and defined as a lambda to keep the code concise. This evaluation is defined as the "logic" statement of the expression. The logic statement may utilize other registers, fields, even banks if you have a reference to them; so it is possible to build very complex logic analysis statements.
// DEFINE EXPRESSION TO EVALUATE
expression_example.logic( [this]() -> bool {
bool result =
(bank.example_register.example_field_1.get() == 1 && bank.example_register.example_field_2.get() == 0x011) ||
(bank.example_register.get() == 0x03320110);
return( result);
});
The 'logic' statement is actually part of an evaluation flow which can activate many different vectors.
| Expression Vector | Description |
| eval_true | Executes every time the logic evaluates to true. |
| eval_false | Executes every time the logic evaluates to false. |
| change | Executes every time the logic changes (true->false), (false->true) |
| rising | Executes every time the logic changes from false->true |
| falling | Executes every time the logic changes from true->false |
These expression vectors allow for behavior definition relative to how expression logic should drive the underlying implementation.
// BIND TO DEVELOPER METHOD for FUNCTIONALITY
expression_example.eval_true.execute( [this]() -> void {
std::cout << "expression_example.eval_true" << std::endl;
});
expression_example.eval_false.execute( [this]() -> void {
std::cout << "expression_example.eval_false" << std::endl;
});
expression_example.change.execute( [this]() -> void {
std::cout << "expression_example.change" << std::endl;
});
expression_example.rising.execute( [this]() -> void {
std::cout << "expression_example.rising" << std::endl;
});
expression_example.falling.execute( [this]() -> void {
std::cout << "expression_example.falling" << std::endl;
});
It is also possible to make an expression sensitive to another expression via an expression vector. Imagine if a second register existed, and expression_example_2 is only sensitive to expression_example.rising. One could do
the following to check example_register_2 value only when the rising condition of the first expression was met.
sme::expression expression_example_2 { "expression_example_2"};
expression_example_2.sensitive_to( expression_example.rising);
expression_example_2.logic( [this]() -> bool {
bool result = (bank.example_register_2.get() == 0x12345678);
return( result);
});
It is good to make note that you typically will use either notification rules or expressions (which are composed of notification rules) based on the complexity of content evaluation needed. It may be easier to just use expressions in most cases, but for simple tasks notification rules work fine.
State machines have long been a cornerstone of modeling complex HW flows in virtual platforms. Simics currently lacks any API that specifically defines a state machine. Luckily for us there are several API's developed in the last decade which excel at state machine definition and keeping the footprint and executable code size to a minimum.
Boost SML is probably the most advanced implementation, a highly template meta-programming based implementation which actually treats each state as a type; and the binary code inherently is executing from its state. This creates a bit of a headache in capturing the active state for save/restore, but otherwise the framework is completely UML declaration compliant and is amazing to work with.
This example will showcase the implementation of a minimal state loop of the following state machine:
IP_ON_BUSY -> DEASSERT_RESET : by sending command DEASSERT_RESET
DEASSERT_RESET -> IP_READY_2_RESET : internal acknowledge of state entered
IP_READY_2_RESET -> IP_RESET_COMPLETE: internal bit write (IP has no power cycle)
IP_RESET_COMPLETE -> IP_ON_BUSY : timer expired
most of the transitions are pseudo-code because this is C++ test-harness based example.
Example top level: <Simics Base Package>/src/devices/c++-api/extensions/unittests/doc_example.cxx
Example sub files: <Simics Base Package>/src/devices/c++-api/extensions/unittests/doc_example
There is an example logger for the state machine events and transitions under third_party_integration/fsm_logger.hpp. While this is somewhat useful, it may be desirable to create your own logger for BOOST SML, this is defined in the declaration of the SM instance which is covered further down.
For this example we need a few registers (to begin with) that will enable the need for complex expressions to drive the state machine.
// Device container class
class FsmRegDevice : public simics::MappableConfObject {
public:
explicit FsmRegDevice(simics::ConfObjectRef o)
: simics::MappableConfObject(o) {
}
static void init_class(simics::ConfClass *cls);
};
class FsmRegBank : public simics::PortBank<> {
public:
using PortBank::PortBank;
void resetAllRegisters() {
unsigned numOfRegs = number_of_registers();
for (unsigned i = 0; i < numOfRegs; ++i) {
std::pair<size_t, simics::RegisterInterface *> reg_pair = register_at_index(i);
reg_pair.second->reset();
}
}
class IP_MASK : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class IP_BIT : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
IP_BIT IP_BIT {
this, simics::Name("IP_BIT"),
simics::Description("Mask bit for IP selection"),
simics::Offset(15),
simics::BitWidth(1)
};
};
class STATE_CONTROL : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class COMMAND : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
class EXECUTE : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
COMMAND COMMAND {
this, simics::Name("COMMAND"),
simics::Description("When set, requests IPs move into this state"),
simics::Offset(0),
simics::BitWidth(5)
};
EXECUTE EXECUTE {
this, simics::Name("EXECUTE"),
simics::Description("sets command to be executed upon..."),
simics::Offset(7),
simics::BitWidth(1)
};
};
class FSM_ACTIONS : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class GATE_CLK : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
GATE_CLK GATE_CLK {
this, simics::Name("GATE_CLK"),
simics::Description("1: Gate output clock, 0: No action"),
simics::Offset(16),
simics::BitWidth(1)
};
};
class IP_SLEEP : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class IP_BIT : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
IP_BIT IP_BIT {
this, simics::Name("IP_BIT"),
simics::Description("IP_BIT will go to sleep"),
simics::Offset(15),
simics::BitWidth(1)
};
};
class DRIVER_FSM_STATE_IP_1 : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class state : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
state state {
this, simics::Name("state"),
simics::Description("IP driver FSM Status"),
simics::Offset(0),
simics::BitWidth(5)
};
};
class DRIVER_FSM_STATE_IP_2 : public simics::BankRegister<sme::reg<simics::Register> > {
public:
using BankRegister::BankRegister;
class state : public simics::RegisterField<sme::field<simics::Field> > {
public:
using RegisterField::RegisterField;
};
public:
state state {
this, simics::Name("state"),
simics::Description("IP 2 driver FSM Status"),
simics::Offset(0),
simics::BitWidth(5)
};
};
IP_MASK ip_mask {
this, simics::Name("ip_mask"),
simics::Description("Mask to target specific IP."),
simics::Offset(0xddc),
simics::ByteSize(4),
simics::InitValue(0x0)
};
STATE_CONTROL state_control {
this, simics::Name("state_control"),
simics::Description("desired state"),
simics::Offset(0xde4),
simics::ByteSize(4),
simics::InitValue(0x0)
};
FSM_ACTIONS fsm_actions {
this, simics::Name("fsm_actions"),
simics::Description("fsm actions"),
simics::Offset(0xb4),
simics::ByteSize(4),
simics::InitValue(0x0)
};
IP_SLEEP ip_sleep {
this, simics::Name("ip_sleep"),
simics::Description("sets which IP will go to sleep."),
simics::Offset(0x1054),
simics::ByteSize(4),
simics::InitValue(0x0)
};
DRIVER_FSM_STATE_IP_1 driver_fsm_state_ip_1 {
this, simics::Name("driver_fsm_state_ip_1"),
simics::Description("Read IP 1 Driver FSM state"),
simics::Offset(0xf44),
simics::ByteSize(4),
simics::InitValue(0x0)
};
DRIVER_FSM_STATE_IP_2 driver_fsm_state_ip_2 {
this, simics::Name("driver_fsm_state_ip_2"),
simics::Description("Read IP 2 Driver FSM state"),
simics::Offset(0xf4c),
simics::ByteSize(4),
simics::InitValue(0x0)
};
};
// BankPort container class
class FsmRegBankPort : public simics::BankPort<FsmRegDevice> {
public:
using BankPort<FsmRegDevice>::BankPort;
FsmRegBank bank {
this, simics::Description("FSM Register bank")
};
};
There are a few additional features which need to be implemented such as save/restore of the state which requires extra work outside of the standard Boost SML definition. There is also some implications with scoping of callbacks which must be considered.
Declaration - device_declaration.hpp - Declaration of the event & state types, state machine, data & callback target
Definition - SM_device_declaration.hpp - Brings the callbacks, state machine instantiation, and get/set state capabilities into a single class for use by developer.
Developer Source - As defined above, is the source that the developer should be concerned with, knowing the state machine is declared and defined correctly.
The code below defines an enumeration of states, and TYPES for all events and states of the state machine definition.
#include <boost/sml.hpp>
#include <iostream>
#include <stdint.h>
#include <functional>
#include <iostream>
#include <cassert>
namespace sml = boost::sml;
#ifndef __IP_DECLARATION_HPP__
#define __IP_DECLARATION_HPP__
class SM_DEFINITION;
namespace IP
{
struct SM {
enum E { // This E is a designation for ENUMERATION, not event!!!
START = 0,
IP_ON_BUSY,
DEASSERT_RESET,
IP_READY_2_RESET,
IP_RESET_COMPLETE,
SEND_RESET_PREP,
WAIT_FOR_ACKNOWLEDGE,
SEND_POWER_CYCLE,
RESTORE_IP,
ERROR_COULD_NOT_DETECT_STATE
};
};
// special for START state
struct E_INIT {};
// hierarchical events - used at top level and within sub sm
struct E_GOTO_ON_AVAILABLE {};
struct E_TIMER_EXPIRED {};
// top level only events
struct E_FSM_RESET_ASSERTED {};
struct E_ENTER_RESET{};
// sub sm only events
struct E_ENABLE_LOCK {};
struct E_IP_GO_TO_SLEEP {};
struct E_EXECUTE_RESET {};
struct E_RESET_PREP_SENT {};
struct E_ACKNOWLEDGE_RECEIVED {};
struct E_POWER_UP {};
struct E_LOCK_DEASSERTED {};
// states
class START;
class IP_ON_BUSY;
struct IP_IMPL; // sub state machine
class DEASSERT_RESET;
class IP_READY_2_RESET;
class IP_RESET_COMPLETE;
class SEND_RESET_PREP;
class WAIT_FOR_ACKNOWLEDGE;
class SEND_POWER_CYCLE;
class RESTORE_IP;
Below the type definitions a data type (for storage of interesting SM relevant data) and a bevior class (to facilitate callbacks) are provided as constructs which will be utilized by the state machine.
// data and behavior declaration
class behavior_t;
class data_t {
public:
SM::E state;
bool lock_in_effect {false};
bool power_cycle_bypassed {false};
behavior_t * beh;
};
class behavior_t {
public:
data_t d;
behavior_t() { d.beh = this; }
// State entry / exit callbacks
virtual void DEASSERT_RESET_on_enter() = 0;
virtual void DEASSERT_RESET_on_exit() = 0;
virtual void IP_RESET_COMPLETE_on_enter() = 0;
virtual void IP_RESET_COMPLETE_on_exit() = 0;
virtual void IP_ON_BUSY_on_enter() = 0;
virtual void IP_ON_BUSY_on_exit() = 0;
virtual void IP_IMPL_on_enter() = 0;
virtual void IP_IMPL_on_exit() = 0;
// event callbacks
virtual void do_initialize() = 0;
virtual void do_complete_request() = 0;
virtual void do_deassert_reset() = 0;
virtual void do_set_lock() = 0;
virtual void do_send_reset_prep() = 0;
virtual void do_no_power_cycle() = 0;
virtual void do_cycle_power() = 0;
virtual void do_restore_state_if_needed() = 0;
virtual void do_trigger_timer() = 0;
virtual void do_is_not_powering_down() = 0;
// Save & Restore methods
virtual IP::SM::E get_current_state() = 0;
virtual void set_current_state( IP::SM::E _state) = 0;
// Accessor Methods
std::string state_to_string( IP::SM::E _state) {
std::string retval = "";
switch( _state) {
case IP::SM::E::START:
retval = "START";
break;
case IP::SM::E::IP_ON_BUSY:
retval = "IP_ON_BUSY";
break;
case IP::SM::E::DEASSERT_RESET:
retval = "DEASSERT_RESET";
break;
case IP::SM::E::IP_READY_2_RESET:
retval = "IP_READY_2_RESET";
break;
case IP::SM::E::IP_RESET_COMPLETE:
retval = "IP_RESET_COMPLETE";
break;
case IP::SM::E::SEND_RESET_PREP:
retval = "SEND_RESET_PREP";
break;
case IP::SM::E::WAIT_FOR_ACKNOWLEDGE:
retval = "WAIT_FOR_ACKNOWLEDGE";
break;
case IP::SM::E::SEND_POWER_CYCLE:
retval = "SEND_POWER_CYCLE";
break;
case IP::SM::E::RESTORE_IP:
retval = "RESTORE_IP";
break;
default:
retval = "ERROR: COULD NOT DETECT STATE";
break;
}
return( retval);
}
IP::SM::E string_to_state( std::string _str) {
if( _str == "START") return( IP::SM::E::START);
else if( _str == "IP_ON_BUSY") return( IP::SM::E::IP_ON_BUSY);
else if( _str == "DEASSERT_RESET") return( IP::SM::E::DEASSERT_RESET);
else if( _str == "IP_READY_2_RESET") return( IP::SM::E::IP_READY_2_RESET);
else if( _str == "IP_RESET_COMPLETE") return( IP::SM::E::IP_RESET_COMPLETE);
else if( _str == "SEND_RESET_PREP") return( IP::SM::E::SEND_RESET_PREP);
else if( _str == "WAIT_FOR_ACKNOWLEDGE") return( IP::SM::E::WAIT_FOR_ACKNOWLEDGE);
else if( _str == "SEND_POWER_CYCLE") return( IP::SM::E::SEND_POWER_CYCLE);
else if( _str == "RESTORE_IP") return( IP::SM::E::RESTORE_IP);
else return( IP::SM::E::ERROR_COULD_NOT_DETECT_STATE);
}
};
Finally the flow of the state machine is described in BOOST SML which follows a near UML style of definition and includes both sub-state machines and guards. Notice that lambda's are used to pass the SM data reference to the events, and the actual callback is executed from the data object handler. This keeps the definition of the state machine clear and separate from the implementation of behavior.
struct IP_IMPL
{
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
* state<DEASSERT_RESET> + on_entry<_> / []( data_t & d) { d.beh->DEASSERT_RESET_on_enter(); }
, state<DEASSERT_RESET> + event<E_ENABLE_LOCK> / []( data_t & d) { d.beh->do_set_lock(); } = state<IP_READY_2_RESET>
, state<DEASSERT_RESET> + sml::on_exit<_> / []( data_t & d) { d.beh->DEASSERT_RESET_on_exit(); }
, state<IP_READY_2_RESET> + event<E_EXECUTE_RESET> / []( data_t & d) { d.beh->do_send_reset_prep(); } = state<SEND_RESET_PREP>
, state<IP_READY_2_RESET> + event<E_IP_GO_TO_SLEEP> / []( data_t & d) { d.beh->do_no_power_cycle(); } = state<IP_RESET_COMPLETE>
, state<SEND_RESET_PREP> + event<E_RESET_PREP_SENT> = state<WAIT_FOR_ACKNOWLEDGE>
, state<WAIT_FOR_ACKNOWLEDGE> + event<E_ACKNOWLEDGE_RECEIVED> / []( data_t & d) { d.beh->do_cycle_power(); } = state<SEND_POWER_CYCLE>
, state<SEND_POWER_CYCLE> + event<E_POWER_UP> / []( data_t & d) { d.beh->do_restore_state_if_needed(); } = state<RESTORE_IP>
, state<RESTORE_IP> + event<E_LOCK_DEASSERTED> / []( data_t & d) { d.beh->do_trigger_timer(); } = state<IP_RESET_COMPLETE>
, state<IP_RESET_COMPLETE> + on_entry<_> / []( data_t & d) { d.beh->IP_RESET_COMPLETE_on_enter(); }
, state<IP_RESET_COMPLETE> + sml::on_exit<_> / []( data_t & d) { d.beh->IP_RESET_COMPLETE_on_exit(); }
);
}
};
const auto guard_state_IP_RESET_COMPLETE = []( const auto & event, data_t & d) { return( d.beh->get_current_state() == IP::SM::E::IP_RESET_COMPLETE); };
struct IP
{
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
* state<START> + event<E_INIT> / []( data_t & d) {d.beh->do_initialize(); } = state<IP_ON_BUSY>
, state<IP_ON_BUSY> + on_entry<_> / []( data_t & d) { d.beh->IP_ON_BUSY_on_enter(); }
, state<IP_ON_BUSY> + event<E_ENTER_RESET> / []( data_t & d) { d.beh->do_complete_request(); } = state<IP_ON_BUSY>
, state<IP_ON_BUSY> + event<E_GOTO_ON_AVAILABLE> / []( data_t & d) { d.beh->do_deassert_reset(); } = state<IP_IMPL>
, state<IP_ON_BUSY> + sml::on_exit<_> / []( data_t & d) { d.beh->IP_ON_BUSY_on_exit(); }
, state<IP_IMPL> + on_entry<_> / []( data_t & d) { d.beh->IP_IMPL_on_enter(); }
, state<IP_IMPL> + event<E_TIMER_EXPIRED> [guard_state_IP_RESET_COMPLETE] = state<IP_ON_BUSY>
, state<IP_IMPL> + event<E_FSM_RESET_ASSERTED> / []( data_t & d) { d.beh->do_is_not_powering_down(); } = state<IP_ON_BUSY>
, state<IP_IMPL> + sml::on_exit<_> / []( data_t & d) { d.beh->IP_IMPL_on_exit(); }
);
}
};
}
The SM_DEFINITION is an extension of the IP::behavior_t; where the logger (optional) and state machine instance are declared.
To work with simics replay/stop/reverse a state machine must have a mechanism to save & restore state. The enumerstion for the state targets is utilized here with developer required methods which must be implemented for Simics.
class SM_DEFINITION : public IP::behavior_t {
public:
extras::fsm_logger logger;
sml::sm< IP::IP, sml::testing, sml::logger< extras::fsm_logger> > sm{ d, logger};
SM_DEFINITION() {;}
IP::SM::E get_current_state() override {
using namespace sml;
using namespace IP;
IP::SM::E retval = IP::SM::E::ERROR_COULD_NOT_DETECT_STATE;
if( sm.is( state<IP_ON_BUSY>)) retval = IP::SM::E::IP_ON_BUSY;
else if( sm.is( state<START>)) retval = IP::SM::E::START;
else if( sm.is( state<IP_IMPL>)) {
if( sm.is<decltype( state<IP_IMPL>)>( state<DEASSERT_RESET>)) retval = IP::SM::E::DEASSERT_RESET;
else if( sm.is<decltype( state<IP_IMPL>)>( state<IP_READY_2_RESET>)) retval = IP::SM::E::IP_READY_2_RESET;
else if( sm.is<decltype( state<IP_IMPL>)>( state<IP_RESET_COMPLETE>)) retval = IP::SM::E::IP_RESET_COMPLETE;
else if( sm.is<decltype( state<IP_IMPL>)>( state<SEND_RESET_PREP>)) retval = IP::SM::E::SEND_RESET_PREP;
else if( sm.is<decltype( state<IP_IMPL>)>( state<WAIT_FOR_ACKNOWLEDGE>)) retval = IP::SM::E::WAIT_FOR_ACKNOWLEDGE;
else if( sm.is<decltype( state<IP_IMPL>)>( state<SEND_POWER_CYCLE>)) retval = IP::SM::E::SEND_POWER_CYCLE;
else if( sm.is<decltype( state<IP_IMPL>)>( state<RESTORE_IP>)) retval = IP::SM::E::RESTORE_IP;
}
return( retval);
}
void set_current_state( IP::SM::E _state) override {
using namespace sml;
using namespace IP;
if( _state == IP::SM::E::IP_ON_BUSY) sm.set_current_states( state<IP_ON_BUSY>);
else if( _state == IP::SM::E::START) sm.set_current_states( state<START>);
else {
sm.set_current_states( state<IP_IMPL>);
if( _state == IP::SM::E::DEASSERT_RESET) sm.set_current_states<decltype( state<IP_IMPL>)>( state<DEASSERT_RESET>);
else if( _state == IP::SM::E::IP_READY_2_RESET) sm.set_current_states<decltype( state<IP_IMPL>)>( state<IP_READY_2_RESET>);
else if( _state == IP::SM::E::IP_RESET_COMPLETE) sm.set_current_states<decltype( state<IP_IMPL>)>( state<IP_RESET_COMPLETE>);
else if( _state == IP::SM::E::SEND_RESET_PREP) sm.set_current_states<decltype( state<IP_IMPL>)>( state<SEND_RESET_PREP>);
else if( _state == IP::SM::E::WAIT_FOR_ACKNOWLEDGE) sm.set_current_states<decltype( state<IP_IMPL>)>( state<WAIT_FOR_ACKNOWLEDGE>);
else if( _state == IP::SM::E::SEND_POWER_CYCLE) sm.set_current_states<decltype( state<IP_IMPL>)>( state<SEND_POWER_CYCLE>);
else if( _state == IP::SM::E::RESTORE_IP) sm.set_current_states<decltype( state<IP_IMPL>)>( state<RESTORE_IP>);
}
}
};
Developer source is the implementation of behavior for the state machine. Because this is a unit test (C++) the code will simply print basic messages.
class SM_Behavior : public SM_DEFINITION {
public:
SM_Behavior() : SM_DEFINITION()
{;}
// Developer would override callbacks here...
// State entry / exit callbacks
void DEASSERT_RESET_on_enter() override { std::cout << "DEASSERT_RESET ***OVERRIDDEN*** on_enter" << std::endl; }
void DEASSERT_RESET_on_exit() override { std::cout << "DEASSERT_RESET ***OVERRIDDEN*** on_exit" << std::endl; }
void IP_RESET_COMPLETE_on_enter() override { std::cout << "IP_RESET_COMPLETE ***OVERRIDDEN*** on_enter" << std::endl; }
void IP_RESET_COMPLETE_on_exit() override { std::cout << "IP_RESET_COMPLETE ***OVERRIDDEN*** on_exit" << std::endl; }
void IP_ON_BUSY_on_enter() override { std::cout << "IP_ON_BUSY ***OVERRIDDEN*** on_enter" << std::endl; }
void IP_ON_BUSY_on_exit() override { std::cout << "IP_ON_BUSY ***OVERRIDDEN*** on_exit" << std::endl; }
void IP_IMPL_on_enter() override { std::cout << "IP_IMPL ***OVERRIDDEN*** on_enter" << std::endl; }
void IP_IMPL_on_exit() override { std::cout << "IP_IMPL ***OVERRIDDEN*** on_exit" << std::endl; }
// event callbacks
void do_initialize() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_initialize" << std::endl; }
void do_complete_request() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_complete_request" << std::endl; }
void do_deassert_reset() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_deassert_reset" << std::endl; }
void do_set_lock() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_set_lock" << std::endl; }
void do_send_reset_prep() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_send_reset_prep" << std::endl; }
void do_no_power_cycle() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_no_power_cycle" << std::endl; }
void do_cycle_power() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_cycle_power" << std::endl; }
void do_restore_state_if_needed() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_restore_state_if_needed" << std::endl; }
void do_trigger_timer() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_trigger_timer" << std::endl; }
void do_is_not_powering_down() override { std::cout << "EVENT_CALLBACK: ***OVERRIDDEN*** do_is_not_powering_down" << std::endl; }
};
For this example the behavior code is written inside the expressions, but in many cases one would want more of the behavior code to exist in the behavior callbacks of the state machine.
Only 2 expressions are required to define the 4 state transitions based on the description in 13.4.
The ... in the code is where you would place your simics module parent type / implementations
class doc_example : public ::testing::Test, ... {
public:
virtual ~doc_example() {
if (bank_parent) {
delete bank_parent;
bank_parent = nullptr;
}
if (bp) {
delete bp;
bp = nullptr;
}
}
MockObject *bank_parent;
FsmRegBankPort *bp;
SM_Behavior ip;
sme::expression ex__deassert_reset {"ex__deassert_reset"};
sme::expression ex__ip_goto_sleep {"ex__ip_goto_sleep"};
The ex__deassert_reset will define both a rising and falling edge event capture. Technically the "process_event" methods are the only code which should exist in the execute. Other code should be implemented in the behavior callbacks defined in section 13.4.3.3.
explicit doc_example(const std::string &name = "") {
...
bp = new FsmRegBankPort { bank_parent->obj() };
// ex_goto_deassert_reset
ex__deassert_reset.sensitive_to( bp->bank.state_control.EXECUTE, stage::POST_WRITE);
ex__deassert_reset.logic( [this]() -> bool {
bool result = ( bp->bank.ip_mask.IP_BIT.get() == 1 &&
bp->bank.state_control.COMMAND.get() == IP::SM::E::DEASSERT_RESET &&
bp->bank.state_control.EXECUTE.get() == 0x01);
return( result);
});
ex__deassert_reset.rising.execute( [this]() -> void {
bp->bank.state_control.EXECUTE.set (0); // reset control (should be a scheduled event as shown in comments below)
// ex__deassert_reset.process( 1); // which should execute the next line in theory...
// SIM_realtime_event( 1, &process_ex_goto_deassert_reset, reinterpret_cast< void *>(this), 0, ex_goto_deassert_reset.name().c_str());
ip.sm.process_event( IP::E_GOTO_ON_AVAILABLE{}); // this will change the SM current state
bp->bank.driver_fsm_state_ip_1.state.set( ip.get_current_state()); // relay the new state for driver to read
});
// the falling edge can only happen if true was entered, so this is valid assumption caused by timed event
ex__deassert_reset.falling.execute( [this]() -> void {
ip.sm.process_event( IP::E_ENABLE_LOCK{}); // this will change the SM current state
bp->bank.driver_fsm_state_ip_1.state.set( ip.get_current_state()); // relay the new state for driver to read
});
// IP GOTO SLEEP as an expression
ex__ip_goto_sleep.sensitive_to( bp->bank.ip_sleep.IP_BIT, stage::POST_WRITE);
ex__ip_goto_sleep.logic( [this]() -> bool {
return((ip.get_current_state() == IP::SM::E::IP_READY_2_RESET) && (bp->bank.ip_sleep.IP_BIT.get() == 1));
});
ex__ip_goto_sleep.rising.execute( [this]() -> void {
bp->bank.ip_sleep.IP_BIT.set(0);
ip.sm.process_event( IP::E_IP_GO_TO_SLEEP{});
bp->bank.driver_fsm_state_ip_1.state.set( ip.get_current_state());
// ideally this would schedule a timer before auto-returning to IP_ON_BUSY
// flush the state & ensure that the falling edge is recorded (i.e. reset)
ex__ip_goto_sleep.evaluate(true);
});
}
};
Finally we get to the unit test code itself, which is mocking a few different styles of transitions.
TEST_F(doc_example, example_bank_created) {
// GLOBAL INIT
EXPECT_EQ(bp->bank.bank_name(), "bar");
ip.set_current_state( IP::SM::E::START);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::START);
// transition to state IP_ON_BUSY
ip.sm.process_event( IP::E_INIT{});
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_ON_BUSY);
// transition to state DEASSERT_RESET
bp->bank.ip_mask.IP_BIT.write( 0x01);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_ON_BUSY);
bp->bank.state_control.COMMAND.write( IP::SM::E::DEASSERT_RESET);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_ON_BUSY);
bp->bank.state_control.EXECUTE.write( 1); // <-- fire expression.rising
EXPECT_EQ( bp->bank.driver_fsm_state_ip_1.state.get(), IP::SM::E::DEASSERT_RESET);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::DEASSERT_RESET);
// verify execute cleared but evaluation status unchanged
EXPECT_EQ( bp->bank.state_control.EXECUTE.get(), 0);
EXPECT_EQ( ex__deassert_reset.last_state(), true);
EXPECT_EQ( ex__deassert_reset.evaluate(), false);
EXPECT_EQ( ex__deassert_reset.last_state(), true);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::DEASSERT_RESET);
// simulate a timed event
ex__deassert_reset.on_sensitivity(); // execute ex__deassert_reset.falling
// that should have triggered a transition to IP_READY_2_RESET
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_READY_2_RESET);
// now simulate a go to sleep (short path)
bp->bank.ip_sleep.IP_BIT.write( 1);
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_RESET_COMPLETE);
{ // simulate a timer expiring
ip.sm.process_event( IP::E_TIMER_EXPIRED{});
}
EXPECT_EQ( ip.get_current_state(), IP::SM::E::IP_ON_BUSY);
}
From a device model's point of view there are only a few incompatible changes between Simics C++ API v1 and v2 to consider. Migration should thus be straight forward and trivial.
Here is the list of known changes that can cause problems when migrating from Simics C++ API v1:
USE_CC_API to 2 in the Makefile.simics/cc-api.h instead of simics/c++/device-api.h.simics::ConfObject class instead of simics::SimicsObject.simics::ConfObjectRef instead of simics::SimicsObjectRef.simics_obj of ConfObject has been renamed to obj.make_class to register a class. The parameters are same as the previous ClassDef.simics::Attribute.simics::ClassAttributeadd method.init_class, but can use the old way of registering everything in the init_local static functionvoid instead of simics::SetResult. If an error occurs during the setting process, a C++ runtime_error exception should be thrown.state_mapper anymore. Any existing state_mapper is dead code and can be removed.