Understanding renode and custom peripherals
Contents:
# Renode
Renode is a simulation environment for embedding systems. It allows us emulate hardware with peripherals and run a firmware binary on THAT instead. To set this up you can refer to this guide.
In this blog, I will be trying my best to explain renode as I understood it when I was reading through the documentation.
The basics are of course that we need to give it an elf binary. Something that you will flash onto your MCU. There is support for different kinds of MCUs which you can find here. To find a more exhaustive list, check this one.
One cool thing to note is that renode supports all of STM32 types. That’s the golden zone. And no ESPs or RPis (except Pico) make it in there. If you want to emulate ESP32 and family, then your best bet is the espressif fork of QEMU. You can check the code out here, or use this blog post as a reference for setting that up and running with it.
You either make your own class in C# or figure out something better to do -> Replace with a stub if its a sensor data (essentially mock it?). Easier wins come from: Zephyr-supported STM32/nRF/RISC-V board, logic/protocol/state-machine testing, common peripherals.
# RESD file format
You can mock the sensor data using the .resd format. This is the renode sensor data format and renode also has tools to convert from CSV to .resd, so that’s good. Read this to understand how this exactly works: https://github.com/renode/renode/blob/master/tools/csv2resd/csv2resd.py. This will show how the csv needs to be structured.
We can mock both regular and irregular sensors because there are two kinds of valid data formats for .resd files as well.
- Arbitrary timestamp sample blocks -> add a timestamp and a value -> For irregular sensor simulation
- Constant frequency sample blocks -> let’s you add a metadata to specify the intiail timestamp and period between timestamps. This is for a regular sensor data testing.
Timestamps are unsigned 8-byte values expressed in virtual nanoseconds counted from the beginning of the file.
Read this documentation carefully for .resd.
# Writing custom peripheral models
To make writing peripherals easy, we can use this tool called
peakrdl-renode.
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.
We’ll see what this class means in a second but the idea is that, all the register management will then be handled by the tool, we just need to write the core logic.
You can take a look at the sample autogenerated part and the logic part to get a better understanding.
Its awesome that the larger file is the one which is autogenerated (which actually also means that its easier to write that too cause its a set of predefined logic anyways)
Source: Writing Peripherals
Renode allows the user to “model” HW peripherals in several ways:
- automatic tags from the SVD file used mainly for logging purposes,
- manual tags with return value used for logging and trivial flow control,
- Python peripherals used for implementing very simple logic,
- C# models, used to describe advanced peripheral logic and interconnect
The C# part is what we’re looking at right now. How does the access to the system bus work?
- Read and write by the CPU are either gonna go to internal memory (maybe this is important for this?) or passed to the C# implementation to handle.
There are two types of memory (is that the right way to say this?) and the fundamental difference between these two is how the memory is backed and whether the CPU can access them directly or not.
- ArrayMemory:
ArrayMemorystores everything in a single garbage collector managed array. It cannot be handed to the translation engine. It’s mostly intended to be used for memory smaller than page size. It’s not the main RAM.
(If you’re confused about that last statement it means that given your page size, say 4kb or 16kb for newer architectures, you can hold until that much in the ArrayMemory. Page size is a single unit of the page table. Its a contiguous block of virtual memory)
- MappedMemory: To understand this, we’ll have to understand what
tlibis how it works. Let’s park it for now and move on.
┌──────────────┐ C to C ┌─────────────┐
│ MappedMemory │ ◀──────── │ CPU │
└──────────────┘ └─────────────┘
│
│ C to C#
▼
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Peripheral1 │ ◀──────── │ SystemBus │ ──▶ │ Peripheral2 │
└──────────────┘ └─────────────┘ └─────────────┘
│
│
▼
┌─────────────┐
│ ArrayMemory │
└─────────────┘
All MappedMemory access is handled in C whle ArrayMemory access requires conversion to C# (why? No clue).
Now referencing https://renode.readthedocs.io/en/latest/advanced/writing-peripherals.html#writing-a-peripheral-model-in-c
- Must implement an IPeripheral interface.
This is the first thing to notice here:
public
This is the actual contract, and this states the claim that “I am a peripheral” and hence whatever works on a peripheral, works on this.
There are two base inferences. IEmulationElement is the root of everything that’s part of the emulation. This carries identify and the logging parts.
using ..;
And the IAnalyzable is basically to attach an analyzer (like a backend or an observer). So, by just declaring your peripheral with IPeripheral, it already means: nameable, loggable, observable.
Reset must be defined by us! This tells what exactly the return to power-on state must mean, whether you want to clear the registers or what.
But only by this you cannot become readable and writable by the CPU. For that you need to implement atleast these three:
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. (Yeah same thing I mentioned before, this NEEDS to be implemented by us)
Here is a list of various Renode peripheral models that can be used as an inspiration:
- UART
- Timer
- GPIO controller
- I2C controller
- SPI controller
- I2C sensor
- SPI sensor
Let’s look at the repl for x86.
biosMemory: Memory.MappedMemory @ {
sysbus 0x0;
sysbus <0xFF0000, +0x10000>;
sysbus <0xFFFF0000, +0x10000>
}
size: 0xFF0000
sdram: Memory.MappedMemory @ sysbus 0x1000000
size: 0x20000000
uart: UART.NS16550 @ sysbus 0xE00003F8
cpu: CPU.X86 @ sysbus
cpuType: "n270"
lapic: lapic
lapic: IRQControllers.LAPIC @ sysbus 0xFEE00000
IRQ -> cpu@0
hpet: Timers.HPET @ sysbus 0xFED00000
sysbus:
init:
Tag <0xE0000020 1> "PIC1_CMD"
Tag <0xE0000021 1> "PIC1_DATA"
Tag <0xE00000A0 1> "PIC2_CMD"
Tag <0xE00000A1 1> "PIC2_DATA"
Tag <0xE0000040 1> "PIT_CHANNEL0"
Tag <0xE0000041 1> "PIT_CHANNEL1"
Tag <0xE0000042 1> "PIT_CHANNEL2"
Tag <0xE0000043 1> "PIT_CMDREG"
Tag <0xE0000CF8 4> "PCI_ADDRESS"
Tag <0xE0000CFC 4> "PCI_DATA"
Tag <0xE0000070 1> "CMOS_ADDRESS"
Tag <0xE0000071 1> "CMOS_DATA"
This uses a timer called HPET (High Precision Event Timer) which is common for modern x86. Let’s see what the C# module tells us.
//
// Copyright (c) 2010-2026 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in 'licenses/MIT.txt'.
//
using ..;
using ..;
using ....;
using ..;
using ..;
using ...;
using ..;
The first step is to clearly look at the specification cause this heavily references it. This is the actual specification and this is an overview.
# Notes from this specification
- A comparator register and a value register is a hardware component used to trigger an action when a live tracking metric matches a pre-configured target value.
For a timer to have a comparator and value register, it means that it holds the current count in the value register and compares the current value with a match register (via the comparator). This is to implement a trigger.
Motherboard architecture was split into two parts:
- Northbridge -> High speed communication
- Southbridge -> Slower I/O capabilities (Audio, SATA ports, USB, PCI slots)
Nowadays, northbridge is IN the chip itself, and the Southbridge evolved into the PCH (Platform Controller Hub). The PCH acts as the coordinator for almost every other component on the motherboard.
- It communicates CPU via a high-speed bus called
DMI(Direct Media Interface).
Each individual timer can generate an interrupt when the value in its value register matches the value in the main counter. That’s the point! The comparator and value register is to generate an interrupt.
- The main counter uses the PCH’s 24-MHz crystal as its clock. The accuracy of the main counter is as accurate as the crystal that is used in the system.
Stuff you can use the timer for:
- Scheduling -> Threads, tasks, processes
- Synchronizing -> Digital Audio/Video
- Time stamping
HPET, Multimedia Timer, MMT and MM Timer should be treated as the same timer hardware. The terms Timer, Event Timer, HPET, MMT and MM Timer refer to the combination of a Value, Comparator, and Match Register. Intel HPET allows 32 compare/match registers per counter.
- Fmin = 10 MHz -> Note how this is the same value used in the module
- Number of comparators = 3 -> This is also what is written in the code above
- Periodic capable timers = 1 -> This is probably not modeled in the C# module
- Interrupt Delivery via IOxAPIC -> This is a requirement according to the specs, how are we doing this in the C# module?
Okay turns out that this is ancient technology. 2005. No one does this anymore. And the reason I realized this is because HPET implements Interrupts via a specific required delivery mechanism, Interrupt Delivery via IOxAPIC -> But this C# module doesn’t implement any output from this timer at all! Its only a query me module. That’s all there is to this. And its not its fault. No one really needed this.
Let me actually look at the base class before anything else now, cause all timers inherit from it:
//
// Copyright (c) 2010-2026 Antmicro
// Copyright (c) 2011-2015 Realtime Embedded
//
// This file is licensed under the MIT License.
// Full license text is available in 'licenses/MIT.txt'.
//
using ;
using ..;
using ..;
using ...;
using ..;
Let’s break this motherfucker down:
public
Arguments:
clockSource-> This is a source clock for the timerfrequency-> This must be the frequency for the clockowner-> This is a peripheral owner. I think this is related to which peripheral implements aLimitTimerbut I am not sure yet. Let’s see.localname-> This is a name. I think its just a refernence for the peripheral which implements this but again not surelimit-> Okay maybe its the max value it can increment upto until it rolls around?direction-> This is weird. Why is this defaulting to decreasing. (note that the defalt limit is also 2^64-1)enabled-> Enabled what? The timer?workMode-> Periodic timer -> This is cool. This was in the HPET specs as well. 1 out of 3 being periodic.eventEnabled-> What event are we enabling? Interrupts?autoUpdate-> Is this the roll around thing? Or is this saying that no relying on RTC at all ever? Always update on the crystal counter.divider-> Not sure what this is for. Let’s see.
if
if
if
irqSync = ; // I have created some memory here
this.clockSource = clockSource;
initialFrequency = frequency;
initialLimit = limit;
initialDirection = direction;
initialEnabled = enabled;
initialWorkMode = workMode;
initialEventEnabled = eventEnabled;
initialAutoUpdate = autoUpdate;
initialDivider = divider;
this.owner = this is IPeripheral && owner == null ? this : owner;
this.localName = localName;
;
This part is chill. It ensures some values are reasonable. Then creates an object called
irqSync. This sounds suspiciously like interrupt syncing. The reason you donew object()inC#is when you want to create a lock (to not allow multiple threads to access the same memory) or as a placeholder. So, this creates free memory in the heap and allocates it to this object (its like a malloc which mallocs about 24 bytes of memory just to exist).this.clockSource = clockSource;-> This is basically doingself.clockSource = clockSourcein python. However, you don’t really have to do this in C# because this is implicit. In python its explicit. So the reason one would do it is to just make this clear (especailly if names are the same as argument).Then it says: Set
this.ownerto myself (this) if I am a peripheral and no owner was provided. Otherwise, setthis.ownerto the provided ownerThen it calls
InternalReset()I have no idea what this function call does, but we’ll probably figure this out.
public ulong
So with my limited C# knowledge I am assuming that this a method. I know, no need to bask in my glory. This is a method which is returning an unsigned long integer AND the currentLimit.
This is some C# hookey-pookey. When we write out ulong currentLimit we’re actually passing a reference (a pointer) to a blank spot in the computer’s memory. Note how the currentLimit is actually assigned a value before we return something. Insane stuff.
Actually now that I am doing this, I think I should go by how the runtime will see this rather than sequential cause these devs they mangle it all. So, remember InternalReset? Let’s look at that.
private void
Okay so this is a private method, I don’t really care about that but sure. So, there is a new clockEntry and an object initializer. So that Value in there is basically, 0 if initialDirection is ascending (recall how the default is descending), and initialLimit if descending. And initialLimit is limit which cannot be 0 and defaults to 18446744073709551615 -> The largest 64 bit unsigned integer.
So what is the ClockEntry? Did we define it before? I mean in this scope? Because its using new so its assinging some memory in the heap for this. But how is this like a dataclass?
This btw is the GetClockEntry:
public virtual ClockEntry GetClockEntry(Action handler)
- This takes in an
Action handler-> This is where we pass in theOnLimitReachedfunction. And if we look atOnLimitReachedcarefully:
protected virtual void OnLimitReached()
This checks if event is enabled, and if not, then returns it (event is not enabled by default). Then calls alarm() which is basically calling LimitReached. The reason they do is this way is for thread safety. If another thread sets LimitReached to null and you do LimitReached() you get a nullReferenceException. But this delegation is immutable so alarm will always point to LimitReached.
To understand the .Period part, we’ll have to look inside the ClockEntry.cs file:
public That’s what the setting value of currentLimit in GetValueAndLimit. Okay so anyway, sorry for the digression. Let’s get back to the InternalReset. So, now we’re creating a ClockEntry:
var clockEntry = ;
Well it wasn’t a digression at all! Cause now we know what ClockEntry is. Its a struct in some sense. It’s just holding data. So we have a clockEntry and we have the value I explained before.
clockSource.;
EventEnabled = initialEventEnabled;
AutoUpdate = initialAutoUpdate;
rawInterrupt = false;
This is doing some work here:
public virtual void ExchangeClockEntryWith(Action handler, Func<ClockEntry, ClockEntry> visitor,
Func<ClockEntry> factoryIfNonExistent)
The ClockEntry is an immutable struct, so updating the timer value is not possible. We’ll have to replace it, and therefore to update the individual values, we use this function. Its basically, “override if a value is supplied, else keep whatever I am giving you” which is just to say that whatever the new value says, goes.
You know what I think I more or less understand the base case. Now let me look at a specific one, like the riscv_machine_timer.
//
// Copyright (c) 2010-2026 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in 'licenses/MIT.txt'.
//
using ..;
using ..;
using ....;
using ..;
using ...;
The thing is, this must be modelling some behvaiour, so let’s pull out the datasheet for this bad boy. There are some properties here, and this is the driver for it. Here’s what I learnt about this:
- sys_clock_announce() accepts at most INT32_MAX -> This seems familiar.