This chapter provides some insights into various guidelines and recommendations for how to build high quality models that integrate and perform well in Simics. You may also refer to chapter 8 for further guidance.
The intention of this chapter is to standardize how to write DML devices. This includes how to structure the source code and naming DML objects, DML files, and devices. The purpose is to learn how to produce readable and maintainable code.
The DML device name must contain the modeled device identification name. It is not recommended to name the device after the type of device (such as "watchdog" or similar), as there can be more than one device of that type in Simics.
The complete human-readable device name should be set using the device's desc
parameter.
Example: a TBD4711 watchdog device should be named TBD4711 and its desc
parameter should be set to "TBD4711 watchdog"
.
Running the project-setup
script will give you a DML skeleton file and module directory with the same name as the device and in general this pattern should be followed. Occasionally when modeling a chip with distinct logical units which are not used individually it is appropriate to model both devices in one module directory, see section 9.5 for guidelines. The other case where deviation from standard naming is appropriate is when the device name is overly long. In these cases the following rules should be followed:
TBD4711.dml
or watchdog.dml
. BAR17_watchdog.dml
or watchdog.dml
. desc
parameter Descriptive bank names are vital to make the DML code easy to read. Bank names are also used when mapping regions in memory spaces.
This is the priority list when naming a bank:
regs
to highlight that the bank contains common registers. The register field definitions can be written in several ways. Here are some examples of recommended ways to define fields.
register a size 4 @ 0x0 {
field Enable @ [0:0];
field Disable @ [4:1];
field Trigger @ [31:11];
}
register b size 4 @ 0x4 {
field Enable @ [0];
field Disable @ [4:1];
field Trigger @ [31:11];
}
register c size 4 @ 0x8 {
field Trigger @ [31:11];
field Disable @ [4:1];
field Enable @ [0];
}
The field order should always comply with the device documentation. It is otherwise hard to compare code and documentation.
It is often better to use @ [0:0]
when there are several multi-bit fields in the device. But it is better to use @ [0]
in a register with only single-bit fields.
This section proposes a DML file structure that makes DML code easy to read. Keep in mind that you have to adapt these recommendations for your own devices.
The recommended order is:
file description
A DML file should always begin with a short description of the file in a comment.
// sample-dma-device.dml - sample DML code for a Simics DMA device. In this
// case used to illustrate how to structure DML code.
version declaration
The version declaration is required. It defines the DML version this file is
written for. A device can consist of DML files written for different DML
versions.
dml 1.4;
device declaration
A device must contain a device declaration when defining a device.
device dml_structure;
device short description string
Add a short, one-line string describing the device.
param desc = "example DMA device";
device documentation string
Add a string describing the device.
param documentation =
"Example of a DMA device supporting contiguous memory or scatter-gather "
+ "lists. The device has a controllable throughput (words per second) "
+ "and supports either polling mode or interrupt based signalling upon "
+ "DMA completion.";
header
Add C code to the beginning of the generated C file. Create a new C file and
include it if there is a lot of code to add. Try to avoid adding C code if
you can.
header %{
static double compute_delay(double throttle, int count);
%}
import
Import additional DML files. Avoid using paths when importing files. It is better to add paths to the Makefile.
import "utility.dml";
import "simics/devs/memory-space.dml";
import "simics/devs/signal.dml";
#
# Example Makefile
#
[…]
DMLC_FLAGS = -I$(SIMICS_BASE)/src/import-dir
See also the documentation on EXTRA_MODULE_VPATH
in section 3.5.3.
extern
Add all extern declarations.
extern double compute_delay(double throttle, int count);
types
Declare types used in this file only. Place common declarations in a DML file and import it when it is needed.
typedef layout "big-endian" {
uint32 addr;
uint16 len;
uint16 reserved;
} sg_list_head_t;
constant
Add all constant declarations.
constant DEFAULT_THROTTLE = 1e-6;
parameter
Add all parameter declarations.
param byte_order = "big-endian";
loggroup
Create log groups for the different functional units of the device.
loggroup lg_interrupt;
connect All external connections.
connect target_mem_space {
param documentation =
"The memory space on which the DMA engine operates. Data will be "
+ "read from and copied to the memory associated with this memory "
+ "space.";
param configuration = "required";
interface memory_space;
}
attribute
Attributes for the device.
attribute throttle is (double_attr) {
param documentation =
"Delay in seconds per 32-bit word of memory copied, default is 1μs.";
param configuration = "optional";
}
bank declarations
Basic declarations of the contents of each bank. The purpose of having the bank declarations early in the file is to help code readers to get a quick overview of the device. The declaration of a bank should only contain basic declarations of its registers, by defining their names, offsets, description strings and templates. It is usually recommended to place each bank in a separate DML file, but if a device contains smaller banks that are tightly coupled, it can be better to place them in the same DML file.
bank regs {
param register_size = 4;
register DMA_control @ 0x00 "Control register";
register DMA_source @ 0x04 "Source address";
register DMA_dest @ 0x08 "Destination address";
register DMA_interrupt_posted is (unmapped)
"Internal register to track if interrupts are posted.";
}
implement
The implement declarations often require more code than for example the connect or the attribute declarations. It is therefore added after the bank declaration.
bank regs {
param register_size = 4;
register DMA_control @ 0x00 "Control register";
register DMA_source @ 0x04 "Source address";
register DMA_dest @ 0x08 "Destination address";
register DMA_interrupt_posted is (unmapped)
"Internal register to track if interrupts are posted.";
}
event
Event declarations are added to this part of the file for the same reason as implement declarations.
event delay_transfer is (uint64_time_event) {
method event(uint64 data) {
// [...]
}
}
session/saved
Always be careful when adding session variables. Such variables are not checkpointed with the rest of the configuration. Saved variables should be used instead for simple checkpointed state; however, the types of saved variables are restricted to what is considered serializable, meaning non-pointer types and (nested) structs and arrays of such types.
session bool external_disable;
Never use generic session variables for state data even if attributes save and restore the data when checkpointing. It is better to define session variables in attributes and write specific get and set methods to save the data.
attribute foo {
session int bar;
method set(attr_value_t attr) {
bar = SIM_attr_integer(attr) * 2;
}
}
[…]
foo.bar = 4;
common methods
Define device generic methods. Bank specific methods should instead be added to the bank definition.
method read_mem(void *dst,
physical_address_t src,
physical_address_t len) throws {
local exception_type_t exc;
exc = target_mem_space.memory_space.access_simple(dev.obj,
src,
dst,
len,
Sim_RW_Read,
Sim_Endian_Target);
if (exc != Sim_PE_No_Exception) {
log error: "an error occurred when reading target memory";
throw;
}
}
template
Add templates used in this file only. Place common templates in a DML file imported everywhere the template is used.
template dma_starter is (register) {
method write_register(uint64 value, uint64 enabled_bytes, void *aux) {
default(value, enabled_bytes, aux);
do_dma_transfer();
}
}
bank definition
This part defines the actual register and field functionality for all banks. Unimplemented register templates should be added to the bank declaration not to the bank definition. However, registers with unimplemented fields should be added here. Non device specific methods should be added to the bank definition. This prevents the global scope from being clobbered. The bank definition part tends to be very long compared to the bank declaration part. The bank definition part does not give a good overview of all registers.
bank regs {
register DMA_control is (dma_starter) {
field EN @ [31] "Enable DMA";
field SWT @ [30] "Software Transfer Trigger";
field ECI @ [29] "Enable Completion Interrupt";
field TC @ [28] is (write) "Transfer complete" {
method write(uint64 value) {
// [...]
}
}
// [...]
}
method complete_dma() {
// Log that completion is done
log info, 2: "DMA transfer completed";
// [...]
}
}
post_init() and init()
Add the post_init()
and init()
methods at bottom of the file if the file defines a device. Add the methods to a device generic file and call sub methods per file if the device consist of several files. Alternatively, objects can instantiate the init
or post_init
templates, which causes any init()
or post_init()
method declared within them to be called at the appropriate time.
method post_init() {
// [...]
}
method init() {
throttle.val = DEFAULT_THROTTLE;
}
Add extra C code at the bottom if needed.
footer %{
static double compute_delay(double throttle, int count) {
return throttle * count / 4.0;
}
%}
To learn more about the sample DMA device and how it is implemented, refer to section 18.
DML allows you to group methods and data together with DML objects. Here is an example:
attribute fifo {
param type = "[i*]";
session uint8 vals[MAX_VALS];
// [...]
method pop() -> (uint8) {
// [...]
}
method push(uint8 val) {
// [...]
}
}
// [...]
fifo.push(17);
The pop()
and push()
methods and the vals
session variable are members of the fifo
attribute. This makes the usage of FIFO simpler and there is no confusion which method pops and which methods push data on the FIFO, as it would if the methods where global.
Here is another very useful template for attributes to use when saving dbuffer
data:
template dbuffer_attribute {
param type = "d|n";
session dbuffer_t *buf;
method set(attr_value_t val) throws {
if (buf)
dbuffer_free(buf);
if (SIM_attr_is_data(val)) {
buf = new_dbuffer();
memcpy(dbuffer_append(buf, SIM_attr_data_size(val)),
SIM_attr_data(val), SIM_attr_data_size(val));
} else {
buf = NULL;
}
}
method get() -> (attr_value_t) {
if (!buf)
return SIM_make_attr_nil();
else
return SIM_make_attr_data(dbuffer_len(buf),
dbuffer_read_all(buf));
}
}
// [...]
attribute frame {
is dbuffer_attribute;
param documentation = "An Ethernet frame.";
}
// [...]
send_frame(frame.buf);
This chapter describes how to write device models that are easy to use and the generic rules on how to write device modules that comply with the standard way of writing Simics modules.
The user interface of a Simics module consists of three parts: its attributes, its interfaces, and the commands it adds to the Simics command line interface. You should try to make the user interface of your model similar in style to that of existing Simics models.
Every model should have an info
command, giving static information about the device, and a status
command, that gives dynamic information. See chapter 13.7 for more information. Model Builder also includes a library for writing tests to check that all devices in your modules have info
and status
commands. See the API Reference Manual for more information.
Look at the interfaces of similar devices to see what other commands may be useful, and try to use the same names for similar commands and parameters. Use existing API functionality where appropriate, rather than inventing your own.
The ability to checkpoint and restore the state of a system is crucial to Simics functionality. Your device model should support checkpointing. In particular, you should ensure that:
As attribute setter functions for more complex attributes can be hard to get right, be sure to read 4.2.7.3 very carefully.
Attributes containing configuration parameters that never change during the lifetime of the device still need to accept setting their values. But since they will only be set with the value they already have, they only have to check that the value is unchanged and signal an error if not.
Ensure that the internal state of the device model is consistent at all times. If, for example, the model caches some information that depends on attribute values, these caches need to be flushed when the attribute is set. This is usually not a problem when restoring checkpoints from disk, but when using micro checkpoints and reverse execution it can easily cause trouble.
The checkpointing and reverse execution test libraries included with Model Builder can be used to test for at least basic support for these features.
Simics is deterministic and to keep the system deterministic all device models must be deterministic.
The basic rule to make a model deterministic is to save all device state data when writing checkpoints. The state is read from the device via the device attributes. Several DML object types implicitly corresponds to device attributes, examples are; attribute, register and connect.
Take extra care when using the data declaration as it does not implicitly correspond to an attribute.
There are several ways to save device data. The best way to save the data depends on how much data to save. A state with little data is best saved by creating an attribute with an integer or floating-point type or a small array:
attribute counter is uint64_attr {
param documentation = "Counting number of packets.";
}
Saving larger blocks of unstructured data is best done by creating an attribute with type set to data:
attribute buffer_attribute is pseudo_attr {
param documentation = "Packet data.";
param type = "d|n";
}
Structured state can be saved in the form of lists, or list of lists etc:
attribute structured_attribute is pseudo_attr {
param documentation = "A string and two integers.";
param type = "[sii]";
// [...]
}
The best way to save a large amount of data is to use Simics images. Images are optimized to only save differences between two consecutive checkpoints and not all data in each checkpoint:
import "simics/model-iface/image.dml";
connect data_image {
param documentation = "Image holding data";
param configuration = "required";
interface image;
}
method save_data(uint64 address, const uint8 *buffer) {
data_image.image.write(
cast(buffer, const void *),
address,
256);
}
As listed in the Simics Model Development Checklist - Device Checklist , DE-11; device objects should handle inquiry accesses correctly. In Simics an 'inquiry access' is defined as an access without any side effects beyond changing the value of the register being accessed. Other domains call this 'debug access'. When using DML this is automatically handled for registers where the read_access and write_access methods have not been overridden. If overridden, or access is handled at bank level or elsewhere, the model must add the corresponding logic to handle inquiry accesses.
The model should handle errors in a forgiving way. Avoid crashing or triggering assertions; instead, log an error message and try to continue anyway.
There are three different kinds of errors that should be reported by a Simics device:
Outside architecture error. A part of the device whose behavior is not specified in the hardware documentation has been accessed. For example, a reserved register has been written to. Use log spec_viol
for this kind of error.
Unimplemented error. A part of the device which has been left unimplemented in the model (abstracted away) was accessed. This suggests a bug in the model, or that the model is used with software it was not developed for. Use log unimpl
for this kind of error.
In some cases it is sufficient to give a warning for this kind of situation, for example if the model returns approximate or invented values.
Internal error. The internal state of the model implementation has been corrupted beyond repair. Look for "this will never happen" comments and reconsider*…* Use log error
for this kind of error.
Simics has extensive support for logging, allowing you to assign the output to different message categories, and different levels of verbosity. See the DML 1.4 Reference Manual and the API Reference Manual for details. Logging is mostly used during development and debugging of the model, but is also useful to aid inspection of the device state during actual simulation.
Always use detailed error messages. Often, the error messages are the only source of information when a bug is reported by another user. It is not certain that the execution can be repeated, or that the developer will have access to the same setup.