Peripheral modeling guide

Renode allows the user to “model” HW peripherals in several ways:

How does access to the system bus work?

read/write operations executed by the CPU (usually in the C implementation in the tlib submodule) are either directed to the internal memory or passed to the system bus and handled by the framework at the C# level.

Access to the memory modeled as MappedMemory is handled entirely at the C level, all other operations are passed from C to C# via TranslationCPU.Read{Byte,Word,DoubleWord,QuadWord}FromBus/ TranslationCPU.Write{Byte,Word,DoubleWord,QuadWord}ToBus functions.

NOTE: It is possible to change MappedMemory type to ArrayMemory in order to handle all memory operations at the C# level. Keep in mind this might cause a significant drop in performance.

┌──────────────┐  C to C   ┌─────────────┐
│ MappedMemory │ ◀──────── │     CPU     │
└──────────────┘           └─────────────┘
                             │
                             │ C to C#
                             ▼
┌──────────────┐           ┌─────────────┐     ┌─────────────┐
│ Peripheral1  │ ◀──────── │  SystemBus  │ ──▶ │ Peripheral2 │
└──────────────┘           └─────────────┘     └─────────────┘
                             │
                             │
                             ▼
                           ┌─────────────┐
                           │ ArrayMemory │
                           └─────────────┘

What if there is no peripheral mapped at given offset?

In the case of write, the operation will be ignored and a warning message will be generated in the log.

In the case of read, the default value of 0 will be returned and a warning message will be generated in the log.

What happens when the peripheral does not implement the given access width?

By default this situation is treated as if there was no peripheral mapped at a given offset.

It is, however, possible to enable automatic translation of access type at the peripheral level using the AllowedTranslation attribute - see an example of usage.

Note that automatic translation might generate more accesses on the bus, e.g., 4 byte reads per one double word read or 1 double word read and one double word write per one byte write. This might have unintended side effects for some registers, e.g., automatically incrementing FIFO data register, issuing the “read-to-clear” behavior or others, depending on the registers’ semantics. It is up the developer to verify if the automatic translation is safe in the context of a given peripheral model.

Writing a peripheral model in C#

A C# class is considered a peripheral model if it implements the IPeripheral interface.

In order for the peripheral to be attachable to the system bus, it must implement at least one (but can implement a few) of: IBytePeripheral, IWordPeripheral, IDoubleWordPeripheral, IQuadWordPeripheral interfaces, enabling 8, 16, 32 and 64-bit accesses respectively.

Double word bus peripherals must implement at least three methods:

  • for reading (e.g., ReadDoubleWord) - called by the system bus in order to read a value from the peripheral,

  • for writing (e.g., WriteDoubleWord) - called by the system bus in order to write a value to the peripheral,

  • for resetting (Reset) - called by the framework to restore the state of the peripheral to the initial state.

Although it’s technically possible to implement read/write method in any way, the preferred one is to use the Register Framework (the source code). For an example of usage, see the LiteX UART.

You can even use a base class (Basic{Byte,Word,DoubleWord}Peripheral) to simplify the code - see an example.

The following section explains how to design a peripheral using the Register Framework.

Register modeling guidelines

Create a private enum, preferably named Registers, that lists all registers supported by the peripheral. Conforming to the enum naming convention (or marking it with the RegistersDescription interface) allows the system bus to generate better log messages by including register name in logs generated by sysbus LogPeripheralAccess.

Use human-readable, PascalCase encoded names (i.e., InterruptEnable instead of IEN) even if they are referred to differently in the documentation. As values of the enum fields, use the offset from the beginning of the peripheral’s memory space (i.e., offsets relative to the beginning of the peripheral, not absolute addresses). Keep in mind that a platform can have multiple peripherals of a given type. Please be reasonable here - there are sometimes peripherals with too many registers or registers forming a repeatable pattern - in such case a creative approach is encouraged.

Do not implement all registers - only those that are actually used by the software and can be therefore tested.

For each register list all fields but implement only the necessary ones. Fields that are not implemented should be marked as tags, reserved or ignored - this will help generating better access logs.

There are different type of fields available in Registers Framework:

  • flags - single-bit fields (example),

  • enum fields - single-or-multiple bit fields where bit patterns encode some non-numeric value (example),

  • value fields - single-or-multiple bit fields encoding a numeric value (example).

