Component classes are registered with Simics using the register_component_class() function. When a component class is registered, a corresponding Simics class is created with the supplied list of attributes. The Simics class uses the name stored in the classname data member of the Python class.
def register_component_class(python_class, attributes, checkpoint_attributes = [], top_level = False)The first argument, python_class, is the Python component class, attributes is a list of attributes that can be used to configure the component at creation time. All attributes in this list will be available as command arguments in the create-
The checkpoint_attributes is a list with the same format as attributes. The difference is that checkpoint_attributes is only intended for attributes representing internal state that has to be checkpointed. The attributes in this list are not visible as arguments to the creation commands. This means that the attributes can not be of the pseudo or required kinds.
The last argument, top_level, should be set to True for top level components; the default value is False. The register function will add some additional attributes for top level components, and also check that the component class implements all top-level-only methods.
Example of a component class:
class ide_disk_component(component_object): classname = 'std-ide-disk' basename = 'ide_disk' description = ('The "std-ide-disk" component represents an IDE disk.') connectors = { 'ide-slot' : {'type' : 'ide-slot', 'direction' : 'up', 'empty_ok' : False, 'hotplug' : False, 'multi' : False}} ... <class method code removed> ... register_component_class( ide_disk_component, [['size', Sim_Attr_Required, 'i', 'The size of the IDE disk in bytes.'], ['file', Sim_Attr_Optional, 's', 'File with disk contents for the full disk Either a raw file ' 'or a CRAFF file.']])
The connectors member is a dictionary indexed by connector name. For each connector there is another dictionary describing it with the keywords type, direction, empty_ok, hotplug and multi:
There are a few class methods that have to be implemented by the component class since they are used by the base class when the component is created:
There are several methods that have a default implementation in the base component class, but that the component sub class may override.
Top-level components need two additional methods that are used by the component system to handle clock domain configuration and to keep track of all processors in the component hierarchy. There is also one optional method that is used to restore the processor list from a checkpoint.
When the component object has been created, the add_objects() method is called. Its responsibility is to create pre-configuration objects representing the configuration objects that implement the functionality of the component. The pre_obj() Python class is typically used instead of pre_conf_object(), since it provides improved name handling, as described in 12.3.11.
The pre-configuration objects should be added to the o namespace of the component. When the component later is instantiated, all object references in this namespace are converted to configuration objects.
The o namespace may contain simple pre-configuration objects, and also arrays of objects.
Example from a Serengeti processor board component:
def add_objects(self): for i in range(self.num_cores): self.o.mmu[i] = pre_obj('mmu(x + %d)' % i, self.mmuclass) self.o.cpu[i] = pre_obj('cpu(x + %d)' % i, self.cpuclass) self.o.cpu[i].processor_number = get_next_cpu_number() self.o.cpu[i].mmu = self.o.mmu[i] self.o.cpu[i].control_registers = [['mid', i]] self.o.cpu[i].freq_mhz = self.freq_mhz
After instantiation, the o namespace will contain references to the newly created Simics configuration objects instead.
For each connector, some information must be supplied to the other component of the connection. This usually includes a configuration object that implements a Simics interface, which is needed by some configuration object in the other component. The information is stored in the connector_info data member of the component, and passed to the other component in the call to its connect method. The connector_info dictionary is indexed by connector name. This dictionary should be filled in by the add_connector_info() method that is called directly after add_objects(), and is also called when restoring an instantiated component from a checkpoint.
The format of the values in the connector_info dictionary is a list in the common case. It may be a tuple as well, described in 12.3.10. The length of the list, and the types of the values, depends on the kind of the connector. The up and the down part of a connector may have different arguments in the list.
At instantiation, any pre-configuration references in connect_info will be converted to configuration object references.
A dual-function PCI device with two SCSI controllers may have the following add_connector_info() method. The first controller is implemented by configuration object sym0 and the second by sym1.
def add_connector_info(self): self.connector_info['pci-bus'] = [[[0, self.o.sym0], [1, self.o.sym1]]] self.connector_info['scsi-bus0'] = [self.o.sym0, 7] self.connector_info['scsi-bus1'] = [self.o.sym1, 7]
For the pci-bus connector type, the argument is a list with PCI function number and object implementing the PCI_DEVICE_INTERFACE interface. The scsi-bus connector type have two arguments, the object implementing the SCSI_TARGET_INTERFACE and the initial SCSI ID of the component on the SCSI bus.
For each connector type that a component implements, it must also have a corresponding connect_<connection-type>() method.
The first argument (excluding the Python self reference) to the connect method is the name of the connector. The following arguments come from the connector_info list of the other component in the connection. The connector method typically sets attributes in the objects of the component that are related to the connection.
In the example in the previous section, the connector_info for an up-directed pci-bus connector in a PCI device was shown. The corresponding connect method in the component that has a down-directed pci-bus connector may look like the following. The component in the example has four pci-bus connectors, called pci-slot0, etc.
def add_connector_info(self): for i in range(4): self.connector_info['pci-slot%d' % i] = [i, self.o.pcibus] def connect_pci_bus(self, connector, device_list): slot = self.connector_info[connector][0] bus = self.connector_info[connector][1] for dev in device_list: bus.pci_devices += [[slot, dev[0], dev[1]]]This method first reads some data for the components own connector, to know what PCI slot and bus object it has associated with the connector. It then iterates over device_list, that is the list with PCI functions and objects from the previous example. This list, together with the PCI slot number, is used to set the pci_devices attribute in the bus object.
A connect method should make as few assumptions as possible about the objects that are passed to it. This makes it easier for developers to add new components that use the same standard connector definitions, so that they can be connected to existing components.
The connect methods are called when the components are instantiated, and not when the CLI command <component>.connect, or connect-components, is invoked. This simplifies coding the connect methods, since they are only called once, and implementing disconnection of components before instantiation is simpler.
The connect methods are called starting from the top-level component and working down the hierarchy. The connect method in the lower component of a connection is called while traversing down the hierarchy downwards. When the leaves have been reached, the connect methods are called in reverse order back up the hierarchy. This way components may pass data down the hierarchy which can be used to by lower components to determine what data to pass up the hierarchy.
Sometimes the generic connector mechanism does not provide sufficient support for determining when a connection is allowed or not. For example, it may require device specific knowledge. To solve this, the component may have a check method that, unlike the connect method, is called when the connect command is invoked. The check method is called with the same arguments as the connect method by default. Optionally the other component may supply a tuple of two different argument lists. Then the first list is supplied to the connect method, and the second list to the check method.
Check methods are often used for connectors that support multiple connections, i.e. they have the multi attribute set to True. One example is the ISA bus component that has a multi-connector where the check method makes sure that ports of the attached devices do not collide.
If a check method determines that a connection is not allowed, it should raise a Python exception with a message saying why the connection failed.
Components set the names of configuration objects when the corresponding pre_obj object is created, for example:
self.o.cpu = pre_obj('cpu', 'x86-p2')The x86-p2 processor object will get the name cpu unless there already exist another object with the same name. In that case, an underscore and a unique number will be appended to the name, for example cpu_0 for the first duplicate and cpu_1 for the second.
The component can decide where in the name the unique number should appear by including a $ sign in the name. Underscores are not added in that case.
self.o.cpu = pre_obj('cpu$', 'x86-p2') self.o.tlb = pre_obj('cpu$_tlb', 'x86-tlb')The first set of objects created by this code will be called cpu0 and cpu0_tlb, and the second cpu1 and cpu1_tlb. As with the _<number> suffix, the number for the name is assigned when the pre_obj object is created.
In some cases, a simple automatic numbering of objects is not sufficient, but object names are still unknown at creation time. A typical example is a large server with processor boards and a backplane, where processors should be numbered depending on what slot of the backplane the processor board is connected to. The component system solves this by allowing simple parentheses-enclosed expressions in the object name, where x is replaced with a supplied value. This expression can be re-evaluated at anytime until the component is instantiated, by calling the rename_component_objects() method.
The following example creates four processors, numbered from 4 up to 7. The non-evaluated name is cpu(x + 0), cpu(x + 1), etc. When rename_component_objects() is called, x is replaced by 4, and the final object names will be cpu4, cpu5, etc.
for i in range(4): self.o.cpu[i] = pre_obj('cpu(x + %d)' % i, 'x86-p2') self.rename_component_objects('4')