Platform description format

To address the need of easily assembling peripheral models into complete platform definitions, a YAML-like platform description format was created for Renode based on common use cases as experienced in daily work with the framework.

Typically, files in this format have the .repl (REnode PLatform) extension.

The format is meant to be human-readable, concise, easy to parse, base upon, extend and modify.

Indentation

Within Renode’s platform description format, meaningful indentation (similar to e.g. Python) is used alongside curly braces ({, }). The rules are as follows:

  1. Only spaces are used for indentation and indent has to be a multiple of four spaces.

  2. Syntactically one level of indentation corresponds to one brace (opening one if we indent, and closing if dedent).

  3. The indentation inside braces is not meaningful, this also applies to new line characters. They are all treated as white characters. When meaningful indentation is used, we refer to it as indent mode (as opposed to non-indent mode). To separate elements in non-indent mode (corresponding to lines in indent mode), a semicolon must be used.

For example these files are equivalent:

line1
line2
    line3
    line4
        line5
    line6
line1
line2 { line3; line4 { line5 }; line 6 }

Comments

There are two types of comments:

  • line comments start with // and continue to the end of the line;

  • multiline comments are delimited by /* and */ and can span multiple lines.

Both comments can be used in indent and non-indent mode, but there is one special rule. When a multiline comment spans multiple lines, it has to end at the end of the line. Otherwise it would be difficult to establish what indenation should be used for the rest of the line.

In other words this source is legal:

line1 /* here a comment starts
 here it continues
and here ends*/
line2

But this one is not:

line1 /* here a comment starts
 here it continues
and here ends*/ line2

Basic structure

Each platform description format consists of entries. An entry is a fundamental unit of peripheral description. The basic format of an entry is as follows:

variableName: TypeName registrationInfo
    attribute1
    attribute2
    ...
    attributeN

All of TypeName, registrationInfo and attributes are optional, but at least one of them must be present. If an entry contains a TypeName, then it is a creating entry (otherwise it is an updating entry).

Each creating entry declares a variable, there can be only one declaration for a given variable. The declaration can be done anywhere in the file hierarchy. All peripherals that are registered in the machine are also imported as variables and can have their updating entries (but not creating entries).

Both of the below are legal:

variable1: SomeType
    property: value

variable1:
    property: otherValue
variable1:
    property: value

variable1: SomeType
    property: otherValue

Entries (the ones that do not specify a type) can update and override information provided by the former ones. Eventually all entries corresponding to the given variable are merged so that the merge result contains attributes from all entries. Some of these can possibly be invalidated (overridden) by another (newer) entry.

Note

In practice, that means that the “newest” value for an attribute will be selected - that is the one which is provided last in the file. This also takes into account all files chained by using keywords. The entry specifying the type is not handled in any special way, nor does it have any priority when selecting properties’ values.

TypeName must generally be provided with the full namespace the type is located in. However, if the namespace starts with Antmicro.Renode.Peripherals, then this part can be omitted.

A creating entry (not an updating one) can be optionally prepended with local - the variable declared in this entry is then called a local variable.

For example:

local cpu: SomeCPU
    StringProp: "a"

cpu:
    IntProp: 32

If the variable is local, then we can reference it only within that file. This will be clearer after reading the next section, but generally if one file depends on another, both can declare same named local variable and they are completely independent, in particular they can have different types.

Values

