4 Overview 6 Building Well-Behaved Models
Model Builder User's Guide  /  II Device Modeling  / 

5 Programming with DML

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.

5.1 Modules, Classes, and Objects

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..

5.2 Parameters

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.

5.3 Attributes

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.

5.3.1 A Simple Example

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.

5.3.2 A Pseudo Attribute

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.

5.3.3 Attribute Errors

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 set +method encountered an error, the 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.

5.4 Banks and Registers

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.

5.4.1 Register Banks

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.

5.4.2 Registers

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;
        }
    }

5.4.3 Register Fields

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.

5.4.4 The get and set methods of 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.

5.4.5 Bank and Register Arrays

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.

5.5 Interfaces

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.

5.5.1 Using Interfaces

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.

5.5.2 Implementing an 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);
    }
}

5.5.3 Ports

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.

5.5.4 Defining a New Interface Type

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.

5.6 Templates

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 -------------------------------------------------------------

5.7 Logging

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:

A small example:

loggroup example;

method m(uint32 val) {
    log info, 4, example : "val=%u", val;
}

5.7.1 Log Groups

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.

5.7.2 Log Levels

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.

5.8 Events

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);

5.8.1 Event data

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.

5.8.2 Managing posted events

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);

5.8.3 After

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.

5.8.4 Event Example

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.

5.9 Haps

5.9.1 Providing Haps

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.

5.9.1.1 Adding a New Type

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:

- *hap* The name of the hap, which must be unique - *params* A string specifying the number of parameters of the hap and their types. The return value and the first two parameters of the callback function are always the same and are not included in the list. A valid parameter description string contains only the following type description characters: - `i` – int - `I` – int64 (64 bit integer) - `e` – exception\_type\_t - `o` – object (i.e., void\* in DML and C, and a Python object in Python) - `s` – string - `m` – memory transaction (`generic_transaction_t *` in DML and C) - `c` – configuration object (`conf_object_t *` in DML and C) - `v` – `void *` - *param_desc* space separated list of descriptive parameter names (in the same order as `params`, so that the first word is the name of the first parameter. If `param` is the empty string, `param_desc` may be None. - *index* A string describing the index value for the hap, or None if there is no index value. The meaning of indexes is up to you to define. - *desc* A human readable description of the hap. - *old_hap_obj* Always 0.

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.

5.9.1.2 Triggering a Hap

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:

- *hap* The handle to the hap type, as returned from `SIM_hap_add_type()` and `SIM_hap_get_number()`. - *obj* The object for which the condition is met. - *value* Only meaningful if the hap is indexed. The meaning is defined by you.

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)
    }
}
4 Overview 6 Building Well-Behaved Models