For each field you can select an access mode (Read&Write by default) that defines which operations are allowed and how they are handled by the framework. Possible basic values (that can be combined together with a bitwise | OR operator) are:

  • Read,

  • Write,

  • Set - writing 1 sets the bit, writing 0 has no effect,

  • Toggle - writing 1 toggles the current value, writing 0 has no effect [this is most likely usable for flag fields only],

  • WriteOneToClear - writing 1 clears the bit, writing 0 has no effect [this is most likely usable for fields flag only],

  • WriteZeroToClear - writing 0 clears the bit, writing 1 has no effect [this is most likely usable for fields flag only],

  • ReadToClear - the value is set to 0 after read.

Writing a non-zero value to a read-only field will be ignored and generate a warning in the log. Reading from the write-only field will return the default value of 0 (but will not generate a warning in the log, as it’s impossible to infer which fields are read).

By default each register provides an automatic backing field. It means that the software will read the previously written value (assuming that fields are writable and readable). It is possible to access the backing field and modify its value from the code. In order to do that use an out parameter - see an example.

There are helper methods for generating groups of registers - see the DefineMany usage example.

It is also possible to attach callbacks for situations when the field is:

  • written (with any value) (example),

  • changed (written with a value different than the current one) (example),

  • read (the value is taken from the backing field) (example),

  • read - value provider (the value is generated by the callback itself) (example).

There are also callbacks for the whole register - WriteCallback and ReadCallback. They are useful when the value of multiple fields is necessary for the callback logic.

Bus peripheral size

In most cases the size of the peripheral on the bus is well defined and can be included in the model. In order to do that, the class must implement the IKnownSize interface. The size encoded in the Size property is expressed in bytes.

Note: Peripherals not implementing the IKnownSize interface can be also used in Renode, but it is required to provide the size each time the device is registered (in the repl file).

Testing guidelines

For a peripheral to be pushed to Renode upstream repository, it is required that a test is provided, executing at least one binary. The preferred way of testing peripherals is to use standard tests/samples if available, e.g. Zephyr samples, driver tests etc. or provide a custom specific binary. All testing binaries should be buildable from sources.

The test case should be described in Robot Framework. See examples of simple tests.

Example peripherals

Here is a list of various Renode peripheral models that can be used as an inspiration:

Automatic generation of peripheral stubs

To avoid writing the register layout by hand, Renode provides a tool to automate the process - to “scaffold” a generic peripheral structure, thus enabling the developer to focus mostly on implementing the behavioral logic. The peakrdl-renode tool works by parsing a peripheral’s memory map described in the SystemRDL file, and outputting an autogenerated file containing a partial class.

The class has to be then supplemented by writing custom logic, describing the behavior of the peripheral. This allows the developers to simplify the manual and error-prone process of codifying the registers, which can be especially useful during the hardware development phase, where the devices’ register layout is not yet stable. SystemRDL data can also be used as a source of truth for software and RTL development, ensuring a consistent state of various parts of the project.

Renode provides an example of an autogenerated peripheral in the repository:

Only the logic is written by hand, the autogenerated file shouldn’t be modified at all.

This sample demonstrates that for large models containing dozens or hundreds of registers, the tool substantially improves the development process.

To start using the peakrdl-renode tool, execute the following commands:

python3 -m venv .venv
source .venv/bin/activate
pip install peakrdl
pip install git+https://github.com/renode/renode/#subdirectory=tools/PeakRDL-renode

Run peakrdl renode --help to verify that the tool was installed correctly.

You can experiment with a very basic model to see how the tool behaves.

Let’s create a sample SystemRDL file, describing a layout of a peripheral.

addrmap Peripheral {
    regfile {
        default regwidth = 32;

        reg {
            name = "BitFields";
            field {
                name = "FIRST";
                sw = r;
                hw = w;
            } first [0:0];
            field {
                name = "SECOND";
                sw = rw;
                hw = rw;
                onwrite = wzc;
            } second [1:5];
        } bit_fields @ 0x0;

        reg {
            field {
                name = "FIRST";
                sw = rw;
                hw = rw;
            } first [0:0];
        } also_bit_fields @ 0x10;
    } registers @ 0x100;
};

To generate the C# scaffolding, assuming that the source file is named peripheral.rdl, run:

