Device Modeling Language (DML) is a domain-specific programming language for developing device models to be simulated with Simics. DML has been designed to make it easy to represent the kind of things that are needed by a device model, and uses special syntactic constructs to describe common elements such as memory mapped hardware registers, connections to other Simics configuration objects, and checkpointable device state.
DML is an object-oriented language, where each device is represented through an object, which — as members — may feature pieces of mutable state, configurable parameters and attributes, subroutines (called methods), and subobjects that may have their own members. In contrast to most general-purpose object-oriented languages, objects in DML are statically declared rather than dynamically created, and typically represent logical components of the device.
A complete DML model specifies exactly one device model, together with:
These are the crucial properties of device models that must be made visible to Simics, and each of these have specialized language features in order to declare them. Beyond these, DML also has a number of features to improve the expressive power of the language and simplify development; for instance, DML offers templates, a powerful metaprogramming tool that allows for code reduction and reuse, as well as a means of building abstractions.
The DML compiler is called Device Modeling Language Compiler,
dmlc
. It translates a device model description written in DML into C
source code that can be compiled and loaded as a Simics module.
This document describes the DML language, the standard libraries, and
the dmlc
compiler, as of version 1.4 of DML. See also Simics Model
Builder User's Guide for an introduction to DML.
The following is an example of a small DML model defining a very simple device. This lacks many details that would appear in a real device.
dml 1.4;
device contraption;
connect wakeup {
interface signal;
}
bank config_registers {
register cfg1 size 4 @ 0x0000 {
field status @ [7:6] is (read, write) {
method read() -> (uint64) {
local uint2 ret;
ret[0] = enable.val;
ret[1] = coefficient[1] & 1;
return ret;
}
}
field enable @ [8] is (read_unimpl, write) {
method write(uint64 val) {
if (this.val == 0 and val == 1) {
wakeup.signal.signal_raise();
} else if (this.val == 1 and val == 0) {
wakeup.signal.signal_lower();
}
}
}
}
register coefficient[i < 4] size 8 @ 0x0008 + i * 8 is (read, write) {}
}
The device contraption;
statement declares the name of the device.
The connect wakeup
part declares that the device can be configured
to communicate with other devices using the signal
interface.
The bank config_registers
part declares a bank of memory-mapped
registers. If the bank is mapped into a memory space, then software
can use this to control the device through reads and writes.
The bank contains registers, which declare sizes and offsets statically. When the bank is accessed with a memory transaction, it will check which register the transaction hits, and redirect as a read or write operation in that register.
The cfg1
register is further subdivided into fields, one
covering bits 7 and 6 and one covering bit 8.
The bank, registers and fields form a hierarchy of objects, which provides a simple mechanism for encapsulation. Each object is a separate namespace, and the object hierarchy naturally forms a nested scope.
The is
statement specifies a number of templates to be instantiated for
the associated object. The read
and write
templates prepare code for the
targeted field which makes it software readable and writable, as well as
methods read
and write
that may be overridden in order to customize the
behavior upon a software read or write. In contrast, the read_unimpl
template prepares code that causes the field to log a warning if read by
software.
Methods in DML are much like functions in C. The statements of the
method body are similar to C with some differences; e.g., integers
support bitslicing using the syntax value[a:b]
. Methods also have
a place in the object hierarchy, and can freely access the object's
state and connections.
coefficient
is an array of register objects, marked by the use
of [i < size]
. The object specification provided
to an object array is used for each element of the array, and the
i
parameter can be used for element-specific logic. In this case,
i
is used to assign each register of the array to different
address offsets.
The above example demonstrates how a DML device is built from a hierarchy of objects, such as banks and register. The hierarchy is composed of the following object types:
Each DML model defines a single device
object,
which can be instantiated as a configuration object in Simics. All objects
declared at the top level are members of the device object.
An attribute
object creates a Simics
configuration attribute of the device. An attribute usually has one of three
uses:
A bank
object makes sets of registers
accessible by placing them in an address space. Register banks can be
individually mapped into Simics memory spaces.
A register
object holds an integer value, and is
generally used to model a hardware register, used for communication via
memory-mapped I/O. A register is between 1 and 8 bytes wide. Registers divide
the address space of a bank into discrete elements with non-overlapping
addresses.
field
objects constitute a further subdivision of
register
objects, on the bit level. Each field can be accessed separately,
both for reading and for writing. The fields of a register may not overlap.
group
is the general-purpose object type, without
any special properties or restrictions. Groups are mainly used as container
objects — in particular, to define logical groups of registers within a
bank. The generic nature of groups also makes them useful as a tool for
creating abstractions.
A connect
object holds a reference to a Simics
configuration object. (Typically, the connected object is expected to
implement some particular Simics-interface.) An attribute with the same name
is added to the device
; thus, a connect
is similar to a simple attribute
object. Usually, initialization is done when the device is configured, e.g.
when loading a checkpoint or instantiating a component.
An interface
object may be declared within a
connect
object, and specifies a Simics interface assumed to be implemented
by the connected object. In many cases, the name of the interface is
sufficient, and the body of the object can be left empty.
A port
object represents a point where an outside
device can connect to this device. This is done by creating a separate Simics
object; if a device has a declaration port irq
and the device is
instantiated as dev
in Simics, then the port object is named dev.port.irq
.
An implement
object specifies an implementation
of a Simics interface that the device implements. An implement
object is
normally declared inside a port
, and defines the interfaces registered on
the corresponding Simics configuration object; however, implement
can also
be declared on the top-level device
object or in a bank
object.
The methods defined within the implement
object must correspond to
the functions of the Simics interface.
A device can implement the same interface several times, by creating
multiple port
objects with implement
objects of the same name.
An event
object is an encapsulation of a Simics
event, that can be posted on a time queue (CPU or clock).
A subdevice
object represents a subsystem of a
device, which can contain its own ports, banks, and attributes.
Methods are the DML representation of subroutines.
They may be declared as members of any object or template. Any method may have
multiple input parameters, specified similarly as C functions. Unlike C, DML
methods may have multiple return values, and the lack of a return value is
indicated through an empty list of return values rather than void
. The
following demonstrates a method declaration with no input parameters or return
values:
method noop() -> () {
return;
}
Alternatively:
method noop() {
return;
}
The following demonstrates a method declaration with multiple input and parameters and return values:
method div_mod(uint64 dividend, uint64 divisor)
-> (uint64, uint64) {
local uint64 quot = dividend / divisor;
local uint64 rem = dividend % divisor;
return (quot, rem);
}
This also demonstrates how local, stack-allocated variables within methods may
be declared; through the local
keyword. This is analogous to C’s auto
variable kind, but unlike C, the keyword must be explicitly given. DML features
two other variable kinds: session
and
saved
. Unlike local
variables, session
and saved
variables may also be declared as members of any object within the
DML model, and can only be initialized with constant expressions.
session
variables represent statically allocated variables, and act as the
DML equivalent of static variables in C. The value of a session
variable
is preserved for the duration of the current simulation session, but are not
automatically serialized and restored during checkpointing. This means that
it is the model developer’s responsibility to manually serialize and restore
any session
variables upon saving or restoring a checkpoint.
saved
variables behave exactly like session
variables, except the value of
saved
variables are serialized and restored during checkpointing. Because of
this, a saved
variable must be of a type that DML knows how to serialize.
Most built-in non-pointer C types are serializable, and any struct
that consists solely of serializable types are also considered serializable.
Pointer types are never considered serializable.
Methods have access to a basic exception-handling mechanism through the throw
statement, which raises an exception without
associated data. Such exceptions may be caught via the try { ... } except { ... }
statement. If a method may throw an
uncaught exception, that method must be declared throws
; for example:
method demand(bool condition) throws {
if (!condition) {
throw;
}
}
A template specifies a block of code that may be inserted into objects. Templates may only be declared at the top-level, which is done as follows:
template name { body }
where name is the name of the template, and body is a set of object statements.
A template may be instantiated through the is
object statement, which
can be used within either objects, or within templates. For example:
bank regs {
// Instantiate a single template: templateA
is templateA;
// Instantiate multiple templates: templateB and templateC
is (templateB, templateC);
register reg size 1 @0x0;
}
The is
object statement causes the body of the specified templates to be
injected into the object or template in which the statement was
used.
is
can also be used in a more idiomatic fashion together with the declaration
of an object or template as follows:
// Instantiate templates templateA, templateB, and templateC
bank regs is (templateA, templateB, templateC) {
register reg size 1 @0x0;
}
A language feature closely related to templates are parameters. A parameter can be thought of as an expression macro that is a member of a particular object or template. Parameters may optionally be declared without an accompanying definition — which will result in a compile-time error if not overridden — or with a default, overridable definition. Parameters declared this way can be overridden by any later declaration of the same parameter. This can be leveraged by templates in order to declare a parameter that the template may make use of, while requiring any instance of the template to provide a definition for the parameter (or allow instances to override the default definition of that parameter).
Parameters are declared as follows:
param name;
param name default expression;
param name = expression;
Much of the DML infrastructure, as well as DML’s built-in features, rely heavily
on templates. Due to the importance of templates, DML has a number of features
to generically manipulate and reference template instances, both at compile time
and at run time. These are templates as
types, each
-in
expressions, and in each
declarations.