A value is a notion widely used in the platform description format. There are four kinds of values:

  • simple values that can be further divided into:

    • strings (delimited by a double quote with \" used as an escaped double quote);

    • multiline strings (delimited by triple quotes ''' with \''' used as escaped triple quotes) (example below);

    • boolean values (either true or false);

    • numbers (decimal or hexadecimal with the 0x prefix);

    • ranges (described below)

  • reference values, which point to a variable and are given just as the name of the variable;

  • inline objects that denote an object described in the value itself and not tied to any variable (described later);

  • lists, which can contain all types of values (described later).

A range represents an interval and can be supplied in two forms:

  • <begin, end> or

  • <begin, +size> where begin, end and size are decimal or hexadecimal numbers.

Examples: <0, 100>, <0x10000, +0x200>.

A list is a collection of values, enclosed in square brackets and separated by commas. For example, a list of integers can be defined as:

[1, 2, 3]

Lists can contain all types of values, including other lists:

[["a", "b", "c"], ["d", "e", "f"]]

They can also contain reference values:

[cpu0, cpu1]

as well as inline objects:

[new Point { x: 640; y: 480 }, new Point { x: 5120; y: 2160 }]

Example of a multline string with an escaped delimiter:

name: '''this is \'''
some 
multiline
name'''

Registration info

Registration info tells in which register a given peripheral should be registered and how. A peripheral can be registered in one or more registers. For a single registration the format of registration info is as follows:

@ register registrationPoint as "alias"

where registrationPoint is a value and is optional. The as "alias" part is called an alias and is also optional. Using registrationPoint, the registration point is created or directly used (if the value specified is a registration point): If the registration point is not given, then either a NullRegistrationPoint is used or (if NullRegistrationPoint is not accepted) a registration point with no constructor parameters or all parameters optional.

If the registration point is a simple value, then a registration point is used with a constructor taking one parameter to which this simple value can be converted and possibly other optional parameters. Note that any ambiguity in the two cases mentioned above will lead to an error.

If the registration point is a reference value or an inline object then they are directly used as a registration point.

During registration, the registered peripheral is normally given the same name as the name of the variable. The user can, however, override this name with a different one using the mentioned alias.

Multiple registrations are also supported; this has the following form:

@ {
    register1 registrationPoint1;
    register2 registrationPoint2;
    ...
    registerN registrationPointN;
} as "alias"

The meaning and optionality of the elements is the same as it was in the previous case with the only difference that the peripheral is registered multiple times, possibly in different registers. Note that - as was mentioned at the beginning of this document - the indentation within braces does not matter.

Registration info can be given in any entry (creating or updating), also in more than one entry. In such case only the registration from the newest entry takes place. Registration can also be cancelled, i.e. overridden without providing new registration info. This is done using @ none notation, for example:

variable: @none

Core-specific registrations

Normally, when a peripheral is registered on the bus, it is visible to all cores. However, it is possible to restrict the registration to a specific core. This is done by using inline registration objects instead of a range value.

To specify a core-specific registration, you can use BusPointRegistration or BusRangeRegistration. Example of a core-specific registration:

nvic: IRQControllers.NVIC @ sysbus new Bus.BusPointRegistration {
        address: 0xE000E000;
        cpu: cpu_m33
    }

In this example, the nvic is only visible at address 0xE000E000 to the cpu_m33.

If you need to specify the size of the registration explicitly, you can use BusRangeRegistration:

sysbus new Bus.BusRangeRegistration { address: 0x81000000; size: 0x1000; cpu: cpu0 }

This is equivalent to the <address, +size> range syntax, but allows specifying additional parameters like cpu.

Attributes

There are five kinds of attributes:

  • constructor or property attributes;

  • interrupt attributes;

  • preinit attributes;

  • init attributes;

  • reset attributes.

Constructor or property attributes

A constructor or property attribute has the following form:

name: value

name is the name of the property (if the initial letter is uppercase) or constructor parameter (otherwise) and value is a value. When used with a property, if the attribute’s value is convertible to this property type, then such converted value will be set (otherwise an error is produced).

Note, however, that another entry may update the property so that only the final (i.e. the last containing an attribute setting this property) entry is effective.

The none keyword can also be used instead of a value. Having it there means that the property is not set using any value and its value before applying the description is kept. It can be useful when some entry sets some value and we want to update this entry but not set any value.

The empty keyword can be used to set the default value of property or constructor parameter:

  • numerical values are set to 0;

  • string values are set to null;

  • enum values are set to value corresponding to index 0 in this enum;

  • reference types are set to null;

Constructor attributes are merged in a similar way, i.e. attributes from all entries belonging to the given variable are analyzed and for each name we take the last one value with this name. The constructor of the peripheral is chosen based on the set of merged attributes. For each possible constructor of the type specified in the creating entry we check whether:

  • each parameter of the constructor has a default value or corresponding attribute, i.e. attribute having same name as the name of the parameter;

  • the corresponding attribute has value convertible (for simple types) or assignable (otherwise) to the parameter type;

  • all attributes have been used.

If all the conditions are satisfied then the analyzed constructor is marked as usable. If only one constructor is usable, then the object is created using this constructor. If there is no such constructor or there are more than one, an error is produced.

Because it is much easier to debug constructor selection problems if all the data are in one place (i.e. name of the type and constructor attributes), a warning is issued whenever a non creating entry contains constructor parameters (effectively updating a creating one).

Note that it is only possible to provide constructor attributes for an entry whose variable is going to be created, so it is not possible to provide any on variables represeting peripherals existing before a given description is processed.

Interrupt attributes

As the name suggests, interrupt attributes are used to specify which interrupts of the variable in which the attribute is defined are connected and where. The simplest format of such attribute is as follows:

-> destination@number

where destination is a variable implementing the IGPIOReceiver interface and number is the destination interrupt number. Note that there is nothing specified on the left side - this is only possible if there is a single property of type GPIO, or there are multiple properties, but one of them is marked with a DefaultInterrupt attribute. This is the one that gets connected.

Whenever the user wants to specify which property should be connected, a more general form can be used:

propertyName -> destination@number

where propertyName is the name of the property (of the GPIO type) that should be connected. Also, if the type implements INumberedGPIOOutput, a number can be used instead of the property name.

If more than one interrupt is to be connected to the same destination peripheral, the following form of the attribute can be used:

[irq1, irq2, ..., irqN] -> destination@[irqDest1, irqDest2, ..., irqDestN]

Where irq1 connects to irqDest1 etc. Again, irq s can be names or numbers (if INumberedGPIOOutput is implemented) and irqDest s have to be numbers. Naturally, the arity of sources and destinations has to match.

There is also a possibility of connecting a single source to multiple destinations with the | sign, which can be used with every interrupt attribute format:

-> destination@number | another_destination@number
propertyName -> destination@number | another_destination@number
[irq1, irq2, ..., irqN] -> destination@[irqDest1, irqDest2, ..., irqDestN] | another_destination@[irqDest1, irqDest2, ..., irqDestN]

Note that every attribute separated by the | sign has to be of the same arity as the source.

There is also a notation used in case of local interrupts:

source -> destination#index@interrupt

destination has to implement ILocalGPIOReceiver and index is the index of the local GPIO receiver. This notation can also be used with multiple interupts:

[irq1, irq2, ..., irqN] -> destination#index@[irqDest1, irqDest2, ..., irqDestN]

Just as in the case of properties, interrupt attributes can update older ones. This is done basing on the source interrupt, i.e. if two attributes from different entries use the same source interrupt, only the one from the latter is used. Again, as in properties, the user may want to cancel the irq connection without specifying a different one. The keyword none can be used for this purpose:

source -> none

Init attributes

Init attributes are used to execute monitor commands on the variable. They have one of the following forms:

init:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN
init add:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN

The difference between them is that during merge phase the first one overrides the given variable’s previous init attribute (if there is one) and the second one concanates itself to that previous one. The final entry is eventually executed: every statement is prepended with the name of the peripheral the variable is tied to and then directly parsed by the Monitor. Note that this means that the init section is only legal for variables that are registered.

Preinit attributes

Preinit attributes are used to execute monitor commands before the variable is created. This is useful for setting up global state or preparing the environment for the creation of the variable (e.g. compiling a peripheral model from C# source). These commands are executed in the global context, so they are not automatically prepended with the variable name.

They have one of the following forms:

preinit:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN
preinit add:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN

Just like Init attributes, preinit overrides previous preinit attributes for the same variable, while preinit add appends to them.

The order of execution for preinit blocks is as follows: sysbus preinit blocks are executed first, then other variables’ preinit blocks are executed in the order the variables appear in the file.

Each preinit block defined in a repl file is executed with the file’s directory prepended to the Monitor path, so relative paths are resolved starting from the file’s containing directory. For example, given a directory structure like

.
├── inner
│   ├── MyPeripheral.cs
│   └── myplatform.repl
└── MyPeripheral.cs
└── outer.repl

then, if outer.repl includes a using of ./inner/myplatform.repl, which contains

peripheral: MyPeripheral @ sysbus 0x0
    preinit:
        include @MyPeripheral.cs

then the include invocation inside myplatform.repl will find inner/MyPeripheral.cs.

Reset attributes

Reset attributes are used to execute monitor commands on the variable when the corresponding peripheral is being reset. They have one of the following forms:

reset:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN
reset add:
    monitorStatement1
    monitorStatement2
    ...
    monitorStatementN

Similarly to Init attributes, the reset overwrites the previous reset attribute (if exists), while the reset add concatenates itself with the previous one.

Internally, this attribute creates an <entry>.reset macro for a given entry, e.g. for the peripheral entry, peripheral.reset will be defined with contents of the reset attribute. This macro is run when the peripheral is reset by machine, e.g. as a result of machine Reset in the Monitor.

Inline objects

Inline objects are values similar to reference values, but instead of creating a separate variable and then referencing it, it is defined directly in the place of reference. The form is as follows:

new Type
    attribute1
    attribute2
    ...
    attributeN

The effect is the same as creating an entry of this type and with those attributes, but it cannot be updated and is only available in the place of reference. So, for example, these codes lead to the same effect:

variable: SomeType
    SomeProperty: point

point: Point
    x: 5
    y: 3
variable: SomeType
    SomeProperty: new Point {x: 5; y: 3}

Depending on other files

One description can depend on another, in which case it can use all (non-local) variables from that file.

The using keyword declares a dependency:

using "path"

The line above is called a using entry. Using entries have to come before any other entries.

Using entries can form arbitrary hierarchies. The only limitation is that there can be no cycles (these are reported as errors).

Entry override order

As mentioned before, entries can be overriden. Variables are gathered in a certain order, and later ones override the previous. First, the using entries are processed, top to bottom. Then, entries in the current file are gathered from top to bottom. This process is applied recursively when there are more levels of usings.

For example, consider the following platform definition:

// myplatform.repl
using "a.repl"
using "b.repl"

variable: SomeType
// a.repl
variable:
    SomeProperty: "set by a, first"

variable:
    SomeProperty: "set by a, second"
// b.repl
variable:
    SomeProperty: "set by b, first"

variable:
    SomeProperty: "set by b, second"

The final value of variable.SomeProperty is “set by b second”. The reason is, that it’s the last override of variable.SomeProperty available. We can override this further in myplatform.repl.


Last update: 2026-05-21