This chapter provides an overview of the workflow used when modeling devices using DML. It starts from an overview of the set up of the build environment, and moves on to actual modeling and testing. After reading this chapter you should have an understanding of the workflow used when developing device models in Simics and be ready for the details provided in the following chapters.
This section describes how to set up a functional build environment that will be used to write new devices in DML and create new configurations throughout the remaining sections of the overview. The Simics build environment depends on a working GCC compiler toolchain on Linux and Windows. On Windows, the Microsoft Visual C++ compiler can be used as well, but only for C++ modules. See chapter 3 for details.
DML provides distinct advantages for ease of model creation and performance when compared to alternative modeling languages. The DML compiler (DMLC) translates a device model description written in DML into C source code that will be compiled and loaded as a Simics module. The output of dmlc
is a set of C source and header files that can be compiled in the same way as a hand-written C module would be. Refer to the DML 1.4 Reference Manual for details related to the DML language and compiler usage not covered in this introduction.
The Model Builder product is installed together with Simics Base if a decryption key for it is provided. In the rest of this document the file system path of the Simics Base package directory of your Simics installation will be referred to as [simics]
, where also Model Builder features may exist, your "home" directory as [home]
, and your Simics project directory as [project]
. The project is where you have all your modules and scripts to set up your system in Simics.
Shell commands are indicated by lines starting with a $
sign; you can use your favorite shell (in Windows, the builtin command prompt should suffice). Most shell commands should be issued from the project directory; this is indicated by lines starting with project$
.
On the Windows platform, you need the MinGW tools installed. See the Simics Installation Guide for more information. How to set up a working Simics build environment is described in detail in chapter 3.
Any text editor can be used to write DML code, but we recommend Emacs. The Emacs DML mode is described in section 3.6.
If you install Simics Model Builder with Simics Base package, there will be example source code in [simics]/src/devices/ for many different classes of devices which can be used as the bases for writing new models in Simics. Some of the available examples are listed below:
DS12887
A Dallas Semiconductor DS12887 Real Time Clock. It is used in several X86 systems.
AM79C960
An AM79C960 Ethernet adapter. It is used in several X86 systems.
DEC21140A-dml
A PCI Fast Ethernet LAN controller.
sample-device-dml
A very simple DML device including the most common DML object types.
sample-pci-device
A simple PCI device using the DML PCI support in Simics.
sample-i2c-device
A very simple device using the i2c_device
interface.
This section describes how to write a simple memory mapped device and how to load it into Simics and test it. The example device code in this section is based on the sample-device-dml
device which can be found in [simics]/src/devices/sample-device-dml/
.
The following DML code models a memory-mapped device with a single 32-bit (4-byte) register at offset 0. Upon a read access this device will return the value 42 as the result of the operation, simultaneously printing a Simics console log message with the text "read from counter".
To compile this example device, you first need to set up a Simics project, using the project-setup
script (see chapter 3 for details). Type this to setup a project in an empty directory [project]
:
Windows
> cd [project]
project> [simics]\bin\project-setup.bat
Linux
$ cd [project]
project$ [simics]/bin/project-setup
The project will contain project-local versions of most simics scripts. It is important to always change directory to your project directory and run the local versions of the scripts, to avoid tampering with your base installation of Simics.
Make sure you have a working MinGW installation, before you try to set up the project. See chapter 3 or the Installation Guide for details.
Pass the --device=device_name
flag to the project-setup
script to create Makefiles and DML skeleton files for your new device. For example:
Windows
project> bin\project-setup.bat --device=simple_device
Linux
project$ ./bin/project-setup --device=simple_device
You will now have a directory [project]
containing (among other things) a GNU Makefile and a subdirectory named modules
, which is where your modules are located.
A Simics module is a self contained library of code which can be loaded into Simics. Each module consists of one or more Simics classes each of which implements the some functionality useful in a simulation. This document will use the term device, when referring to a class which is a model of a piece of hardware. Your new device is called simple_device and is located in [project]/modules/simple_device/simple_device.dml
. This file is only a skeleton. It implements a single register at offset 0.
Now, go to the [project]
directory and run GNU make
. By default, this builds all your modules.
The newly created device model also includes a simple test using the Simics test framework. The test framework provides tools and libraries to make it easy to check that your modules behave as expected. The test framework looks for tests in several directories in your project: test
, modules
, and targets
. We recommend that you place tests for a particular module in a subdirectory of that module's source. For example in [project]/modules/foo/test
if your module is named foo
. This way the test is kept close to the code it is testing.
You run tests in the project with the [project]/bin/test-runner
tool or by using make. The tool can also be used to list all test suites it finds in the project. For complete documentation see the Simics Reference Manual.
When project-setup
creates a new DML device, it automatically creates a test suite in the source directory for your module, with an empty test file for your device. You can run the test suite now:
project$ make test
.
Ran 2 tests in 1 suites in 0.680668 seconds.
All tests completed successfully.
We want our device to have a single register, which always reads as 42. To write a test for this behavior open [project]/modules/simple_device/test/s-simple_device.py
and change it to look like this:
import dev_util
import conf
import stest
# Create an instance of the device to test
dev = pre_conf_object('dev', 'simple_device')
SIM_add_configuration([dev], None)
dev = conf.dev
# Create a register wrapper for the register
r = dev_util.Register_LE(dev.bank.regs, 0)
# Test that reading from the register returns 42...
stest.expect_equal(r.read(), 42)
# ...even if we write something else to it.
r.write(0x4711)
stest.expect_equal(r.read(), 42)
We can now run our test to check if the device behaves as expected:
project$ make test
f
[project]/logs/tests/linux64/modules/simple_device/test/test.log:1: *** failed () ***
Ran 2 tests in 1 suites in 0.872507 seconds.
Failures: 1 Timeouts: 0
You can look at the log file to get more information about the failure, but the reason is simple: the needed functionality is not implemented yet. The next section will describe how to change the device to pass the test.
Now implement the functionality needed to pass the test. Open the generated skeleton file in your favorite text editor, and modify its contents to look like as follows:
dml 1.4;
device simple_device;
param desc = "sample DML device";
param documentation = "This is a very simple device.";
bank regs {
register counter size 4 @ 0x0000 is (read) {
method read() -> (uint64) {
log info: "read from counter";
return 42;
}
}
}
The read
template provides a method, read
, which is called when a memory
transaction performs a read at the address of the register. Registers can also
instantiate a corresponding template write
, which provides the write
method, which is called for write transactions. Instantiating either of these
templates will override the default behaviour of a register which is to acquire
its read value by reading its fields, or writing its value by writing to its
fields. It's worth looking at the DML 1.4 Reference Manual to find the details
on how defining semantics for fields and registers works.
Rebuild the module and rerun the test. You can do this in a single step since make test
automatically builds all modules in the project:
project$ make test
=== Building module "simple_device" ===
DML-DEP simple_device.dmldep
DMLC simple_device-dml.c
DEP simple_device-dml.d
CC simple_device-dml.o
CCLD simple_device.so
.
Ran 2 tests in 1 suites in 0.638387 seconds.
All tests completed successfully.
You can read more about writing functional tests for your models in chapter 16.
Congratulations you have now created your first Simics module. You can find the module binary in the [project]/host/lib
directory.
In order to interact with our device from the Simics prompt, we need to create a simple machine with our device (refer to section 27 for more detailed information about Simics configuration scripts). For now, just create the file [project]/targets/vacuum/my-vacuum.simics
with the following contents:
run-command-file "%script%/vacuum.simics"
@SIM_create_object("simple_device", "dev1")
phys_mem.add-map dev1.bank.regs 0x1000 0x100
The script above creates an almost empty machine with our sample device mapped at 0x1000
in the phys_mem
memory space. Please note the dev:port syntax, which is the preferred way of mapping a bank in memory.
We can now start the newly created machine configuration and interact with our device:
project$ ./simics targets/vacuum/my-vacuum.simics
simics> phys_mem.read 0x1000 -l
This returns value of 42 and causes the log message "read from counter" to be printed.
DML has direct support for writing log messages to the Simics logging facility, through the log
statement. The most important logging concepts are the type
and the verbosity level
of the message. The most common message types are info
and error
. The verbosity level is a number between 1 and 4, where 1 is used for important messages that should always be displayed, and 4 is used for detailed debugging messages that should only be printed when verbose logging has been requested. Error messages are always printed regardless of verbosity level setting. By default, Simics only displays messages of level 1 on the console.
In the previous example, no level was provided, which will make it default to 1. To set the level of a message, add it after the type string, but before the colon, as in:
log info, 2: "This is a level 2 message.";
To change what messages are displayed, use the log-level
command.
simics> log-level 4
This will cause all log messages with log-level 1-4 to be displayed. Now make a memory access to the device, as before:
simics> phys_mem.read 0x1000 -l
This time (apart from the "read from counter" message), you should see an info message saying something like
"Read from register regs.counter -> 0x2a"
. This is logged by the built-in code that handles register read accesses,
and such messages can be very useful when debugging a device model.
A Simics configuration consists of a machine description and a few other parts, and it is divided into a number of configuration objects. Each device instance in the configuration is represented by such an object. Any Simics configuration object has a number of attributes. An attribute is a named property that can be read or written using the Simics API. The value of an attribute can be an integer, a floating-point number, a string, an object reference, a boolean value, a list of values, or a mapping from values to other values.
Attributes are used for several related purposes, but the most important uses are for configuration and checkpointing. The internal state of a device object must be available through the attributes, so that a checkpoint of the current state can be saved by reading all the attributes and storing the values to disk. By reloading a configuration and setting all attributes from the previously saved checkpoint, the states of all devices can be restored to the checkpointed state and simulation can continue as if it had never been interrupted. When creating a new configuration, some of the state must be given an explicit initial assignment, which makes those attributes also part of the configuration. There may also be attributes that are not part of the state, in the sense that they do not change during simulation. Instead, they control the behavior of the model, such as buffer sizes, timing parameters etc. Those configuration attributes can generally not be modified once the object has been created.
Attributes can also be used as a simple interface to an object, e.g., for inspecting or manipulating the state for debugging purposes.
A DML device model usually defines a number of attributes. By default, each register
defines a corresponding attribute that can be used to get or set the register value, but more attributes can be defined by explicitly declaring them in the DML source.
Registers in DML automatically create a corresponding integer attribute, which stores the value of the register. In our example above, for the register counter
of bank regs
there will be a device attribute named counter
under the regs
bank sub-object.
The attribute can be accessed from the Simics command line. Continuing the example from the previous sections, enter:
simics> dev1.bank.regs->counter = 17
and then enter
simics> dev1.bank.regs->counter
which should print the value 17.
However, if a new memory access is performed:
simics> phys_mem.read 0x1000 -l
The message "read from counter" and the value 42 is still generated. However, entering dev1.bank.regs->counter
once again still returns 17. What is going on?
The answer is that the read
method is hard-coded to always return 42, no matter what. But this does not affect the behavior of the attribute, or the write
method. Let us try to make a write access:
simics> phys_mem.write 0x1000 0xff -l
Entering @conf.dev1.regs_counter
now prints the value 255 as expected. You can change the line
return 42;
in the program to:
return this.val;
recompile, and try the same accesses again to check how a normal register would behave. Then change the code back to return 42 for the rest of this section.
It is in fact often useful to create registers which either return a constant (usually zero), or return a value that is computed on the fly.
A full implementation of such a "synthetic" constant register could contain method definitions like the following:
method write(uint64 value) {
/* do nothing */
}
Try adding them to the body of the register, recompile, and run the example again.
The standard library file utility.dml
contains several pre-defined templates for common implementations such as this one. To use it, add the declaration import "utility.dml";
to your source file. The constant register can now simply be implemented as follows:
register counter size 4 @ 0x0000 is constant {
param init_val = 42;
}
or, if you still want to get a log message for each read access:
register counter size 4 @ 0x0000 is (constant, read) {
param init_val = 42;
method read() -> (uint64) {
local uint64 to_return = default();
log info, 1: "read from counter";
return to_return;
}
}
As you can see, the example overrides the read
method
provided by the read
template to hook in "after-read" behavior by executing
code after the default call.
It is sometimes useful to have device attributes that are not associated with any register. To define the attribute to behave as a data field, which stores a value of a simple data type such as int64
or bool
, use one of the builtin templates as follows:
attribute int_attr is int64_attr "An integer attribute";
Try adding this code to your device, either before or after the bank
, recompile and rerun the example. Enter the following command:
simics> help attribute:dev1.int_attr
This prints some information about the attribute. Note that the descriptive string you specified in the program is included in the online documentation.
You can now experiment with setting and getting the value of the attribute; e.g., entering
simics> dev1->int_attr = 4711
simics> dev1->int_attr
should print 4711.
If it is important that other parts of the device are updated whenever the value of the attribute is modified, the method set
can be overridden to perform such updates. Override the default implementation, call into the default, and perform any side-effects you need either before or after the default call. For example:
method set(attr_value_t value) throws {
local uint64 before = this.val;
default(value);
log info: "Updated from %d to %d", before, this.val;
}
Add this method to the body of the attribute, recompile and restart Simics, then try setting and getting the value of the attribute.
If you want the attribute to do things differently, such as not store the value between calls, or use a more complex data type, you need to do more work on your own, instead of using the provided simple attribute-type templates; see the DML 1.4 Reference Manual for details.
It is a relatively common scenario that setting an attribute should cause some side effect. For example, setting the "i2c_bus" attribute of an I2C device should trigger an interface call to the bus object to register the device on the bus. This side effect could also depend on the value of a second attribute. In our I2C device example, an address needs to be supplied when registering the device. This address could be that second attribute. Since the initialization order of attributes in a DML device model is undefined, this could pose a problem. In a C model the initialization order is defined, but it is usually a good idea to not depend on it.
In order to avoid these potential problems it is recommended to delay any side effects, until the finalize phase. This makes the attribute setting phase a "dumb" phase where no side effects should occur. In particular, it is strictly forbidden to call the interface of another object before this phase. The finalize phase corresponds to the post_init
method of a DML model, and the finalize
function of a C model. At that point, all attributes of all objects in the simulation has been set. After the finalize phase, the object is said to be configured.
There is a number of situations that causes an attribute to be set. The most obvious ones are when starting a Simics script or loading a checkpoint. However, connecting and disconnecting components using the Simics CLI will in most cases also cause some attributes to be set, and running the simulation in reverse most certainly does. Therefore, for most complex attributes, it is necessary to perform a conditional test on whether the object is configured or not. This can be done with SIM_object_is_configured
. If the object is configured the side effect should be executed immediately. If the object is not configured the side effect should be delayed until the finalize phase. If the side effect in post_init
/finalize
calls the interface of another object (or invokes functionality in that other object in some other way), it is necessary to call SIM_require_object
on that object before making the call. Below is an example of how to do this in DML.
dml 1.4;
device doc_attrib_init;
param desc = "sample device to show attribute init";
param documentation = "A sample device to show attribute initialization";
import "simics/devs/i2c.dml";
attribute address is uint64_attr {
param documentation = "I2C address";
method set(attr_value_t value) throws {
default(value);
log info, 1: "%s was set to 0x%x", qname, this.val;
}
}
connect i2c_bus {
param documentation = "The I2C bus that this device is connected to";
param configuration = "optional";
interface i2c_bus;
method set(conf_object_t *new_obj) {
log info, 1: "Entering setter of %s", qname;
if (obj) {
log info, 1: "Unregistering from I2C bus %s",
SIM_object_name(obj);
i2c_bus.unregister_device(dev.obj, address.val, 0xff);
}
default(new_obj);
log info, 1: "%s set to %s", qname, SIM_object_name(obj);
if (!obj || !SIM_object_is_configured(dev.obj))
return;
log info, 1: "Registering with I2C bus %s",
SIM_object_name(obj);
i2c_bus.register_device(dev.obj, address.val, 0xff, I2C_flag_exclusive);
}
}
// Stub i2c_device interface implementation to keep the bus happy
implement i2c_device {
method set_state(i2c_device_state_t state, uint8 address) -> (int) {
return 0;
}
method read_data() -> (uint8) {
return 0;
}
method write_data(uint8 value) {
}
}
method post_init() {
log info, 1: "post_init called";
if (i2c_bus.obj) {
SIM_require_object(i2c_bus.obj);
log info, 1: "post_init: Registering with I2C bus %s",
SIM_object_name(i2c_bus.obj);
i2c_bus.i2c_bus.register_device(dev.obj, address.val, 0xff,
I2C_flag_exclusive);
}
}
The example above shows a very simple I2C device. It is actually so simple that it does not do anything, all methods where the actual functionality of a real device would be implemented are just stubs. However, this simple device demonstrates how to connect a device to a bus in a correct way. When the i2c_bus
attribute is set, the device should register to the I2C bus object by calling the register_device
of its i2c_bus
interface. If it is already connected to another bus, it should unregister from that bus first. Note that to register to the bus, the address
attribute is needed, so there is a possible attribute initialization order problem.
This is solved in the set
method of the i2c_bus
connect. Before going ahead and calling the interface function, the device checks if it is configured. If it is not, nothing is done, since it is not allowed to call interface functions of other objects at this point. As if that were not enough, we cannot even be sure that the address
attribute has been set yet, and that attribute is needed to register.
Since the device is not configured, we know that sooner or later, its post_init
method will be called, and here we can call the register_device
interface function, but only after making sure the bus object is ready to handle the call, using SIM_require_object
.
Back to the set
method of the i2c_bus
connect. If the device is configured, the i2c_bus object has been configured as well, and the address
attribute has already been set, so all that remains is to call the interface function to register to the bus. If the device was already registered to another bus it must first unregister from it. This was done in the same method, before the default call.
A number of things has been left out of this device to keep the example short and comprehensible, things that should be included in a production device model. For example, there are no checks that bus registration succeeds, and there are no checks for exceptions after calling SIM_*
functions. Furthermore, this model does not support changing its address when connecting to another I2C bus, since it has to be unregistered from the old bus with the same address as it was registered with.
In general, it is not a good idea to have logging in attribute setters like we have in the device above. When running the simulation in reverse, attributes will be set very often and the logging would cause a lot of text with no informative value to be printed on the Simics console. In this example, the log messages are there for demonstrative purposes.
Running the simple test case below illustrates how this works.
print('Create object')
bus1 = pre_conf_object('bus1', 'i2c-bus')
bus2 = pre_conf_object('bus2', 'i2c-bus')
dev = pre_conf_object('dev', 'doc_attrib_init')
dev.i2c_bus = bus1
dev.address = 0x47
SIM_add_configuration([dev, bus1, bus2], None)
if conf.bus1.i2c_devices != [['dev', 0x47]] or conf.bus2.i2c_devices != []:
SIM_quit(-1)
print("Switching to bus2")
conf.dev.i2c_bus = conf.bus2
if conf.bus2.i2c_devices != [['dev', 0x47]] or conf.bus1.i2c_devices != []:
SIM_quit(-1)
print("Test passed")
The output should look something like the following. Note that in this example, the i2c_bus
attribute was indeed set before the address
attribute:
Create object
[dev info] i2c_bus setter about to be called
[dev info] i2c_bus set to bus1
[dev info] address was set to 0x47
[dev info] post_init called
[dev info] post_init: Registering with I2C bus bus1
Switching to bus2
[dev info] i2c_bus setter about to be called
[dev info] Unregistering from I2C bus bus1
[dev info] i2c_bus set to bus2
[dev info] after_set: Registering with I2C bus bus2
Test passed
Normally the act of setting an attribute should not produce any side effects. This ensures proper operation during reverse execution. When introducing attributes with side effects it is important to consider reverse execution; where attributes are frequently set from previously stored values. For example, it is not allowed to change any state that can be observed by the target software. For example, side effects that should not be performed are, raising interrupts or modifying registers that have their state stored in other attributes. A typical side effects that is performed is to handle connection of objects such as links. This will not cause any software visible state changes and are thus allowed.
This section includes an overview how to use interfaces in Simics. As this part of the overview deals with more advanced topics, you might prefer to skip this section and come back to it when you want know more about interfaces and connections between objects in Simics.
Section 5.5 includes a more light-weight description of how to use interfaces and can be uses as reference.
In addition to attributes, Simics configuration objects can have interfaces, which are sets of methods with specific names and types. An object can also implement port interfaces. Each port has a name and implements one or more interfaces. This makes it possible for a device to implement the same interface more than once, but with a separate name for each implementation.
A Simics configuration consists of a number of interacting configuration objects; for example, the machine vacuum
consists of a clock, a memory space, and a RAM. The clock acts as a pseudo-CPU and is needed to drive time. Each of these is represented as a separate configuration object, and the interaction between the objects is done through interfaces.
To take a concrete example: when a CPU wants to make a memory access, the CPU will look up which object represents its physical memory (via an attribute) and call the access function of the memory_space interface implemented by that object.
A very fundamental question in this context is; how does one object find another object? Usually, objects are connected through attributes; e.g., a CPU object could have an attribute that holds a reference to a memory space object, and the memory space object has an attribute that contains mapping information, which includes references to the mapped objects, and so on. Such bindings are typically set up in the components or the configuration scripts for a simulated machine, and are not changed after the initialization is done.
DML has built-in support both for letting your device implement any number of interfaces, and for connecting your device to objects implementing particular interfaces.
In general, the DML built-in constructs and standard libraries are successful at hiding details of the Simics API from the user.
The following is a simple implementation of the signal
interface, designed to be used with the example device in section 4.2.1. Note the use of an attribute for storing raised
so that the value can be checkpointed.
attribute raised is uint64_attr "current signal value";
implement signal {
method signal_raise() {
raised.val += 1;
log info: "Signal raised to %u", raised.val;
if (raised.val == 0)
log error: "Too many raises (counter wraparound)";
}
method signal_lower() {
if (raised.val == 0)
log error: "Too many lowers (counter wraparound)";
raised.val -= 1;
if (raised.val == 0)
log info: "Signal cleared";
else
log info: "Signal lowered to %u", raised.val;
}
}
The signal
interface is for instance used for interrupt signals. A device connected to another device implementing the signal
, calls signal_raise()
to raise the signal from low to high, and signal_lower()
to lower the signal from high to low.
The method declarations within an implement
section are translated directly into C functions as expected by the Simics API; a pointer to the device object itself is automatically added as the first argument to each function. The methods can only have one or zero output parameters, which correspond directly to return values in C. In this example, the C function signatures for signal_raise
and signal_lower
are:
void signal_raise(conf_object_t *obj);
void signal_lower(conf_object_t *obj);
A DML device can implement port interfaces by placing one or more implement
sections inside a port
section.
For more details about Simics interfaces and data types see the API Reference Manual.
The standard way to connect caller and callee via an interface is done by having an object attribute in the caller pointing at the callee. The attribute is typically set up in an initialization script or component. Although it is possible to write an attribute
definition, suitable for connecting an object with a particular interface by hand, it is much better to use a connect
definition in DML, which creates such an attribute with minimal effort. It also allows you to connect to an object or a port of an object without any additional work.
An interface, in Simics, is a struct
containing function pointers, and the definition of the struct
must be visible both to the caller and the callee. The convention in the Simics API is to use a C typedef
to name the struct
type, using the suffix _interface_t
, and the DML compiler by default follows this convention when it generates interface-related code. For example the io_memory
interface is described by a data type io_memory_interface_t
, which is a struct
containing two function pointers map
and operation
. If the user wants to create new interfaces, he must write his own struct
definitions; this is demonstrated below.
In the following example a second device is created and connected to the first device via a user-defined interface. Start with the example device in section 4.2.4. In our test of the connection in section 4.3.3.4 we have also assumed the device was renamed "connect_device" by changing the device declaration on the second line of code. Add the following declaration:
connect plugin {
interface talk {
param required = true;
}
}
Replace the line "log info: ā¦;
" with the following C function call:
plugin.talk.hello();
Note that the first argument to the C function is omitted, since it is assumed to be the object that is providing the interface. This is standard in most interfaces used in Simics.
The device will now have an attribute named plugin
, which can hold object or port references; the attached objects are required to implement the talk
interface. However, the module can not yet be compiled to module, since it is missing the definition of the interface.
When writing several related models, it is often useful to share code between them to reduce code duplication. The two kind of files that are most often shared are
As an example of sharing DML code, we will show how to define an interface and use the same definition in several models.
First, create the file [project]/include/talk.dml
with the following contents:
dml 1.4;
typedef struct {
void (*hello)(conf_object_t *obj);
} talk_interface_t;
This typedef will create a new struct type that is also exported to a C header so it can be used by any C code including that header file..
Then, add this line to the example device code, to import the new file:
import "talk.dml";
Finally, edit the Makefile
for the example device: [project]/modules/simple_device/Makefile
, and add the following option to the definition of DMLC_FLAGS
:
-I$(SIMICS_PROJECT)/include
in order to tell dmlc
where to look for additional include files.
You should now be able to compile the example device with the connect
added as described above.
Sharing C header files is similar to the above: just add a C compiler "-Iā¦
" flag to the CFLAGS
variable in the makefile, and instead of the DML import
directive, use a C #include
within a header
section, as in:
header %{
#include "stuff.h"
%}
Create a new object that speaks the talk
interface, which can be used to connect to the device. For this purpose, add a new module to the project, as follows (cf. section 4.2.1):
Windows
project> bin\project-setup.bat --device=plugin_module
Linux
project$ ./bin/project-setup --device=plugin_module
Edit the generated skeleton file [project]/modules/plugin_module/plugin_module.dml
to look like this:
dml 1.4;
device plugin_module;
param documentation =
"Plugin module example for Model Builder User's Guide";
param desc = "example plugin module";
import "talk.dml";
implement talk {
method hello() {
log info: "Hi there!";
}
}
The only way to use the objects of this class is through the talk
interface - there are no memory-mapped registers or similar connections.
Also edit the device makefile: [project]/modules/plugin_module/Makefile
, and add the option -I$(SIMICS_PROJECT)/include
to the definition of DMLC_FLAGS
, just as for the first example device.
Simply running make test
(or gmake test
) from the [project]
directory should now compile both modules and run the tests. As you can see the test fails:
project$ make test
[...]
.f
[project]/logs/tests/linux64/modules/simple_device/test.test.log:1: *** failed () ***
Ran 2 tests in 2 suites in 1.274424 seconds.
Failures: 1 Timeouts: 0
The reason for the failure is that simple_device
has a new required attribute, which needs to be set to an object or port implementing the talk
. We need to update the test to make this connection. Change [project]/modules/simple_device/test/s-simple_device.py
to look like this instead:
import dev_util
import conf
import stest
# Create an instance of the devices to test
dev = pre_conf_object('dev', 'connect_device')
plugin = pre_conf_object('plugin', 'plugin_module')
dev.plugin = plugin
SIM_add_configuration([dev, plugin], None)
dev = conf.dev
# Create a register wrapper for the register
r = dev_util.Register_LE(dev.bank.regs, 0)
# Test that reading from the register returns 42...
stest.expect_equal(r.read(), 42)
# ...even if we write something else to it.
r.write(0x4711)
stest.expect_equal(r.read(), 42)
Now the tests pass:
project$ make test
[...]
..
Ran 2 tests in 2 suites in 1.542259 seconds.
All tests completed successfully.
To make it easy to integrate a device model in a complete simulated system it should be wrapped in a component. A component is a Simics concept. Each component represents a hardware unit in the system: PCI devices, motherboards, disks, etc.
The main advantage of components is that they provide a high level view of the system. Instead of performing a lot of low level connections between device models ā some which reflect the hardware, and some which are artifacts of the way the models work ā you connect logical high level components: PCI cards, Ethernet devices, motherboards, and so on.
If a device model is part of an SoC or board in the hardware it is part of the SoC or board component, but if it is not part of any other such part it is wrapped in its own component. This is often the case with for example PCI devices.
Components are connected to each other via connectors. Each connector has a type and a direction. Common types are Ethernet ports, PCI slots, and serial connections. The direction of the connectors are similar to how physical plugs work: you can only insert a male plug in a female plug, not another male plug.
Connecting at this high level removes a lot of potential for error, and the components only allow connections which make sense: for example you cannot insert a PCI connection in an Ethernet port.
This section describes how to write a simple component for a PCI device. A more detailed description about components can be found in chapter 24.
Here is a very simple PCI component:
import simics
from comp import StandardComponent, SimpleConfigAttribute, Interface
class sample_pci_card(StandardComponent):
"""A sample component containing a sample PCI device."""
_class_desc = "sample PCI card"
_help_categories = ('PCI',)
def setup(self):
super().setup()
if not self.instantiated.val:
self.add_objects()
self.add_connectors()
def add_objects(self):
sd = self.add_pre_obj('sample_dev', 'sample_pci_device')
sd.int_attr = self.integer_attribute.val
def add_connectors(self):
self.add_connector(slot = 'pci_bus', type = 'pci-bus',
hotpluggable = True, required = False, multi = False,
direction = simics.Sim_Connector_Direction_Up)
class basename(StandardComponent.basename):
"""The default name for the created component"""
val = "sample_cmp"
class integer_attribute(SimpleConfigAttribute(None, 'i',
simics.Sim_Attr_Required)):
"""Example integer attribute."""
class internal_attribute(SimpleConfigAttribute(None, 'i',
simics.Sim_Attr_Internal |
simics.Sim_Attr_Optional)):
"""Example internal attribute (will not be documented)."""
class component_connector(Interface):
"""Uses connector for handling connections between components."""
def get_check_data(self, cnt):
return []
def get_connect_data(self, cnt):
return [[[0, self._up.get_slot('sample_dev')]]]
def check(self, cnt, attr):
return True
def connect(self, cnt, attr):
self._up.get_slot('sample_dev').pci_bus = attr[1]
def disconnect(self, cnt):
self._up.get_slot('sample_dev').pci_bus = None
The example component code can be found in [simics]/src/components/sample-components/
.
Components in Simics are written in Python. How to create components will not be explained in depths here. Only the most important parts will be discussed, see chapter 24 for a complete reference.
In the example, the module is named sample-components and the component is named sample_pci_card
. The StandardComponent is the base Python class which includes the basic functionality required for a component. Our class definition starts with a Python docstring which will be used as a class description. The _class_desc
is a shorter description of the class. This component only has one connector and it is a PCI connector. The component can be connected to other components with connectors of the type pci-bus
. The connector is set to be non hot-pluggable, meaning that you can not connect and disconnect the PCI card at anytime and expect it to work. USB components are for instance hot-pluggable.
In the add_objects()
function all objects which are part of the component are defined. The default object attributes are also set here. The example component contains a sample_pci_device
device called sample_dev
.
The connector of the type pci-bus
is added in add_connectors()
. It creates a connector object in its component's namespace through a slot called 'pci_bus'. Note that connectors must be instantiated even if the component has been instantiated. See section 24.6.10.2 for the reason.
The class will automatically be registered in Simics and instances can be created based on it. The component will also get new-
and create-
commands with the class name as suffix with underscores replaced by dashes.
To compile this component you need to set up a project. You can use the same project you created in 4.2.1. To create a new skeleton component named foo_component use the following command:
Windows
project> bin\project-setup.bat --component=foo_component
Linux
project$ ./bin/project-setup --component=foo_component
Or to copy the already existing sample_component
component, use the following command:
Windows
project> bin\project-setup.bat --copy-module=sample-components
Linux
project$ ./bin/project-setup --copy-module=sample-components
Now, go to the [project]
directory and run GNU make
. By default, this builds all your modules.
Note that this component is dependent on the sample-pci-device
class, and you cannot create a sample-pci-card
without it.
Now it is time to test the PCI card. The PCI card will be connected to the Firststeps
machine. This requires that you have installed the QSP-x86 Package.
Load the configuration:
project$ ./simics targets/qsp-x86/firststeps.simics
Write this on the Simics console after loading the configuration:
simics> load-module sample-components
simics> $card = (create-sample-pci-card integer_attribute = 13)
simics> connect "board.mb.sb.pci_slot[0]" $card.pci_bus
simics> instantiate-components
The create-sample-pci-card
command creates a non-instantiated sample-pci-card
and connects it to the south bridge on Firststeps
machine using the board.mb.sb namespace. The instantiate-components
command instantiates the sample-pci-card
.
To list all components in this configuration type list-components
. You will see that the configuration contains many components:
simics> list-components -v
ethernet_switch0 - ethernet_switch (top: board)
------------------------------------------------------------------
device0 ethernet-link any board.mb.sb:eth_slot
device1 ethernet-link any service_node_cmp0:connector_link0
device2 ethernet-link any <empty>
sample_cmp0 - sample_pci_card (top: board)
------------------------------------------------------------------
pci_bus pci-bus up board.mb.sb:pci_slot[0]
board - chassis_qsp_x86 (top: board)
------------------------------------------------------------------
service_node_cmp0 - service_node_comp (top: none)
-----------------------------------------------------------------
connector_link0 ethernet-link down ethernet_switch0:device1
Each component has an instance name, a component name, and a top-level component pointer. The board
object points to itself as it is the top-level component in this configuration. All machines are based on a top-level component. The top-level component is the root of the component hierarchy and is often a motherboard, backplane, or system chassis.
From left to right, the columns show, for each component, the connection name, the connection type, the direction, and the connected component:connection name. The output for the example shows that the sample_cmp0
component is connected to board.mb.sb
on board
component via the pci_slot[0]
connector.
Now it is time to boot the system. Start the simulation and wait to you see login prompt. Now it possible to verify that Linux have found the PCI card. Use the lspci
command to list PCI devices:
(none):~# lspci
00:00.0 PIC: Intel Corporation 5520/5500/X58 I/O Hub to ESI Port (rev 13)
00:01.0 PCI bridge: Intel Corporation 5520/5500/X58 I/O Hub PCI Express Root Port 1 (rev 13)
...
07:00.0 Non-VGA unclassified device: Texas Instruments PCI1050
It worked! Linux found our fake Texas Instruments PCI card without any problem.