peakrdl renode -n MyPeripheral -N Miscellaneous -o MyPeripheral_generated.cs peripheral.rdl

The result should look similar to this:

// Generated by PeakRDL-renode

using Antmicro.Renode.Core.Structure.Registers;
using Antmicro.Renode.Peripherals.Bus;

namespace Antmicro.Renode.Peripherals.Miscellaneous
{
    public partial class MyPeripheral : IProvidesRegisterCollection<DoubleWordRegisterCollection>, IPeripheral, IDoubleWordPeripheral
    {
        /// <summary> Register "registers.bit_fields" at 0x100 </summary>
        protected Registers_BitFieldsType Registers_BitFields;
        /// <summary> Register "registers.also_bit_fields" at 0x110 </summary>
        protected Registers_AlsoBitFieldsType Registers_AlsoBitFields;

        public DoubleWordRegisterCollection RegistersCollection { get; }

        public MyPeripheral()
        {
            RegistersCollection = new DoubleWordRegisterCollection(this);
            Registers_BitFields = new Registers_BitFieldsType(this);
            Registers_AlsoBitFields = new Registers_AlsoBitFieldsType(this);
            this.Init();
        }

        partial void Init();

        partial void Reset();

        void IPeripheral.Reset()
        {
            this.Reset();
            RegistersCollection.Reset();
        }

        uint IDoubleWordPeripheral.ReadDoubleWord(long offset)
        {
            return RegistersCollection.Read(offset);
        }

        void IDoubleWordPeripheral.WriteDoubleWord(long offset, uint value)
        {
            RegistersCollection.Write(offset, value);
        }

        public struct Registers_BitFieldsType
        {
            /// <summary> Field "first" at 0x0, width: 1 bits </summary>
            public IFlagRegisterField FIRST;
            /// <summary> Field "second" at 0x1, width: 5 bits </summary>
            public IValueRegisterField SECOND;

            public Registers_BitFieldsType(IProvidesRegisterCollection<DoubleWordRegisterCollection> parent)
            {
                parent.RegistersCollection.DefineRegister(0x100, 0x0, true)
                    .WithFlag(0, out FIRST, mode: FieldMode.Read, name: "FIRST")
                    .WithValueField(1, 5, out SECOND, mode: FieldMode.Read | FieldMode.WriteZeroToClear, name: "SECOND");
            }
        }

        public struct Registers_AlsoBitFieldsType
        {
            /// <summary> Field "first" at 0x0, width: 1 bits </summary>
            public IFlagRegisterField FIRST;

            public Registers_AlsoBitFieldsType(IProvidesRegisterCollection<DoubleWordRegisterCollection> parent)
            {
                parent.RegistersCollection.DefineRegister(0x110, 0x0, true)
                    .WithFlag(0, out FIRST, name: "FIRST");
            }
        }
    }
}

Having this file generated, you can create a second file, MyPeripheral.cs, and start by implementing the Init and Reset methods. Then, you can refer to the individual registers’ fields, e.g. by attaching callbacks, as shown in the snippet below, in the AttachCallbacks method.

using Antmicro.Renode.Core.Structure.Registers;
using Antmicro.Renode.Peripherals.Bus;
using Antmicro.Renode.Logging;

public partial class MyPeripheral
{
    partial void Init()
    {
        // Add you logic here

        // This is an example of custom logic, achieved by attaching callbacks to registers
        AttachCallbacks();
        this.Reset();
    }

    partial void Reset()
    {
        // Add your logic here

        secondVal = 0;
    }

    private void AttachCallbacks()
    {
        // Example of callbacks attached to a register, implementing behavioral logic
        this.Registers_BitFields.FIRST.ValueProviderCallback += (val) =>
        {
            this.Log(LogLevel.Debug, "Previous FIRST value: {0}", val);
            // Test low bit of "secondVal"
            return (secondVal & 1) == 1;
        };
        this.Registers_BitFields.SECOND.WriteCallback += (old_val, new_val) =>
        {
            secondVal = new_val + 1;
            this.Log(LogLevel.Debug, "second_val: {0} -> {1}", old_val, secondVal);
        };
    }

    private ulong secondVal;
}

Note

For more information, refer to the peakrdl-renode’s README


Last update: 2026-06-18