This chapter describes the basic concepts of DML. This is not a complete guide, see the DML 1.4 Reference Manual for more details. The intention of this chapter is to describe the basic concepts of DML to understand how to model devices which will be discussed in more detail in later sections.
DML hides many of the mechanics of interacting with Simics to make the development of device models easier. For example, DML takes care of all module initialization, so nothing needs to be performed when a DML module is loaded in Simics.
Each DML file mentioned in the module's Makefile
defines a Simics class automatically. The class name is provide by the device
statement at the beginning of the DML file:
device my_device;
DML models devices as a set of nested parts called objects. Each attribute, bank, register, etc is a separate object. The objects can contain other objects as well as methods, session or saved variables, and parameters. Many types of objects only make sense in particular contexts. A complete list of object types and the restrictions on how they can be nested is provided in the DML 1.4 Reference Manual. The set of object types is fixed and you can not extend it.
To refer to objects you give the names of all objects from the device to the sought object separated by .
, ending with the name of the sought object. Example:
the_device.my_attr.my_data = 4;
Do not confuse this concept of object with configuration objects in Simics. They are not the same.
All variables declared in the DML file are automatically defined as object-scope data, which means that DML automatically defines the class structure from which objects will be instantiated. For example:
session int link_id;
defines a link_id
variable in the object structure.
Additionally, local
variables can be declared within method bodies. These variables are not part of the object structure..
Parameters are mostly compile-time constant-valued object members. You can only set their value once. A parameter can be set to a value of any of the types integer
, float
, string
, bool
, list
, reference
or undefined
. The type is automatically set from the value. To declare a parameter use the param
keyword:
param some_parameter = "the value of the parameter";
In addition, in some cases a parameter can be given an explicit type. This will make it part of the type of whatever template it is declared within.
param some_parameter : uint64;
Each object also declares some parameters automatically. Read the DML 1.4 Reference Manual for a complete list.
In code you refer to parameters by directly by using their name.
method some_method {
log info: "some_parameter: %s", some_parameter;
}
In section 5.6 you can read more about how parameters interact with templates.
Registers defined in the DML files are automatically registered as both object structure variables and attributes. The line:
register aprom_0 size 1 @ 0x00 "Address PROM (MAC address)";
will define a variable in the object structure that contains the value of the register aprom_0
. It will also define a corresponding attribute so that the state of the register can be saved and restored during checkpointing.
You can also manually add attributes in DML. All that is required is an attribute declaration, including name and type. If the type of the attribute is simple then using a built-in template is advised, this will
setup the storage for the attribute and provide default set
and get
methods.
To understand attributes in DML, please first refer to section 4.2.7 which gives an overview of attributes. When programming in DML it is especially important to make sure that the attribute initialization order is correct. If this is not the case some Simics features such as checkpointing and reverse-execution may not work; this is covered in detail in section 4.2.7.3.
The simplest possible attribute holds the value of a simple data type and allows the attribute to be read and written without any side effects. Let us take the example of a counter attribute:
attribute counter is int64_attr {
param documentation = "A sample counter attribute";
}
The int64_attr
template provides the necessary semantics
for a simple int64
type attribute. It tells Simics type
system to check the value in set and get operations
for an integer type, and sets the internal representation of the
attribute value. There are a few other built-in templates that provides
similar functionality for some basic types. See the
DML 1.4 Reference Manual for details.
When the data type of an attribute is more complex, the type
parameter
must be set, and the set
and get
methods must be
provided. Here is an example of this;
param type = "n";
method set(attr_value_t val) throws {
// [...]
}
method get() -> (attr_value_t) {
// [...]
return SIM_make_attr_nil();
}
The n
type simply means a "null" type that cannot be assigned a
value.
A slightly more complicated example is a pseudo attribute which, when setting values, will add to the value of the counter, and for which getting is an error.
attribute add_counter is write_only_attr {
param documentation = "A sample pseudo attribute";
param type = "i";
method set(attr_value_t val) throws {
counter.val += SIM_attr_integer(val);
}
}
Here, the write_only_attr
template informs simics that the attribute
cannot be read, and will provide the necessary get
method for you.
We cannot use both the write_only_attr
and the int64_attr
templates
since they have conflicting definitions of get
, so we must provide
the type
param and a custom set
method.
Note that no type check is required in the set
method, since the type i
is unambiguously checked by Simics before calling the set
method such that only integer values can be assigned.
DML provides generic error handling for attributes whose types are simple
enough to use one of the built-in templates. The default behaviour of these
templates is for the get
to be equivalent to a C/Python attribute
getter function that returns an attr_value_t
of the appropriate
value kind, and for the set
method to verify that the value is
in-bounds for the type, and setting an appropriate attribute error if the
value is incorrect. For most attributes it is recommended to use one of
the built-in templates.
For more complex attributes as described in section 5.3.2, where the implementer provides custom get
and set
methods, these methods are responsible for handling and returning any error that may result from the attribute access. They become strikingly similar to how an attribute access function written in C, and most attribute error handling concepts from C can indeed be directly translated to DML. Section 14.4.3 contains the details about attribute errors in C and Python.
The difference between DML and C is the return value of the methods. To signal that a DML attribute throw
statement is used. The effect is that the method is stopped immediately at the point of the throw. The implementer has the option to specify a message to provide details about the error using SIM_attribute_error
before throwing.
DML uses registers and banks to model hardware registers. Banks represent continuous address ranges containing registers. The registers are mapped in their banks with an offset and size. A bank can also contain registers without an offset. These registers are not accessible with memory operations.
A register can be further split into fields. Each field is a range of bits in the register. The value remains stored in the register, the field merely contains a reference to it.
A register bank (or simply bank) is an abstraction that is used to group registers in DML. A bank is defined by using the keyword bank
. A device can have one or more banks. Each bank can be individually mapped in a memory space by specifying the name of the bank as the function to map. This is described in chapter 23.
The same bank can be defined several times. Doing this often helps when looking at the code for a large device with many registers. For example, the first definition at the top of the file only list all registers and their offsets. Later the bank is defined again, but this time with the register functionality. Try splitting up your bank like this:
bank regs {
register r size 4 @ 0x0000;
}
//[...]
bank regs {
register r is read {
method read() -> (uint64) {
log info: "read from r";
return 42;
}
}
}
Note that you can only set register size and offset once.
Registers in DML contain integer values, which are unsigned. The most important parameters for registers are their size
and offset
. You can specify these parameters as any other, but it is easier to use the shorthand notation:
register r size 4 @ 0x1000;
This defines a register r
of size 4 which is mapped at offset 0x1000
in the register's bank. Memory accesses to this location will access the register. The default behavior of registers is to return the register's value when read and set the register's value when written. This behavior can be changed by overriding the
write_register
or read_register
methods. For details
on these methods, see the DML 1.4 Reference Manual.
register r size 4 @ 0x1000 {
method read_register(uint64 enabled_bytes, void *aux)-> (uint64) {
log info: "Reading register r returns a constant";
return 42;
}
method write_register(uint64 value, uint64 enabled_bytes, void *aux){
log info: "Wrote register r";
this.val = value;
}
}
A more simple way of modifying the behavior is to use the
read
or write
templates, and then overriding
the corresponding read
or write
methods.
register r size 4 @ 0x1000 is (read, write) {
method read () -> (uint64) {
log info: "Reading register r returns a constant";
return 42;
}
method write (uint64 value) {
log info: "Wrote register r";
this.val = value;
}
}
Real hardware registers often have a number of fields
with separate meaning. Registers in Simics also support fields. Let us assume bit 0
in register r
is a status bit and bits 1-4
are a counter. It would look something like this:
bank regs {
register r size 4 @ 0x0000 {
field status @ [0];
field counter @ [4:1] is read {
method read() -> (uint64) {
log info: "read from counter";
return default() + 1;
}
}
}
}
method init() {
// [...]
if (regs.r.status.val == ENABLED) {
// [...]
}
}
Using field names instead of doing bit-slicing on the register helps to understand what is happening in the device when reading the code. It is also possible to write special methods for the fields.
Fields support some of the same methods and templates as a register.
The most common methods are get
, set
. While secondarily
the read
and write
methods provided by the similarly named
templates are also common. These methods behave the same for fields as for
registers.
The get
and set
methods of a register are used when you access the register as an attribute. Implementations of these methods should not have any side effects apart from getting and setting the register's value. The default behavior of the methods depends on whether the register has any fields or not. For registers without fields the methods work the same way as the corresponding methods for attributes. For a register with fields the methods take care of calling the corresponding methods of the register's fields. The get
method merges the results from the calls to the fields' get
methods and the set
method splits the value into one part for each field and sends the parts on to the fields' set
methods. Note that for fields where the set
or get
templates are not instantiated, the register will not call into their corresponding methods as a matter of optimization.
In DML it is possible to define rows of registers or banks as register and bank arrays. The sample code below defines a bank array of size two, where each bank contains two registers in a single array.
bank func[i < 2] {
register ctrl[j < 2] size 4 @ 4 * j is read {
method read() -> (uint64) {
log info: "read from %s -> %#x", qname, this.val;
return this.val;
}
}
}
This creates four registers in total each of which has their own state (value) but shares the same behavior (methods).
Each bank in a bank array is mapped individually in memory spaces, normally in a component file, with the same name but different indexes. The following code maps the banks defined above into the memory space mem\_space
at offsets 0x100
and 0x200
, respectively.
mem_space.map = [[0x100, obj.bank.func[0], 0, 0, 0x100],
[0x200, obj.bank.func[1], 0, 0, 0x100]]
Register func[0].ctrl[0]
will then be mapped at address 0x100
, func[0].ctrl[1]
at 0x104
, func[1].ctrl[0]
at 0x200
and func[1].ctrl[1]
at 0x204
, respectively.
Bank and register arrays are apt for modeling devices containing several identical units where each of the units performs the same functionality but operates independently to each other. An example of this is the virtual functions in PCIe SR-IOV devices.
Interfaces is the mechanism used in Simics when Simics objects, such as device models, need to communicate with each other. A DML device can both implement interfaces to provide additional services which other devices and objects can call, and call methods in interfaces implemented by other objects. This section describes how to do this in DML.
Using an interface in a module implemented in DML, is done by connecting an object to the device model you are developing, specifying which interfaces you are planning to use.
The connect
section performs two things at the same time: it defines an attribute that can take an object or an object and a port name as its value, and it tells the DML compiler that a number of interfaces belonging to this object or port can be used in the current device model.
The following code will create an irq_dev
attribute that accepts as a value only objects or ports implementing the signal
interface.
connect irq_dev {
param documentation = "The device that interrupts are sent to.";
param configuration = "required";
interface signal;
}
Once an object has been connected, using the interfaces that were specified is simple:
// [...]
if (!irq_raised.val && irq_dev.obj) {
log info, 3: "Raising interrupt.";
irq_dev.signal.signal_raise();
}
// [...]
To connect the created attribute set it to either a configuration object implementing the correct interfaces or a configuration object and the name of a port in that object which implements the interfaces.
Here is a Python example how to do the connection to an object:
dev.irq_dev = intc
And here is an example showing how to connect to a port:
dev.irq_dev = [intc, "input_levels"]
In both examples dev is the object implementing the connect, and intc is an object implementing the signal
interface. In the second example input_levels is the name of the port in intc implementing the interface.
Implementing an interface in DML is done with the implement
declaration, which contains the implementation of all the functions listed in the interface. The interface is automatically registered by the DML compiler so that other objects can use it on the current device model:
implement ethernet_common {
// Called when a frame is received from the network.
method frame(const frags_t *frame, eth_frame_crc_status_t crc_status) {
if (crc_status == Eth_Frame_CRC_Mismatch) {
log info, 2: "Bad CRC for received frame";
}
receive_packet(frame);
}
}
A device can use interface ports
to have several implementations of the same
interface. The ports have names that can be used to select the implementation
when connecting to the device. Use a port
declaration in DML to define a new
port. See example:
port pin0 {
implement signal {
method signal_raise() {
log info: "pin0 raised";
}
method signal_lower() {
log info: "pin0 lowered";
}
}
}
port pin1 {
implement signal {
method signal_raise() {
log info: "pin1 raised";
}
method signal_lower() {
log info: "pin1 lowered";
}
}
}
Every bank declaration also acts as a port, which means that interfaces implemented inside a bank belong to the port defined by the bank. This allows to map each register bank separately.
The DML compiler will create a port object for each DML port and bank. These are automatically created sub objects of the device object that are specified when connecting other devices to the port or bank. See 34 for more information about port objects.
Port objects for DML ports are created in a port
namespace under the device object. For example, if there is a DML port named irq
in a device object named pic
the full name of the port object will be pic.port.irq
.
These port objects are specified when connecting other objects to the port. For example, connecting the irq output of a UART object (perhaps implemented as a DML connect in that device) to the irq port of our pic
object may look like this in the component code:
uart.irq = pic.port.irq
Port objects for DML banks are created in a corresponding bank
namespace under the device object.
The Simics API defines a number of useful interface types, but sometimes they are not enough, for example if you are using a bus type that is not supported by the predefined interface types.
To define new interfaces you should create a new interface module. This is described in chapter 11.
Templates are a powerful tool when programming in DML. The code in a template can be used multiple times. A template can also implement other templates. Templates are commonly used on registers, but they can be used on all DML object types. Here is a simple template:
template spam is write {
method write(uint64 value) {
log error: "spam, spam, spam, ...";
}
}
bank regs {
// [...]
register A size 4 @ 0x0 is spam;
Register A will write spam, spam, spam
to the console when someone writes to it.
Templates in combination with parameters are even more powerful:
template lucky_number is read {
param extra_1 default 1;
param extra_2;
method read() -> (uint64) {
local uint64 value = this.val * extra_1 + extra_2;
log error: "my lucky number is %d", value;
return value;
}
}
bank regs {
// [...]
register B size 4 @ 0x4 is lucky_number {
param extra_2 = 4711;
}
The extra_1
parameter has a default value so there is no need to define it in B
. But extra_2
must be defined in B
as it does not have a value set. The DML compiler will return an error if extra_2
is not set, forcing everybody using the template to set it.
The DML library contains many standard templates which can be used on registers and fields. The most common ones are the read
and write
templates which provides simple access points (the read
and write
methods) to modify the behaviour of registers or fields. The DML 1.4 Reference Manual lists all standard templates and their functionality.
In addition to facilitating code-reuse by defining templates for common functionality, templates can also be used as a sort of "inheritance" mechanism in DML. For example, two devices may be almost similar except for some parameters and a few functional differences. In this case the common functionality can be implemented as a template, to be shared between the devices where the differences is abstracted out to parameters and methods that are specialized in the two (or more) devices. In a somewhat artificial example, a device may have several banks that have registers that byte-swaps any value written to them. The banks may have different ways to control if the swapping should take place. In this case it is possible to implement the common functionality in a template and to specialize the individual banks. The listing below shows an example of such a device, with comments inlined.
dml 1.4;
device byte_swapper;
param desc = "byte swapper";
param documentation = "This device implements byte swapping functionality";
// This is a template that implements basic byte swapping
// functionality. An object implementing this template must define
// the should_swap and get_name methods.
template swapper {
// Swap value and returns the result. Swapping will only
// take place if should_swap returns true. It will also write a
// log message based on the get_name method.
method swap(uint32 value) -> (uint32) {
if (should_swap()) {
log info, 2: "Swapping in %s", get_name();
value = (value & 0xff) << 24 | (value & 0xff00) << 8
| (value & 0xff0000) >> 8 | (value & 0xff000000) >> 24;
}
return value;
}
}
// This template implements a general swap enable/disable
// functionality through a configuration register. An object
// implementing this template must define a register named CONF with a
// one-bit bit-field named SWAP.
template swap_conf {
method should_swap() -> (bool) {
return CONF.SWAP.val == 1;
}
}
// This template is a bank-template and implements a register bank
// with a configuration register and a byte-swapping register. It also
// implements the swapper template, it ISA swapper.
template swap_bank {
is swapper;
is bank;
param register_size = 4;
param byte_order = "little-endian";
register CONF @ 0 {
field SWAP @ [0];
}
register SWAP @ 4 is read {
method read() -> (uint64) {
return swap(default());
}
}
}
// swap1 bank, implements swap_bank and swap_conf. Swapping is
// controlled through the CONF.SWAP bit.
bank swap1 {
is swap_bank;
is swap_conf;
method get_name() -> (const char *) {
return "swap1";
}
}
// swap2 bank, implements swap_bank and swap_conf. Swapping is
// controlled through the CONF.SWAP bit.
bank swap2 {
is swap_bank;
is swap_conf;
method get_name() -> (const char *) {
return "swap2";
}
}
// swap_always bank, implements swap_bank template, but not the
// swap_conf template. Swapping is always enabled.
bank swap_always {
is swap_bank;
method should_swap() -> (bool) {
return true;
}
method get_name() -> (const char *) {
// Here we leverage the name parameter instead of manually
// specifying the name.
return name;
}
}
In the above example the get_name
and should_swap
methods are specialized in the various bank instances. Although for the should_swap
specialization for the bank swap1
and swap2
is done in a common template, the swap_conf
template. This is a good example of using multiple templates to build more and more specialized instances using common code. Below is a simple test case for the byte-swapper device.
import dev_util as du
import stest
# Create a sample_swap object and raise the log-level
s = SIM_create_object('byte_swapper', 's', [])
s.log_level = 4
# Create a register accessor for the CONF and SWAP registers in bank
# swap1
conf1 = du.Register_LE(s.bank.swap1, 0)
swap1 = du.Register_LE(s.bank.swap1, 4)
# Write a value to s:swap1:SWAP, it should NOT be swapped because CONF
# is 0
swap1.write(0xdeadbeef)
stest.expect_equal(swap1.read(), 0xdeadbeef)
# Now write 1 to CONF, to make the value swapped
conf1.write(1)
stest.expect_equal(swap1.read(), 0xefbeadde)
# Same thing for bank 'swap2'
conf2 = du.Register_LE(s.bank.swap2, 0)
swap2 = du.Register_LE(s.bank.swap2, 4)
swap2.write(0xdeadbeef)
stest.expect_equal(swap2.read(), 0xdeadbeef)
conf2.write(1)
stest.expect_equal(swap2.read(), 0xefbeadde)
# Now for the swap_always bank. The SWAP register will now swap
# without setting the CONF register.
swap = du.Register_LE(s.bank.swap_always, 4)
swap.write(0xdeadbeef)
stest.expect_equal(swap.read(), 0xefbeadde)
This is the corresponding test log.
=BEGIN s-swap -----------------------------------------------------------------
[s info] Write to register swap1.SWAP <- 0xdeadbeef
[s info] Read from register swap1.SWAP -> 0xdeadbeef
[s info] Write to register swap1.CONF <- 0x1
[s info] Swapping in swap1
[s info] Read from register swap1.SWAP -> 0xefbeadde
[s info] Write to register swap2.SWAP <- 0xdeadbeef
[s info] Read from register swap2.SWAP -> 0xdeadbeef
[s info] Write to register swap2.CONF <- 0x1
[s info] Swapping in swap2
[s info] Read from register swap2.SWAP -> 0xefbeadde
[s info] Write to register swap_always.SWAP <- 0xdeadbeef
[s info] Swapping in swap_always
[s info] Read from register swap_always.SWAP -> 0xefbeadde
=END s-swap 0.4 s -------------------------------------------------------------
Logging support is built into the language. Log outputs are made with the log
statement as follows:
log type[, level [ then subsequent_level ] [, groups] ]: string[, value1, ..., valueN];
where the parameters mean:
type
:
One of the identifiers:
info
: Normal informational messageerror
: Unexpected error in the model (indicates a bug in the model)critical
: Serious error that will interrupt the simulationspec_viol
: Target program violates the specificationunimpl
: Attempt to use not yet implemented functionalitylevel
:
An integer from 1 through 4, determining the verbosity level at which the message will be logged. The default is 1. This parameter has no effects if type
is either error
or critical
and may be left unspecified if groups
(see below) is not used.
subsequent_level
:
An integer from 1 through 5. If specified, all logs after the first issued will be on the verbosity level subsequent_level
. A subsequent_level
of 5 means no logging after the initial log will be done.
groups
:
One or several log groups, defined by the global declaration loggroup
. See section 5.7.1 for details. Several log groups can be combined with the bitwise or operator "|
".
string, values
:
A formatting string, as for the C function printf()
, optionally followed by a comma separated list of values to be printed.
A small example:
loggroup example;
method m(uint32 val) {
log info, 4, example : "val=%u", val;
}
Log groups help debugging by grouping log-messages based on different parts of the device functionality. Each log message is associated with a number of groups as described above, and each log object has a builtin CLI command <object>.log-group to select which groups of log messages to show. The log messages of an Ethernet device can for example be divided into different groups for the receive logic and the transmit logic. You can then choose to only see log messages for the part you find interesting when running Simics.
If a log message specifies no log group, it is unaffected by which log groups that are currently selected.
Log levels are very helpful when it comes to finding bugs or examining what is happening inside a device. The default log-level in Simics is 1
. Different log levels target different groups of users:
Here are some logging examples of DML Ethernet controller that will help you to choose the appropriate log-level.
"Receive buffer functionality is not implemented."
"Port status changed to link-up."
"Received an ARP frame with correct CRC."
"External buffer allocated at address 0x1000BEAF with 512 bytes."
In a hardware simulation, it can often be useful to let something happen only after a certain amount of (simulated) time. This can be done in Simics by posting an event, which means that a callback function is placed in a queue, to be executed later in the simulation. The amount of simulated time before the event is triggered is usually specified in a number of seconds (as a floating-point number), but other units are possible; see the DML 1.4 Reference Manual for more information about the timebase
parameter.
The callbacks and posting is handled by declaring event
objects. A simple event example looks like this:
event future is uint64_time_event {
method event(uint64 data) {
log info, 1 : "The future is here";
}
}
The uint64_time_event
template is a built-in template that provides
the necessary wrappings for an event that is posted on seconds and that takes an
uint64
as a data argument. The event()
method is
called with the posted data when the queue reaches a posted event.
There are other built-in templates for posting with other callback argument types, or other time units. See the DML 1.4 Reference Manual for more information on these.
To post an event, use the post()
method on the DML object. Note that
the signature of the post()
method depends on the event template that
is used.
// post an event 0.1 s in the future
future.post(0.1, 0);
Every posted event can be associated with data. For simple events
In the most simple cases you can use the simple_event
variants
of the built-in event templates to use no data argument, but sometimes the
callback needs some more information about the action to be performed.
When posting an event using the post
method,
data can be provided depending on which built-in template variant was used.
If you need more advanced data types than the ones provided by the built-in
templates, you must use the custom_event
variant of the the template.
The data will then become a generic data pointer that you can re-cast as needed.
To support checkpointing, all custom events must implement a few more methods.
The get_event_info
is called when
creating the checkpoint to convert the event data to an attr_value_t
that can be stored in the checkpoint. When restoring from a checkpoint, the set_event_info
method is called to convert from an attr_value_t
to a data pointer that can be passed to the event callback.
If the data pointer points to newly allocated memory that is only passed to the post
method, the allocated data is owned by the event. This means that it is up to the event
method to deallocate the memory. But sometimes events needs to be removed before they are reached, and events using allocated memory must implement a destroy
method that is passed the data pointer. This method should deallocate the data without performing any other action. The destroy
method may not access the Simics configuration, since it may be called while removing objects.
If you changed your mind and a posted, but not yet handled, event is no longer valid, it can be canceled by calling the remove()
method on the event object. Note that the signature of the remove()
method depends
on which built-in event template was used for the event.
future.remove(some_data);
To find out if there is an event posted but not yet handled, the method posted()
can be called, and to get the time remaining until the event will be handled, the method next()
will return the time as specified by timebase
. Again, note that the signature of both of these methods depend on which built-in event template was used for
the event.
local bool is_this_event_posted = future.posted(some_data);
local double when_is_this_event_posted = future.next(some_data);
DML also provides a convenient shortcut with the after
statement. An after
statement is used to call a DML method some time in the future. The arguments to such method must be serializable by DML, as they will be stored in checkpoints. For more information on serializable types see the DML 1.4 reference manual. Method calls posted by after can be cancelled similar to events by calling the cancel_after
method on the DML object that contained the method from which the event was posted. This will cancel after
events that have been posted by methods of that object.
// call my_method() after 10.5s
after 10.5 s: my_method();
The DML program continues immediately with the next statement following after
, i.e., the event is executed asynchronously. Refer to the DML 1.4 Reference Manual for more information.
It is possible to modify our example to post an event when the register is written to, as follows:
dml 1.4;
device sample_device;
param documentation = "Timer example for Model Builder User's Guide";
param desc = "example of timer";
import "utility.dml";
bank regs {
register delay size 4 is unmapped;
register r size 4 @ 0x0000 is write {
method write(uint64 val) {
this.val = 0;
delay.val = val;
ev.post(delay.val);
log info: "Posted tick event";
}
event ev is simple_time_event {
method event() {
r.val++;
log info: "Tick: %d.", r.val;
this.post(delay.val);
}
}
}
}
In the example, the register itself functions as a counter, which is reset to zero upon a write access; the written value is used as the delay in seconds. Once the event happens, it re-posts itself after the same interval. Note the use of the unmapped register delay
to store the delay internally. The counter could have been placed in a session field instead, or in a more realistic example the counter could have been placed in an additional register. Note that the post
method's signature is dependent on the template instantiated by the ev
event, in this case simple_time_event
means that it takes no arguments.
A device that posts events must be connected to a clock
object, which controls when the event is executed. All processor objects function as clocks. This is done by setting the queue
attribute of the device. After recompiling and restarting Simics, enter:
simics> @SIM_create_object('sample_device', 'dev1')
simics> dev1->queue = timer
simics> phys_mem.add-map dev1.bank.regs 0x1000 4
This connects your device to the clock timer, which was pre-defined by the vacuum
target.
Now enter the command continue
(or c
for short). This simply runs the simulation of the hardware. You should see no messages, since there is nothing exciting going on in the machine, except that the clock is ticking away. Press Ctrl-C
to pause the simulation and get back to the prompt.
Now write a large value to the register:
simics> phys_mem.write 0x1000 10000 -l
and enter c
again. You should see "Tick"-messages being written at fairly short intervals. Press Ctrl-C
and write a lower value to the register:
simics> phys_mem.write 0x1000 1 -l
then start the simulation again. The messages are now printed at high speed (although not ten thousand times as fast). The lesson from this is that simulated time is not strictly proportional to real time, and if a machine has very little to do, even 10,000 seconds can be simulated in a very short time.
In this simple case, the event is not associated with any data. All event that have non-NULL data pointer must implement more methods to support check pointing. See section 5.8.1 for more details.
Note that it is often bad for Simics performance to post many events; a counter such as the example device above could have been implemented in a more efficient way. See sections 2.3.3 and 2.3.2 for details.
As the Simics profiling and event viewing systems are based on listening to haps it can be useful for a device to directly trigger haps rather than relying on haps built into the memory, link, and processor models. In these situations the model has to choose between a standard Simics hap and a user defined hap. Standard haps have the benefit of documentation and predefined hap handlers. User defined haps have the advantage of specificity.
Before handlers can be notified of a new hap, the hap must be known. A new hap type is made known through registration. Registering a new hap type is done with the function SIM_hap_add_type()
. The signature is:
hap_type_t
SIM_hap_add_type(const char *hap,
const char *params,
const char *param_desc,
const char *index,
const char *desc,
int old_hap_obj);
where the parameters are:
The return value is a handle that must be saved for operations on the hap.
Example:
session hap_type_t hap_handle;
method init() {
[…]
hap_handle = SIM_hap_add_type("My_Special_Hap",
"ii",
"val1 val2",
NULL,
"Triggered when something special"
" happens in my module.",
0);
if (hap_handle <= 0) {
/× error handling ×/
[…]
}
}
This registration will be executed once for every instance of the device model, but when SIM_hap_add_type
is called with the same arguments it will avoid registering a duplicate hap type and instead return the handle of the previous registration.
Whenever the condition for the hap is met, the handlers for the hap should be notified. Triggering a hap incurs some overhead; if it occurs in performance-sensitive code, it may be beneficial to use one of the SIM_hap_is_active_obj
or SIM_hap_is_active_obj_idx
functions to check if there are any handlers prior to calling the notification function.
bool SIM_hap_is_active_obj(hap_type_t hap, conf_object_t *NOTNULL obj);
bool SIM_hap_is_active_obj_idx(hap_type_t hap,
conf_object_t *NOTNULL obj, int64 index);
where the parameter hap
is the value returned from SIM_hap_add_type()
or from SIM_hap_get_number()
if using a standard hap type. These predicates are approximate, but if they return false, there is no need to trigger the hap since no installed functions would be called.
The notification to handlers is normally done by calling one of SIM_hap_occurred()
, SIM_hap_occurred_vararg()
, SIM_hap_occurred_always()
, and SIM_hap_occurred_always_vararg()
. See the API Reference Manual for information about the differences.
int
SIM_c_hap_occurred_always(hap_type_t hap,
conf_object_t *obj,
int64 value,
...);
The parameters are:
The hap parameters will be provided as additional parameters to the function. A short example:
method some_meth(int v1, int v2) {
if (some_condition) {
if (SIM_hap_is_active_obj(hap_handle, dev.obj))
SIM_c_hap_occurred(hap_handle, dev.obj, 0, v1, v2)
}
}