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:
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 Initialization 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 arguments.# 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 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 with self.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 there are more convenient to setting 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:
default - set a default valueSim_Attr_Optional in kind if the current value is Sim_Attr_Requiredoptional - make attribute optionalSim_Attr_Optional in kindpseudo - make attribute pseudoSim_Attr_Pseudo in kindread_only - make attribute read-onlypseudo must also be set to true (see note below)write_only - make attribute write-onlypseudo must also be set to true (see note below)The reason why pseudo must be set to true when setting either read_only or write_only to true is that attributes that are saved to checkpoints (for example if Sim_Attr_Optional is 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
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.
In this example we use the signal interface, and use the cls.iface.signal.signal_raise and cls.iface.signal.signal_lower to implement the required methods.
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=[])
@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=[])
@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=[])
@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])
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"))