This section describes how to model devices using Python and the confclass
class.
A custom class always starts by creating a confclass object.
The confclass class is available in the simics module.
This is the shortest possible custom class based on confclass:
class MyDevice:
cls = simics.confclass(classname="my_device")
This will create a conf class named my_device, and allow creation of Simics
objects, for example by using
SIM_set_configuration
or
SIM_create_object.
To further develop a custom class, the confclass object (cls) is then used
to:
cls.attr, such as cls.attr.my_attributecls.o, such as cls.o.port.my_interface()cls.portsconf_class_t standard methods, such as @cls.finalize@cls.iface.signal.signal_raise@cls.attr.name.setterNote that hierarchical items also contains decorators, which express hierarchical significance, as will be explained in the attributes and the port objects sections.
A complete Simics module with a device class based on confclass can be created
using the project-setup script:
project> bin\project-setup.bat --py-device-confclass my-confclass-device
project$ ./bin/project-setup --py-device-confclass my-confclass-device
This will create skeleton code for a new device in the
[project]/modules/my-confclass-device/ directory, with all files needed to
build it as a Simics module. The entry point for a module written in Python is
the file [project]/modules/module_load.py, which is executed when the Simics
module is loaded. This file is normally small, containing something like:
from . import my_device_confclass
The rest of the implementation is given in another file, in this case
[project]/modules/my_device_confclass.py.
The confclass class is available from the simics module.
Here is the shortest possible Python device created with confclass:
class MyDevice:
cls = simics.confclass(classname="my_device")
The complete list of confclass creation arguments is:
| Argument | Desc |
|---|---|
classname | the name of the device class |
parent | used to specify parent class when using inheritance (see the Inheritance section) |
pseudo | set to True to make a Sim_Class_Kind_Pseudo class (see class_info_t) |
register | set to False to prevent registration (see the Prevent registration section) |
short_doc | short documentation, see next section |
doc | long documentation, see next section |
This is code for my_class class, but this time with added documentation.
class MyDevice:
cls = simics.confclass(
classname="my_device",
short_doc="one-line documentation",
doc="Long documentation.")
The added optional arguments in the example are:
short_doc - sets a one-line string, which can be seen when using the command
list-classes.
simics> list-classes substr = my_device
The following classes are available:
┌─────────┬──────────────────────┐
│ Class │ Short description │
├─────────┼──────────────────────┤
│my_device│one-line documentation│
└─────────┴──────────────────────┘
doc - a longer class description visible in the CLI help command.
simics> help class:my_device
Class my_device
Description
A longer description. The class my_device is empty
...
Most functions in the class_info_t struct that is passed to the SIM_create_class (see also the Object Lifetime section) can be optionally be overridden using decorators provided by the cls:
class MyDevice:
cls = simics.confclass(classname="my_device")
def __init__(self):
self.calls = []
@cls.init
def initialize(self):
self.calls.append("initialize")
@cls.finalize
def finalize(self):
self.calls.append("finalize")
@cls.objects_finalized
def objects_finalized(self):
self.calls.append("objects_finalized")
@cls.deinit
def deinit(self):
global deinit_called
deinit_called = True
Inside the class, the self argument to object methods contains an instance of
the Python class, which is expected since it is a Python object. This is also
true for Simics callback methods, such as the object init callback in this
example:
class MyDevice:
cls = simics.confclass(classname="my_device")
def __init__(self):
assert isinstance(self, MyDevice)
@cls.init
def init(self):
assert isinstance(self, MyDevice)
On an instance of the Simics class, the instance of MyDevice is available as
<device>.object_data:
device = simics.SIM_create_object("my_device", "device")
assert isinstance(device.object_data, MyDevice)
This is another example showing access to the variable local from the Simics
object device, and object_data.
class MyDevice:
cls = simics.confclass(classname="my_device")
@cls.init
def init(self):
self.local = 1
device = simics.SIM_create_object("my_device", "device")
assert device.object_data.local == 1
Confclass automatically sets the obj-member of the Python object to the
corresponding configuration object (conf_object_t). In this example,
self.obj is used with SIM_object_name, which takes a configuration object /
conf_object_t as argument, to print the object name.
class MyDevice:
cls = simics.confclass(classname="my_device")
@cls.objects_finalized
def objects_finalized(self):
print(simics.SIM_object_name(self.obj))
device = simics.SIM_create_object("my_device", "device")
Attributes are created by adding them to the namespace under <confclass obj>.attr, for example <confclass obj>.attr.<my_attribute>. In this example,
we create an attribute v1 with the attribute type "i":
class MyDevice:
cls = simics.confclass(classname="my_device")
cls.attr.v1("i")
attr_type) is the only mandatory argument.# Create an object named 'device', with default value 1 for v1.
device = simics.SIM_create_object(
MyDevice.cls.classname, "device", [["v1", 1]])
stest.expect_equal(device.v1, 1)
To specify custom attribute getters and setters, decorate the custom
function with the getter or setter decorators in the attribute object. Using
the previous example, an overridden getter function should then be decorated
with @cls.attr.v1.getter, and a setter method would be decorated with
@cls.attr.v1.setter
@cls.attr.v1.getter
def get_v1(self):
return self.v1
@cls.attr.v1.setter
def set_v1(self, new_value):
self.v1 = new_value
Note that the decorators are taken hierarchically from
cls.attr.v1, and that the backing value can be accessed withself.v1.
To document the attribute, set the doc argument. This corresponds to the
desc argument in SIM_register_attribute.
As is documented in the Modelling with
C section and in the API Reference Manual
(SIM_register_attribute),
attributes can have different properties that can be controlled with the
attr_attr_t attr in SIM_register_attribute. For confclass, the corresponding
argument is kind, but it is more convenient to set this to combinations of
attr_attr_t.
As has been mentioned before, kind defaults to Sim_Attr_Required, but when
using the optional arguments, the mutually exclusive (bits in 0x7) parts of
kind (attr_attr_t) will be updated according to the arguments used:
defaultSim_Attr_Optional in kind if the
current value is Sim_Attr_Required. See also additional information on
attribute default values.optionalSim_Attr_Optional in kind.pseudoSim_Attr_Pseudo in kind.read_onlypseudo must also be set to true.
(see note below)write_onlypseudo must also be set to true.
(see note below)The reason why
pseudomust be set to true when setting eitherread_onlyorwrite_onlyto true is that attributes that are saved to checkpoints (for example ifSim_Attr_Optionalis set) must have be both readable and writeable.
In the following example various attributes are created:
class MyDevice:
cls = simics.confclass(classname="my_device")
# Add required attribute.
cls.attr.attr0("i")
# Add an optional attribute by setting 'default' with documentation.
cls.attr.attr1("i", default=1, doc="documentation")
# Add a read-only attribute.
cls.attr.attr2("i", read_only=True, default=2, pseudo=True)
# Add a write-only attribute.
cls.attr.attr3("i", write_only=True, pseudo=True)
# Add a pseudo attribute.
cls.attr.attr4("i", pseudo=True)
# Create a required attribute with custom access methods below.
# The access methods use the decorators from the attribute:
# - @cls.attr.attr5.getter
# - @cls.attr.attr5.setter
cls.attr.attr5("i")
# Use the getter decorator for 'attr5'
@cls.attr.attr5.getter
def get_attr5(self):
return self.attr5
# Use the setter decorator for 'attr5'
@cls.attr.attr5.setter
def set_attr5(self, new_value):
self.attr5 = new_value
If an attribute is of container type, the default value cannot be an instance of that type.
The reason for this is that the default value would otherwise be shared among all instances
of that class. So if the attribute was a list, and obj1 appended an element to the list,
that would also affect obj2, assuming these are instances of the same class.
For example, if having an attribute attr1 that has the type list of integers,
having [] as default value as in the class wrong_default_value will raise
an exception.
class WrongDefaultValue:
cls = simics.confclass(classname="wrong_default_value")
cls.attr.attr1("[i*]", default=[])
As a workaround, instead use a callable that requires no argument, as in the
class right_default_value. This will create a unique list for each instance as
default value.
class RightDefaultValue:
cls = simics.confclass(classname="right_default_value")
cls.attr.attr1("[i*]", default=list)
Allowed default values are:
"o")"b")"n")"f")"d")"i")"s")Forbidden default values are (container types):
[...]){...})The info and status commands can easily be added by using a decorator to the
method that provides the info or status data. In the below example:
@cls.command.info decorator is used to specify that the get_info
method should provide data to the <object>.info command.@cls.command.status decorator is used to specify that the get_status
method should provide data to the <object>.status command.class MyDevice:
cls = simics.confclass(classname="my_device")
@cls.init
def init(self):
self.info_value = 1
self.status_value = 2
@cls.command.info
def get_info(self):
return [("Info", [("value", self.info_value)])]
@cls.command.status
def get_status(self):
return [("Status", [("value", self.status_value)])]
By default a class created using confclass is automatically registered as a
Simics class. To prevent the registration, the argument register can be set to
False. At a later stage, use the register function to register the class.
class MyDevice:
cls = simics.confclass(classname="my_device", register=False)
Interfaces are implemented using cls.iface.<name> where <name> is the name
of the Simics interface.
For each method in the interface, there will be a decorator that is used to
specify custom methods in the interface implementation.
In this example we use the signal interface, and use
the cls.iface.signal.signal_raise and cls.iface.signal.signal_lower
decorators to specify the custom methods in the interface implementation.
class MyDevice:
cls = simics.confclass(classname="my_device")
cls.attr.signal_raised("b", default=False)
@cls.iface.signal.signal_raise
def signal_raise(self):
self.signal_raised = True
@cls.iface.signal.signal_lower
def signal_lower(self):
self.signal_raised = False
Port objects allow a class to have several
implementations of the same interface (see example). Port
objects are added using the <parent confclass>.o namespace, for example
<parent confclass>.o.port.port1(). This gives another confclass instance
which can be used to add attributes and interfaces to the port object, just like
it is done with the parent’s confclass.
When adding the confclass for the port object, the classname argument can be
used or not used to achieve different results:
By not specifying classname, a class name will be assigned based on the
parent class name and the last component of the hierarchical name.
class Parent1:
parent = simics.confclass(classname='parent1') # the parent confclass
child = parent.o.port.child() # default classname
Since the child/port objects lacks a classname, it will automatically be
assigned the name parent1.child.
obj1 = SIM_create_object('parent1', 'obj1', [])
stest.expect_equal(obj1.port.child.classname, 'parent1.child')
It is also possible to specify the last part of a custom class name, by
setting classname to a string that starts with a dot, followed by a valid
class name.
class Parent2:
parent = simics.confclass(classname='parent2') # the parent confclass
child = parent.o.port.child('.custom_child') # name starting with dot
The parent class name will automatically be prepended to the custom class name.
obj2 = SIM_create_object('parent2', 'obj2', [])
stest.expect_equal(obj2.port.child.classname, 'parent2.custom_child')
Finally, it is also possible to specify an existing class.
class Child3:
cls = simics.confclass(classname='child3')
class Parent3:
parent = simics.confclass(classname='parent3') # the parent confclass
child = parent.o.port.child(classname='child3') # child3 defined above
The child port object will have the child3 class, and will get an instance
of the Child3 as object_data.
obj3 = SIM_create_object('parent3', 'obj3', [])
stest.expect_equal(obj3.port.child.classname, 'child3')
stest.expect_true(isinstance(obj3.port.child.object_data, Child3))
An important difference between these examples is the
object_dataof the port object. In the first two examples, theobject_datawill be set to the parent objectsobject_data, since there is no custom Python class, while in the last example, theobject_datawill be set to an instance of the Python classClass3, associated withchild3. This is important, since the state for the port object will be separate from the parent object. The storage for attributes will be separate, and interface implementations will not get direct access to the parent’sobject_dataand must useSIM_port_object_parent(self.obj).object_datato access the parent’s state.
To add an interface, use the reference to the port objects confobject. The
history member can be accessed with self.history from methods decorated
with both parent and child. The reason for this is that the child class does
not have a custom Python class and will hence share object_data with the
parent class.
class Parent:
parent = simics.confclass(classname="parent")
child = parent.o.port.child()
@parent.init
def init(self):
self.history = []
@child.iface.signal.signal_raise
def signal_raise(self):
self.history.append("raise")
@child.iface.signal.signal_lower
def signal_lower(self):
self.history.append("lower")
Call the signal interface on the child/port interface and verify the contents
of history.
obj = SIM_create_object("parent", "obj")
obj.port.child.iface.signal.signal_lower()
obj.port.child.iface.signal.signal_raise()
stest.expect_equal(obj.object_data.history, ["lower", "raise"])
Replace the member variable history in the previous
example with an attribute.
class Parent:
parent = simics.confclass(classname="parent")
child = parent.o.port.child()
child.attr.history("[s*]", default=list)
@child.iface.signal.signal_raise
def signal_raise(self):
self.history.append("raise")
@child.iface.signal.signal_lower
def signal_lower(self):
self.history.append("lower")
Call the signal interface on the child/port interface and verify the contents of the history variable.
obj = SIM_create_object("parent", "obj")
obj.port.child.iface.signal.signal_lower()
obj.port.child.iface.signal.signal_raise()
stest.expect_equal(obj.port.child.history, ["lower", "raise"])
Because the object_data is shared between parent and child, history is accessible from the parent object as well.
stest.expect_equal(obj.object_data.history, ["lower", "raise"])
As mentioned above, the object_data (self) of the port object is set to
either:
object_data of the parent classIf the object_data is shared with the parents, the attributes are required
to be unique. In this example attribute a exists in both the parent and port
object, which will result in a raised exception during class definition.
try:
class Parent1:
parent = simics.confclass(classname="parent1") # the parent confclass
parent.attr.a("i", default=1)
child = parent.o.port.child() # the child/port confclass
child.attr.a("i", default=2) # name collision for attribute "a"
except TypeError:
pass # expected
else:
stest.fail("Did not get TypeError for colliding attributes.")
To correct the situation, add a Python class to the port object. This will
result in that the parent and the port objects will get unique object_data.
class Child2:
cls = simics.confclass(classname="child2")
cls.attr.a("i", default=2)
class Parent2:
parent = simics.confclass(classname="parent2")
parent.attr.a("i", default=1)
child = parent.o.port.child(classname=Child2.cls.classname)
By providing a custom Python class for the port object, the port object will
get a non-shared unique object_data (an instance of Child), thus
eliminating the attribute collision between the parent and the port object.
parent = simics.SIM_create_object("parent2", "parent")
stest.expect_different(parent.object_data, parent.port.child.object_data)
stest.expect_equal(parent.a, 1)
stest.expect_equal(parent.port.child.a, 2)
In the below example, my_device has two port objects with different
implementations of the
signal interface.
class MyDevice:
"""This class contains two implementations of the 'signal' interface
implemented in two different port objects."""
RAISE = "raise"
LOWER = "lower"
cls = simics.confclass(classname="my_device")
reset_1 = cls.o.port.RESET_1()
reset_2 = cls.o.port.RESET_2()
cls.attr.history("[[ss]*]", default=list)
@reset_1.iface.signal.signal_raise
def reset_1_raise(self):
self.signal_raise_common("RESET_1")
@reset_1.iface.signal.signal_lower
def reset_1_lower(self):
self.signal_lower_common("RESET_1")
@reset_2.iface.signal.signal_raise
def reset_2_raise(self):
self.signal_raise_common("RESET_2")
@reset_2.iface.signal.signal_lower
def reset_2_lower(self):
self.signal_lower_common("RESET_2")
def signal_raise_common(self, port):
self.history.append([port, self.RAISE])
def signal_lower_common(self, port):
self.history.append([port, self.LOWER])
To specify an existing class, set classname to an existing class.
This way, an interface implementation can be added from another class.
class Resetter:
"""A class implementing the 'signal' interface."""
cls = simics.confclass(classname="resetter")
cls.attr.raised("b", default=False)
cls.iface.signal()
@cls.iface.signal.signal_raise
def signal_raise(self):
self.raised = True
class MyDevice:
"""Set the class 'resetter' as port object class to get the 'signal'
interface (implemented by 'resetter')."""
cls = simics.confclass(classname="my_device")
reset = cls.o.port.RESET(classname="resetter")
Port interfaces is a legacy feature that allows a class to have several implementations of the same interface. The recommended way to do this is to use Port Objects.
When a port interface has been defined, it can be retrieved from the object with
SIM_get_port_interface.
Below is an example of how port objects are registered in Python using the
confclass framework:
class MyDevice:
RAISE = "raise"
LOWER = "lower"
cls = simics.confclass(classname="my_device")
reset_1_iface = cls.ports.RESET_1.signal
reset_2_iface = cls.ports.RESET_2.signal
cls.attr.history("[[ss]*]", default=list)
@reset_1_iface.signal_raise
def reset_1_raise(self):
self.signal_raise_common("RESET_1")
@reset_1_iface.signal_lower
def reset_1_lower(self):
self.signal_lower_common("RESET_1")
@reset_2_iface.signal_raise
def reset_2_raise(self):
self.signal_raise_common("RESET_2")
@reset_2_iface.signal_lower
def reset_2_lower(self):
self.signal_lower_common("RESET_2")
def signal_raise_common(self, port):
self.history.append([port, self.RAISE])
def signal_lower_common(self, port):
self.history.append([port, self.LOWER])
Simics Events classes can be registered by appending an event name to the
<confclass>.event namespace, and this will create an object with
decorators that can be used to specify the event callbacks.
When creating a Simics object from a confclass that has declared events, there
will be an entry for each event class on Python object, and this entry can be
used to post/cancel/find events for this class.
For example, this would post the my_event event to trigger in 123 cycles,
using the clock conf.clk:
obj.object_data.my_event.post(clock=conf.clk, cycles=123)
The clock object (requirements depends on the delay metrics), can either be
specified with the clock key-word argument, but alternatively, if the object
has an object assigned to the queue attribute, this will be used
(SIM_object_clock
is used internally).
If the object has a clock (queue set), and clock is specified in a call,
the object from clock argument is used.
Events are added by appending an event name to the <confclass>.event
namespace. The only requirement is that a callback function (see
SIM_register_event)
is implemented and decorated with <confclass><event name>.callback. This
is the shortest possible event declaration:
class MyDevice1:
cls = simics.confclass(classname="my_device1")
@cls.event.my_event.callback
def callback(self, data):
pass
The complete list of decorator is:
@<confclass>.event.<event name>.callback (required)@<confclass>.event.<event name>.destroy (optional)@<confclass>.event.<event name>.get_value (required if saved and data needed)@<confclass>.event.<event name>.set_value (required if saved and data needed)@<confclass>.event.<event name>.describe (optional)With a call to the event declaration, it is possible to customize:
flags:
to change the event_class_flag_t, the default is Sim_EC_No_Flags, meaning
that the event are saved checkpoints. See also note below.description:
allows specifying a static event description, as an alternative to using the
@<confclass>.event.<event name>.describe decorator to decorate the custom describe method.saved:
controls if the event is checkpointed or not, by setting or clearing Sim_EC_Notsaved.Here is an example of declaring an non-saved event class with a static description.
class MyDevice2:
cls = simics.confclass(classname="my_device2")
cls.event.my_event(saved=False, description="nice weather")
@cls.event.my_event.callback
def callback(self, data):
assert data is None
Even though events are per default saved to checkpoints, because of the default
flagsbeingSim_EC_No_Flags, the data used when posting events will not be handled properly unless theget_valueandset_valuecallbacks are implemented and assigned (decorated) to handle the data. Ifget_valueandset_valueare not implemented, a default implementation will be provided that always defaults thedataargument to the callback as beingNone.
As mentioned earlier, a member of Python object will be set for each declared event, and this member allows using the event.
class MyDevice3:
cls = simics.confclass(classname="my_device3")
cls.attr.callback_args(
"[s*]", default=list, doc="arguments to each call to 'callback'")
cls.event.my_event(saved=False, description="nice weather")
@cls.event.my_event.callback
def callback(self, data):
self.callback_args.append(data)
Create an object and a clock. Set ‘event’ to the member used to access the event run-time functionality.
simics.SIM_add_configuration([
simics.pre_conf_object("device", MyDevice3.cls.classname),
simics.pre_conf_object("clock", "clock", freq_mhz=100)], None)
device = conf.device
clock = conf.clock
event = device.object_data.my_event
Post an event to trigger after 1 cycles and data “1”. Here the clock is specified as an argument.
event.post(clock=clock, cycles=1, data="1")
Set the ‘queue’ attribute in the object. After this, we don’t have to specify the clock argument in each event operation.
device.queue = clock
Post another event to trigger after 2 cycles and data “2”. Here we make use of that ‘device’ has a clock, and we don’t need to specify this.
event.post(cycles=2, data="2")
Now we will list the posted events, identifying them with the data we used when posting.
stest.expect_equal(event.find_next_cycles(data="1"), 1)
stest.expect_equal(event.find_next_cycles(data="2"), 2)
Cancel the first event posted at cycle 1, and check that it is gone.
event.cancel_time(data="1")
stest.expect_equal(event.find_next_cycles(data="1"), None)
Run forward two cycles, and check that the “2” event has triggered.
cli.global_cmds.continue_cycles(count=2)
stest.expect_equal(event.find_next_cycles(data="2"), None)
stest.expect_equal(device.callback_args, ["2"])
Inheritance works as dictated by Python. However, just using inheritance will
not result in Simics (confclass) specific properties, such as attributes and
interfaces being inherited. For this, the parent parameter must be set to
the confclass class of the parent class.
class Parent:
"""Class with attribute 'a1' and method 'foo'."""
cls = simics.confclass(classname="parent")
cls.attr.a1("i", default=1)
def foo(self):
pass
class InheritPython(Parent):
"""Inherits from 'Parent', but not the Simics parts."""
@classmethod
def check(cls):
stest.expect_true(hasattr(cls, "foo"))
stest.expect_false(hasattr(cls, "a1"))
class InheritPythonAndSimics(Parent):
"""Inherit from 'Parent', including the Simics parts."""
cls = simics.confclass(classname="inherit_py_and_sim", parent=Parent.cls)
@classmethod
def check(cls):
stest.expect_true(hasattr(cls, "foo"))
stest.expect_true(hasattr(cls, "a1"))
class InheritPythonOverrideSimics1(Parent):
"""Inherit from Parent (not Simics parts) and create a Simics class."""
cls = simics.confclass(classname="inherit_py_override_sim")
@classmethod
def check(cls):
stest.expect_true(hasattr(cls, "foo"))
stest.expect_false(hasattr(cls, "a1"))