The Device Modeling Language (DML) is a domain-specific language for creating fast functional transaction-level virtual platform models. The first version of DML was launched in 2005, and it has been the standard way to build device models for the Simics® simulator ever since.
The Simics use of DML as the primary modeling tool is an interesting example for the virtual platform community. The most common approach for virtual platform modeling is to use a general-purpose language (like C++ or C#) along with a modeling library and simulator API. Designing a domain-specific language is philosophically different, and in our experience DML has provided benefits for programmer productivity and model quality that provide a clear return-on-investment.
Some particular observations about the benefits of DML:
- DML code is smaller and clearer than general-purpose languages (which is to be expected from a domain-specific language).
- Maintaining code is easier, thanks to the higher level of abstraction.
- It is easy to follow good transaction-level modeling practice.
- Event-driven execution is built in.
- It encourages the use of metadata at all interface points.
- Virtual-platform-specific actions such as scheduling event callbacks and issuing log message are built-in keywords in the language, not function calls.
- The language definition is not limited to what is provided by the underlying language, making it possible to add useful constructs like multiple return values from methods.
- It automatically generates checkpoint-capable models (unless the programmer explicitly breaks checkpointing).
- Compile-time resolution is used wherever possible, to minimize run-time and storage overhead from generic and common code libraries.
- DML can support new versions of the Simics simulator framework with a simple recompile of the code – since the simulator API is handled by the compiler.
- DML code is portable across host types, as the language and Simics simulator runtime system isolate the model code from the specifics of the host.
Previously, the user base and audience for DML has been fairly limited, as it has only been available as part of the Simics simulator. That has changed. The Simics simulator is available as a public release, and the DML language and compiler have been made available as open-source on github. Taken together, these releases make very easy for anyone interested to try DML and do some device modeling with it, in a virtual platform context.
Some example DML source code being edited in Emacs*
The Background of DML
Before DML existed, device modeling for the Simics simulator was done using C or C++ and the Simics API. This resulted in large amounts of boilerplate code for mandatory tasks such as registering classes with the simulator core. Whenever such unnecessary repetition is seen, the natural reaction of a programmer is to design a little language to distill the interesting parts of the problem. DML grew from an initial C-code generator into a language.
This resulted in DML 1.0, which was released in 2005. The second version, DML 1.2, was released in 2007. It worked very well, but over time the need for a major overhaul became clear. The current version, DML 1.4, had its first release in 2019. It marked a distinct change compared for DML 1.2, overhauling much of the syntax and refining and defining the language semantics. Since then, there has been a steady stream of additions to the language, with more in the pipe.
The DML compiler generates C code with simulator API calls. Models written in DML can be mixed in the same Simics simulator session as models written in plain C, C++, Python, and SystemC.
Device Modeling
The Device Modeling Language is a language used to model devices, i.e., individual hardware blocks. For example, a timer, serial port, interrupt controller, accelerator block, or Ethernet interface. DML assumes an event-driven transaction-level simulator where the device model is driven by a series of function calls from the outside, and in turn operate by calling other device models. Device models are typically leaves in the system hierarchy (alongside instruction-set simulators, memory maps, interconnects, and memories), and are created by a separate simulator configuration system.
The main interfaces of a device model are illustrated below. DML provides dedicated language constructs for each of those interfaces.
- Programming registers (register banks) are used by software drivers
- Connects let a device call out to other devices
- Ports receive calls from other devices
- Events are used to get callbacks at future points in time
- Attributes are used to configure the device and provide back-door access to the device state for tools and users
Example Device Declaration
Every DML device starts with a device declaration, followed by parameters (param keywords) that provide model metadata and control aspects of the device setup. The behaviors of device-level templates are also controlled and specialized using param. Standard libraries, interface declarations, and framework code used in a model are pulled in using import statements.
// DML version
dml 1.4;
// Declaring the name of the device class
device m_control;
// Metadata, as device parameters
param desc = "mandelbrot control unit";
param documentation = "Control unit for synchronizing compute units.";
// Importing general library files
import "utility.dml";
// Importing particular interfaces used in the device
import "simics/devs/signal.dml";
import "simics/devs/memory-space.dml";
import "simics/simulator-api.dml";
import "m-compute-control-interface.dml";
// Settings to use in this device model only
// Enable PCIe setup
param use_pcie = true;
// Enable use of stall performance optimization
param stall_on_status_read = false;
// Compute units: created outside this device and connected at runtime
// up to a fixed maximum
param max_compute_units = 8;
The example code used here can be found in the m-control device that is part of workshop-02 in the training package in the public release of the Intel Simics simulator.
Example Register Bank
Register banks are declared using the bank keyword. Inside each bank, there are registers and fields. For readability, it is recommended to declare the layout of a register bank separate from the implementation. The DML compiler combines all declarations related to the same register into a single implementation, allowing arbitrary code structuring.
An example register bank:
// Declaring a register bank – typically separate from implementation
bank ctrl {
param register_size = 8;
register compute_units @ 0x00 is (read_only) "Available compute units";
register start @ 0x08 is (write) "Start operation";
register status @ 0x10 "Operation status" {
field done @ [63] is (write) "Operation completed" ;
field processing @ [62] is (read_only) "Operation in progress" ;
field unused @ [61:0] is (reserved) "unused" ;
}
register _reserved @ 0x18 is (reserved) "Reserved";
// Managing the compute units
// as bit masks in registers
register present @ 0x20 is (compute_unit_bitmask_register)
"compute units present bitmask";
register used @ 0x28 is (compute_unit_bitmask_register)
"units used in current operation";
register done @ 0x30 is (compute_unit_bitmask_register)
"units used & completed operation";
}
Register layouts generated from specification files (such as IP-XACT*) are typically saved to their own source file, which is then imported into the device model. Templates (such as read_only or reserved in the code above) are used to specify common behaviors. When more complex and specific behaviors are needed, they are added in a second bank declaration that provides implementations for methods like read() and write() on registers and fields.
For example:
// Specifying the behavior in a section of code
// All partial definitions are “merged” in compilation
bank ctrl {
...
register start is write {
method write(uint64 value) {
start_compute(value); // Call method defined elsewhere
this.val = value;
}
}
...
register status {
field done is write {
method write(uint64 v) {
if (v == 1) {
if (this.val == 1) {
// Wrote a 1 bit to clear the done flag
// - Take all side-effect actions needed.
do_clear_done();
} else {
log spec_viol, 1, control:
"Attempt to clear already clear done flag";
}
} else {
log spec_viol, 1, control: "Writing zero to done has no effect";
}
}
}
Note the use of log spec_viol to report bad actions from the software. A virtual platform can check and report on what the software does, and DML makes it very easy to add usage warnings like these.
Example Connect and Port
Another core function of a device is to communicate with other devices. In DML, this is accomplished using unidirectional connections from one device to another. A device receives incoming transactions as function calls in named ports.
For example, this port is called control_in and implements the m_compute_control interface:
port control_in {
param desc = "control input from the control unit";
implement m_compute_control {
// Start an operation
method start_operation() {
log info, 2, control: "Received request to start compute job";
if (ctrl.status.processing.val == 1) {
// Getting to this error state requires that the
// external world calls start_operation() twice without
// completing the operation in the meantime.
log spec_viol, 1, control :
"Operation start request while operation in progress";
return;
}
// Note that it is OK to start a new operation if the device
// is in state "done".
start_compute_job();
}
method clear_done() {
log info, 2, control: "Received request to clear done flag";
// Sanity check
if (ctrl.status.done.val == 0) {
log spec_viol, 1, control :
"Clear done signal received when done flag is not set.";
return;
}
log info, 2, control: "Clearing done flag from signal %s", this.qname;
do_clear_done();
}
}
}
Interfaces are defined outside of the device model, since they are by nature shared between multiple devices. On the sending side of the interface, there is a connect. For example, the following declaration creates an array of connections from a device to a set of devices implementing the m_compute_control interface:
connect compute_unit_control[i<max_compute_units] {
param desc = "Connection to the compute unit control ports";
param configuration = "optional";
param internal = false; // = list-attributes shows it by default
interface m_compute_control;
// This method handles the "protocol" to a compute unit.
// Avoids putting that into the starting-operation method.
method signal_start_operation() {
if (!obj) {
// Print clear error message - even though this should not happen unless
// the user interactively breaks the simulation setup at runtime.
log error: "Model setup is inconsistent! (connect %s has no object)",
this.qname;
return;
}
// Signal the object to start computing
this.m_compute_control.start_operation();
}
method signal_clear_done() {
if (!obj) {
// Print clear error message (same logic as above)
log error: "Model setup is inconsistent! (connect %s has no object)",
this.qname;
return;
}
// Signal the object to clear the done flag
this.m_compute_control.clear_done();
}
}
When used in the Simics simulator, the connect automatically creates a configuration attribute that is used by the simulator setup system to point at the receiving object’s port.
Try It Out!
The above code examples are just a few snippets to demonstrate what DML looks like. To learn more, the best way is to download and install the public release of the Intel Simics simulator. As documented in the get started guide, there is a workshop called w02 that uses DML to model a PCIe-based accelerator. There are also many device models written in DML in the Simics base package and Simics Quick-Start Platform (QSP). The DML language is documented in the Simics simulator documentation, as well as on the github page.
The Simics simulator installation contains a ready-built DML compiler, but you can also build your own DML compiler from the sources and use it to build models in a Simics simulator project.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.