While the majority of this guide focuses on DML, there is also support for writing modules in C and Python. DML is recommended for device models as DML allows the author to focus on the functionality of the device without having to worry about the mechanics of interacting with Simics. Components must be written in Python which is also useful for high level modules which do not model devices directly. C is useful for integrating with legacy code.
This chapter describes how to create modules written in C focusing on the mechanics of interacting with Simics. The general guidelines presented earlier apply to C as well as to DML. Each section focuses on one concept and describes how to implement it in C. It may be useful to review section Classes and Objects in API Reference Manual to clarify the differences between objects, classes, and modules.
Most modules need to do some work when initially loaded into Simics. Typically this work includes registering the classes implemented by the module, and their attributes, with Simics.
A module written in C/C++ must implement the function init_local()
. It must exist, even if it is empty. The function will be run by Simics when the module is loaded into Simics. The function is defined as:
void
init_local(void)
{
}
If the module is written in C++, this function must be declared extern "C" for C linkage.
Each Simics class implemented by a module must be registered with Simics. Remember that classes registered in a module should be listed in the MODULE_CLASSES
variable in the module's Makefile
. This allows Simics to automatically load the required modules when reading a configuration file. Similarly, components registered in the module should be listed in MODULE_COMPONENTS
.
Registering a class is done by creating and filling a class_info_t
structure, and then calling the function SIM_create_class
with the new class name and the class_info_t
structure. The members in the class_info_t
structure are:
init
A function called when creating an instance of the class.
finalize
This function is called once init
has returned, and all attributes in a configuration have been set.
kind
The class kind tells Simics whether objects of this class should be saved when a checkpoint is created. Valid values are:
Sim_Class_Kind_Vanilla
kind
is not given any value.) Sim_Class_Kind_Pseudo
short_desc
A short description of the class, preferably a one-liner. This is for example used by some commands that list classes in table form.
description
A string that should contain a, potentially longer, description of the class.
In C/C++, registration of classes is usually done from within the mandatory init_local() function. The C definition of class_info_t
and SIM_create_class()
is the following:
typedef struct class_info {
conf_object_t *(*alloc)(conf_class_t *cls);
lang_void *(*init)(conf_object_t *obj);
void (*finalize)(conf_object_t *obj);
void (*objects_finalized)(conf_object_t *obj);
void (*deinit)(conf_object_t *obj);
void (*dealloc)(conf_object_t *obj);
const char *description;
const char *short_desc;
class_kind_t kind;
} class_info_t;
conf_class_t *SIM_create_class(
const char *NOTNULL name, const class_info_t *NOTNULL class_info);
SIM_create_class()
returns a pointer to a conf_class_t
structure which is used internally by Simics to keep track of the class information. This pointer can be used when referring to the class in calls to other functions.
A simple init_local()
initialization function could look like this:
void
init_local(void)
{
class_info_t cdata;
conf_class_t *my_class;
memset(&cdata, 0, sizeof(cdata));
cdata.init = my_init;
cdata.kind = Sim_Class_Kind_Pseudo;
cdata.short_desc = "One-liner";
cdata.description = "This is my class";
my_class = SIM_create_class("my-class", &cdata);
// Other initializations...
}
A configuration class implemented in C typically maintains the state of each object in a structure that is co-allocated with the object's conf_object_t
structure, which is used both as a general reference to the object and to keep track of information for the configuration system.
This is done by putting the conf_object_t
first in a C struct
that also contains the object state:
typedef struct {
conf_object_t obj; // Always first!
// Object-specific state follows.
float temperature;
} my_object_t;
Converting between a pointer to my_object_t
and its corresponding conf_object_t
then becomes a cast or member dereference:
// my_object_t -> conf_object_t
conf_object_t *obj = &myobj->obj;
// conf_object_t -> my_object_t
my_object_t *myobj = (my_object_t *)obj;
Of course, the code casting from a conf_object_t
pointer must know for certain that it really is an object of the right type. This knowledge is usually implicit in the function interfaces.
The allocation of the combined structure is done in the alloc
method in the class_info_t
value passed to SIM_create_class
:
static conf_object_t *
my_alloc(conf_class_t *cls)
{
my_object_t *myobj = MM_ZALLOC(1, my_object_t);
return &myobj->obj;
}
The method init
should be defined to initialise the structure:
static void *
my_init(conf_object_t *obj)
{
my_object_t *myobj = (conf_object_t *)obj;
myobj->temperature = 20.0;
return myobj;
}
Configuration classes written in C++ may use the same techniques as in C, or simpler by subclassing simics::SimicsObject
; see the C++ Device API programming guide for details.
Attributes are linked to the class definition, usually just after the class has been declared, with the SIM_register_attribute()
function. It has the following declaration in C:
int SIM_register_attribute(
conf_class_t *cls, const char *name,
attr_value_t (*get_attr)(conf_object_t *),
set_error_t (*set_attr)(conf_object_t *, attr_value_t *),
attr_attr_t attr,
const char *type,
const char *doc);
The parameters of SIM_register_attribute()
are:
cls
The name of the class (previously registered with SIM_create_class()
.)
name
The name of the attribute to register.
get_attr
, set_attr
The get()
and set()
functions for the attribute. If one of these operations is not supported, NULL
(or None
in Python) can be used.
attr
The properties of the attribute, a combination of the configuration type, an optional index type and initialization order. It tells Simics how the attribute will be saved and addressed, and is specified using the constants described below.
The configuration type of an attribute must be selected from one of the following values:
Sim_Attr_Required
Sim_Attr_Optional
Sim_Attr_Pseudo
Attributes may also have the following additional kinds added (using a bitwise or operation).
Sim_Attr_Persistent
save-persistent-state
command. Persistent attributes are used for data that survives power-cycling. Sim_Attr_Internal
In addition the order in which the attribute will be initialized can be defined by adding (also using a bitwise or operation) with one of the following values:
Sim_Init_Phase_0
Sim_Init_Phase_1
Attributes with Sim_Init_Phase_1
will be initialized after attributes with Sim_Init_Phase_0
, but no other order is guaranteed.
type
String describing the data type of the attribute.
desc
A documentation string describing the attribute.
Let us use a simple counter attribute as an example.
In C, we'll have an object declared as:
typedef struct my_object {
conf_object_t obj;
int foo;
} my_object_t;
We want to implement an attribute called counter
, thus we need a pair of set
/get
functions. counter
will internally use foo
to keep its value. The pair of get/set
functions could be defined as:
static attr_value_t
get_counter(conf_object_t *obj)
{
my_object_t *mo = (my_object_t *)obj;
return SIM_make_attr_uint64(mo->foo);
}
static set_error_t
set_counter(conf_object_t *obj, attr_value_t *val)
{
my_object_t *mo = (my_object_t *)obj;
mo->foo = SIM_attr_integer(*val);
return Sim_Set_Ok;
}
In the get_counter()
function, obj
is the object that owns the attribute and arg
is the user information that was registered along with the attribute. Note that obj
can be safely cast to my_object_t
(conf_object_t
is used as a "base type" here). The function creates an attr_value_t
variable that will be of integer type and contain the value foo
. It then returns this attribute value.
The set_counter()
function on the other hand takes a val
argument which contains the value to be written. The return value is of type set_error_t
, which is defined as below. Descriptions of the values can be found in the API Reference Manual.
typedef enum {
Sim_Set_Ok,
Sim_Set_Object_Not_Found,
Sim_Set_Interface_Not_Found,
Sim_Set_Illegal_Value,
Sim_Set_Illegal_Type,
Sim_Set_Illegal_Index,
Sim_Set_Attribute_Not_Found,
Sim_Set_Not_Writable
} set_error_t;
Registering the counter
attribute is just a matter of calling SIM_register_attribute()
:
SIM_register_attribute(my_class,
"counter",
get_counter,
set_counter,
Sim_Attr_Required,
"i",
"A counter");
In the previous example, the attribute counter
provides a direct representation of the value foo
inside the object. Now let us add an attribute called add_counter
that will increase foo
by a given value when the attribute is set, and do nothing when the attribute is read. This would give us the following code:
static set_error_t
set_add_counter(conf_object_t *obj, attr_value_t *val)
{
my_object_t *mo = (my_object_t *)obj;
mo->foo += SIM_attr_integer(*val);
return Sim_Set_Ok;
}
There is no need for a get function since this attribute only can be written. The semantics of set_add_counter()
are also slightly different, since the function actually adds a value to foo
.
It is thus possible to create real attributes whose value corresponds to a real variable in an object, and pseudo attributes which are only used as object "methods".
Registering the add_counter
attribute is straightforward:
SIM_register_attribute(class_name, "add_counter",
NULL,
set_add_counter,
Sim_Attr_Pseudo,
"i",
"A sample pseudo attribute.");
Errors in attribute getter methods are signalled by calling SIM_attribute_error
with an error description, and then returning an arbitrary value. An explicitly "invalid" value is usually returned in these cases, as generated by SIM_make_attr_invalid
, but any value will do. In Python, None
is a good choice.
Errors in attribute setter methods are signalled by returning a set_error_t
value other than Sim_Set_Ok
. In most cases, returning Sim_Set_Illegal_Value
is the appropriate way of indicating that the given value is not valid for the attribute. Optionally, SIM_attribute_error
may be called in order to provide a more detailed error description, prior to returning.
Attribute errors generally give rise to frontend exceptions. If the error occurred during the reading or writing of a checkpoint, that activity is aborted; any partially written checkpoint or created objects are removed.
An object that wants to interact with another through an interface uses the SIM_get_interface()
function to retrieve the interface structure. It can then call the other object using the functions defined in the structure.
conf_object_t *obj = SIM_get_object("phys_mem");
const memory_space_interface_t *ifc;
attr_value_t val;
ifc = SIM_get_interface(obj, "memory_space");
val = ifc->read(obj, SIM_get_object("cpu0"), 0x1234, 4, 0);
When you are using interfaces inside an object, you will often need to define which object you want to talk to via an attribute. Typically this is done by defining an attribute with type o|n
, and checking if the object passed as argument implements the necessary interface:
static attr_value_t
get_an_object(conf_object_t *obj)
{
my_object_t *mo = (my_object_t *)obj;
return SIM_make_attr_object(mo->an_object);
}
static set_error_t
set_an_object(conf_object_t *obj, attr_value_t *val)
{
my_object_t *mo = (my_object_t *)obj;
if (SIM_attr_is_nil(*val)) {
mo->an_object = NULL;
} else {
foo_interface_t *foo_ifc =
(foo_interface_t *)SIM_get_interface(
SIM_attr_object(*val), "foo");
if (SIM_clear_exception() != SimExc_No_Exception)
return Sim_Set_Interface_Not_Found;
mo->an_object = SIM_attr_object(*val);
}
return Sim_Set_Ok;
}
[…]
SIM_register_attribute(class_name, "an_object",
get_an_object,
set_an_object,
Sim_Attr_Optional,
"o|n",
"An object implementing the 'foo' interface");
A limitation of interfaces as described above is that each class can only implement each interface once, but you may need to implement an interface several times in the same device model. For example, consider an interrupt controller that has several interrupt inputs having to implement the signal
interface once for each input.
To get around this a device will usually use port objects. These are sub objects of the main device object that can be used to specify a particular interface instance when connecting objects, see 34. In this case using a port interface is identical to using a regular interface. You use SIM_get_interface()
to get the interface implementation and do not have to care whether the object is a regular object or port object.
However, if your model is required to interact with old models implemented in C or Python you may find that you also have to support an older port mechanism. In this case a port is identified by an object and a port name string.
Your attribute then has to accept either an object or a list with an object and a name string. If a port name is specified you have to use SIM_get_port_interface()
instead of SIM_get_interface()
to get the interface implementation.
The example code from 14.5.1 below is extended with support for the old port mechanism:
static attr_value_t
get_an_object(conf_object_t *obj)
{
my_object_t *mo = (my_object_t *)obj;
if (mo->a_portname != NULL)
return SIM_make_attr_list(
2,
SIM_make_attr_object(mo->an_object),
SIM_make_attr_object(mo->n_portname));
return SIM_make_attr_object(mo->an_object);
}
static set_error_t
set_an_object(conf_object_t *obj, attr_value_t *val)
{
my_object_t *mo = (my_object_t *)obj;
if (SIM_attr_is_nil(*val)) {
mo->an_object = NULL;
} else if (SIM_attr_is_obj(*val)) {
foo_interface_t *foo_ifc =
(foo_interface_t *)SIM_get_interface(
SIM_attr_object(*val), "foo");
if (SIM_clear_exception() != SimExc_No_Exception)
return Sim_Set_Interface_Not_Found;
mo->an_object = SIM_attr_object(*val);
} else if (SIM_attr_is_list(*val)) {
conf_object_t *foo_obj =
SIM_attr_object(SIM_attr_list_item(*val, 0));
const char *foo_port =
SIM_attr_string(SIM_attr_list_item(*val, 1));
foo_interface_t *foo_ifc =
(foo_interface_t *)SIM_get_port_interface(
foo_obj, "foo", foo_port);
if (SIM_clear_exception() != SimExc_No_Exception)
return Sim_Set_Interface_Not_Found;
mo->an_object = foo_obj;
}
MM_FREE(mo->a_portname);
mo->a_portname = foo_port ? MM_STRDUP(PORT) : NULL;
return Sim_Set_Ok;
}
[…]
SIM_register_attribute(class_name, "an_object",
get_an_object,
set_an_object,
Sim_Attr_Optional,
"o|n|[os]",
"An object implementing the 'foo' interface");
The implementation of an existing interface requires the population of all the function pointers that are listed in the interface definition with the functions that should be called. The interface should then be registered using the SIM_register_interface()
function:
SIM_register_interface(conf_class_t *class, char *name, void *iface)
Where the parameters are:
class
name
iface
This gives us the following code:
static cycles_t
my_operate(conf_object_t *mem_hier, conf_object_t *space,
map_list_t *map, generic_transaction_t *mem_op)
{
// do something
}
static conf_class_t *my_class;
static timing_model_interface_t ifc;
void
init_local(void)
{
[…]
ifc.operate = my_operate;
SIM_register_interface(my_class, "timing_model", (void *) &ifc);
[…]
}
If there is no predefined interface that suits your needs, you will need to define your own interface type. This is described in section 11.
If your model needs to implement the same interface more than once, for example, consider the case of an interrupt controller with multiple interrupt inputs, it should do that by implementing the interface on multiple port objects.
Port objects and how to use them from C are described in 34.
Logging in C is handled by SIM_log_register_group()
and the
SIM_LOG_*
macros.
A single call to SIM_log_register_groups()
registers all groups for the class. The function is used as:
SIM_log_register_groups(conf_class_t *cls, const char **gnames)
where the parameters are:
classname
groupnames
An example:
static char *groupnames[] = { "config", "request", "response", NULL };
SIM_log_register_groups(my_class, &groupnames);
The log group values will be defined by the order of the strings in the tuple as a power of 2 series, so in the example above config
corresponds to 1, request
corresponds to 2 and response
corresponds to 4.
Log outputs are handled with SIM_LOG_INFO()
and corresponding macros. They take the following parameters:
SIM_LOG_INFO(int level, conf_object_t *obj, uint64 groups, const char *msg);
with the parameters meaning:
level
SIM_log_error()
function. obj
groups
msg
Logging from a Simics module written in C/C++ should be done with the following macros: SIM_LOG_INFO()
, SIM_LOG_ERROR()
, SIM_LOG_UNDEFINED()
, SIM_LOG_SPEC_VIOLATION()
, and SIM_LOG_UNIMPLEMENTED()
. These macros use the corresponding SIM_log_<type>()
function internally, and should always be used instead for performance reasons.
Note that the macros take a variable number of arguments to allow you to write printf()
-like strings.
A small example:
static attr_value_t
get_counter_array(conf_object_t *obj, attr_value_t *idx)
{
my_object_t *mo = (my_object_t *)obj;
SIM_LOG_INFO(4, obj, 0, "get_counter_array");
if (!SIM_att_is_nil(*idx)) {
if (!SIM_attr_is_integer(*idx))
SIM_LOG_ERROR(obj, 0,
"Index must be integer");
return SIM_make_attr_invalid();
return SIM_make_attr_uint64(
mo->foo[SIM_attr_integer(*idx)]);
}
else {
attr_value_t ret = SIM_alloc_attr_list(10);
int i;
for (i = 0; i < 10; i++) {
SIM_attr_list_set_item(ret, i,
SIM_make_attr_uint64(mo->foo[i]);
}
return ret;
}
}
Event classes are created with the SIM_register_event()
function which should be called at module initialization time. The event class holds a pointer to the callback function. Here is an example from an UART model:
static event_class_t *transmit_event;
static void
uart_transmit(conf_object_t *obj, void *param)
{
uart_device_t *uart = (uart_device_t *)obj;
SIM_LOG_INFO(4, &uart->log, 0, "event called");
}
void
init_local(void)
{
conf_class_t *uart_class;
[…]
uart_class = SIM_create_class(…);
[…]
transmit_event = SIM_register_event(
"transmit character", uart_class, 0, uart_transmit,
0, 0, 0, 0);
[…]
}
To post an event in the future, based on time, the SIM_event_post_time()
and SIM_event_post_cycle()
function is used. An example:
SIM_event_post_time(&uart->log.obj, transmit_event,
&uart->log.obj, 0.00001, NULL);
It is possible to specify an argument to be passed to the callback function, as the sixth parameter of SIM_event_post_time
. In order for checkpointing to work correctly, you must in this case also specify user-data to attr_value_t
conversion functions in the call to SIM_register_event
.
If for some reason you do want to remove a posted but not yet handled event, you can cancel it with a call to SIM_event_cancel_time()
, specifying the object, event class and optionally a parameter.
SIM_event_cancel_time(obj, transmit_event, obj, 0, NULL);
You can also check how long time is left until an event occurs using SIM_event_find_next_cycle()
, again specifying the object, event class and parameter. The time left to the event is returned in cycles.
If you want to post an event a number of simulated CPU steps into the future it should instead post to the step queue. Posting to the step queue is very similar to posting to the time queue, but the functions SIM_event_post_step()
, SIM_event_cancel_step()
and SIM_event_find_next_step()
should be used instead.
Refer to the API Reference Manual for more information on the function prototypes and their parameters.
As the Simics profiling and event viewing systems are based on listening to haps it can be useful for a device to directly trigger haps rather than relying on haps built into the memory, link, and processor models. In these situations the model has to choose between a standard Simics hap and a user defined hap. Standard haps have the benefit of documentation and predefined hap handlers. User defined haps have the advantage of specificity.
Before handlers can be notified of a new hap, the hap must be known. A new hap type is made known through registration. Registering a new hap type is done with the function SIM_hap_add_type()
. The signature is:
hap_type_t
SIM_hap_add_type(const char *hap,
const char *params,
const char *param_desc,
const char *index,
const char *desc,
int old_hap_obj);
where the parameters are:
hap
The name of the hap, which must be unique
params
A string specifying the number of parameters of the hap and their types. The return value and the first two parameters of the callback function are always the same and are not included in the list. A valid parameter description string contains only the following type description characters:
i
– int I
– int64 (64 bit integer) e
– exception_type_t o
– object (i.e., void* in C and Python object in Python) s
– string m
– memory transaction (generic_transaction_t *
in C) c
– configuration object (conf_object_t *
in C) param_desc
space separated list of descriptive parameter names (in the same order as params
, so that the first word is the name of the first parameter. If param
is the empty string, param_desc
may be None.
index
A string describing the index value for the hap, or None if there is no index value. The meaning of indexes is up to you to define.
desc
A human readable description of the hap.
old_hap_obj
Always 0.
The return value is a handle that must be saved for operations on the hap.
Example:
void
init_local()
{
[…]
hap_handle = SIM_hap_add_type("My_Special_Hap",
"ii",
"val1 val2",
NULL,
"Triggered when something special"
" happens in my module.",
0);
if (hap_handle <= 0) {
/× error handling ×/
[…]
}
}
Whenever the condition for the hap is met, the handlers for the hap should be notified. Triggering a hap incurs some overhead; if it occurs in performance-sensitive code, it may be beneficial to use one of the SIM_hap_is_active_obj
or SIM_hap_is_active_obj_idx
functions to check if there are any handlers prior to calling the notification function.
bool SIM_hap_is_active_obj(hap_type_t hap, conf_object_t *NOTNULL obj);
bool SIM_hap_is_active_obj_idx(hap_type_t hap, conf_object_t *NOTNULL obj,
int64 index);
where the parameter hap
is the value returned from SIM_hap_add_type()
or from SIM_hap_get_number()
if using a standard hap type. These predicates are approximate, but if they return false, there is no need to trigger the hap since no installed functions would be called.
The notification to handlers is normally done by calling one of SIM_hap_occurred()
, SIM_hap_occurred_vararg()
, SIM_hap_occurred_always()
, and SIM_hap_occurred_always_vararg()
. See the API Reference Manual for information about the differences.
int
SIM_c_hap_occurred_always(hap_type_t hap,
conf_object_t *obj,
int64 value,
...);
The parameters are:
hap
SIM_hap_add_type()
and SIM_hap_get_number()
. obj
value
list
In C, hap parameters will be provided as additional parameters to the function. A short example:
static void
some_func(conf_object_t *obj, int v1, int v2)
{
if (some_condition) {
SIM_c_hap_occurred(hap_handle, obj, 0, v1, v2)
}
}
If you want to interact with the Python interpreter built into Simics from C, you will need to use the Python/C API. The Python/C API is defined by the simics/python-header.h
header file. This file needs to be included before any other Simics API header files.
In order to use the header file, you will need to include the following two statements in your module's Makefile
:
MODULE_CFLAGS = $(PYTHON_INCLUDE)
MODULE_LDFLAGS = $(PYTHON_LDFLAGS)
These need to be set before the include $(MODULE_MAKEFILE)
line and must use the lazy evaluation assignment operator (=
) rather than :=
or +=
.
See http://docs.python.org for more information about the Python/C API.
You should not include the standard Python.h
header file directly as it contains errors which simics/python-header.h
works around.