Renode Introduction
I stumbled across Renode (renode.io) recently while looking further into software testing and digital twins. In short, this is really impressive and I’m planning to look into this for our more complex applications at work… but let’s start simple.
What is Renode?
Renode is a development framework which accelerates IoT and embedded systems development by letting you simulate physical hardware systems - including both the CPU, peripherals, sensors, environment and wired or wireless medium between nodes.
It lets you run, debug and test unmodified embedded software on your PC - from bare System-on-Chips, through complete devices to multi-node systems.
It is open-source and MIT license, created and maintained by Antmicro (Sweden / Poland).
Part 1 - Embedded Linux pre-built example
For this particular write-up, I’m using Windows, but Renode is cross-platform.
Download and install the “Renode” MSI / Installer.
Supported boards and projects are found in
C:\Program Files\Renode\scripts
by default.Following the “Running your first demo” instructions (https://renode.readthedocs.io/en/latest/introduction/demo.html), I simply typed the following command into the Renode window:
s @scripts/single-node/zedboard.resc
, wheres
is shorthand forstart
. (Also, from that window, you can typehelp
to get a list of commands.)
Once started, the familiar Linux boot console output is displayed with log in prompt upon boot completion.
That almost seems too easy. I plan to come back to the embedded Linux application approach, but let’s prove that it works with a simple example.
Part 2 - Proving it works - STM32 Hello World
After looking at the supported boards, I thought I’d start with something quick and simple to prove that I could create a custom binary and run it in the emulator. I decided on targeting the STM32F4 DISCOVERY which was available out-of-the-box for both Renode and PlatformIO.
The STM32F4DISCOVERY Discovery kit leverages the capabilities of the STM32F407 high-performance microcontrollers, to allow users to develop audio applications easily. It includes an ST-LINK/V2-A embedded debug tool, one ST-MEMS digital accelerometer, one digital microphone, one audio DAC with integrated class D speaker driver, LEDs, push-buttons, and a USB OTG Micro-AB connector.
https://www.st.com/en/evaluation-tools/stm32f4discovery.html
VSCode + PlatformIO + ChatGPT
Create a new project for the target board (decided to use CMSIS as the framework and stay with bare metal application)
main.c - Version 1
Next, I used ChatGPT for a simple Hello World application. This is a simple hello world application.
One advantage of PlatformIO is the quick ramp-up. I basically pasted in my main.c
and hit the build button, generating firmware.elf
.
#include "stm32f4xx.h"
#include <stdint.h>
#include <string.h>
/*
* Simple UART2 initialization on PA2 (TX) / PA3 (RX) at 115200 8N1
*/
static void uart2_init(void);
static void uart2_write_string(const char *str);
int main(void)
{
/*
* SystemInit() is called before main() by the startup code (startup_stm32f407xx.s).
* It typically sets up the vector table location and initial clock config.
* We can also call SystemCoreClockUpdate() to make sure CMSIS knows the current clock.
*/
SystemCoreClockUpdate();
/* Initialize USART2 pins and registers */
uart2_init();
/* Transmit the classic “Hello World” */
uart2_write_string("Hello World!\r\n");
/* Infinite loop */
while (1)
{
/* In a real application, you might do something here... */
}
}
/**
* @brief Initialize USART2 for 115200 baud, 8N1 on PA2 (TX) and PA3 (RX).
*/
static void uart2_init(void)
{
/* 1. Enable clock for GPIOA and USART2 */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock
RCC->APB1ENR |= RCC_APB1ENR_USART2EN; // Enable USART2 clock
/*
* 2. Configure PA2 and PA3 to Alternate Function 7 (USART2) at 115200
* PA2: USART2_TX
* PA3: USART2_RX
*/
// Set PA2/PA3 to AF mode
GPIOA->MODER &= ~((3U << (2 * 2)) | (3U << (3 * 2))); // Clear mode bits for PA2, PA3
GPIOA->MODER |= ((2U << (2 * 2)) | (2U << (3 * 2))); // Set mode to Alternate Function
// Set AF7 for PA2, PA3 (USART2)
GPIOA->AFR[0] &= ~((0xF << (2 * 4)) | (0xF << (3 * 4))); // Clear AF bits
GPIOA->AFR[0] |= ((7U << (2 * 4)) | (7U << (3 * 4))); // AF7 = 7
// Optional: Configure speed, pull-up, etc. as needed
GPIOA->OSPEEDR |= ((3U << (2 * 2)) | (3U << (3 * 2))); // High speed
GPIOA->PUPDR &= ~((3U << (2 * 2)) | (3U << (3 * 2))); // No pull-up/pull-down
/*
* 3. Configure USART2 registers
* Default: 8 data bits, no parity, 1 stop bit
*/
// Reset control register
USART2->CR1 = 0;
USART2->CR2 = 0;
USART2->CR3 = 0;
// Baud rate configuration: 115200
// BRR = fCK / baud. Assuming fCK is APB1 at 16MHz for a default config:
// at 16MHz -> 16,000,000 / 115,200 ~ 138.9 -> 0x8A for Mantissa
// This is approximate if we are running from the HSI (16 MHz).
USART2->BRR = 0x8A; // (You may fine-tune if your clock is different)
// Enable USART2 transmitter and receiver
USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE);
// Finally, enable the UART
USART2->CR1 |= USART_CR1_UE;
}
/**
* @brief Send a null-terminated string via USART2 (blocking).
*/
static void uart2_write_string(const char *str)
{
while (*str)
{
// Wait until TXE (Transmit data register empty) is set
while (!(USART2->SR & USART_SR_TXE))
;
// Write the data to DR
USART2->DR = (*str++ & 0xFF);
}
}
main.c - Version 2
I quickly moved on to a slightly more complex application (even though I tested the simple one first).
This version will write to the UART on a loop. Built again for updated firmware.elf
.
#include "stm32f4xx.h"
#include <stdint.h>
#include <string.h>
/* ====== PROTOTYPES ====== */
static void uart2_init(void);
static void uart2_write_string(const char *str);
static void systick_init(uint32_t ticks_per_second);
static void delay_ms(uint32_t ms);
/* ====== GLOBALS ====== */
static volatile uint32_t msTicks = 0; // Millisecond counter
int main(void)
{
/*
* SystemInit() is called before main() by the startup code (startup_stm32f407xx.s).
* It typically sets up the vector table location and initial clock config.
* We can also call SystemCoreClockUpdate() to make sure CMSIS knows the current clock.
*/
SystemCoreClockUpdate();
/* 1) Initialize SysTick to increment msTicks every 1ms */
systick_init(1000); // 1000 = 1000 ticks per second = 1ms tick
/* Initialize USART2 pins and registers */
uart2_init();
/* Transmit the classic “Hello World” */
uart2_write_string("Hello World!\r\n");
/* 3) Repeatedly print Hello World every 1 second */
int count = 1;
char message[64];
while (1)
{
snprintf(message, sizeof(message), "Hello World %d!\r\n", count);
count++;
uart2_write_string(message);
delay_ms(1000); // wait 1 second
}
}
/**
* @brief Initialize SysTick to generate interrupts at `ticks_per_second` frequency
*
* For example, if ticks_per_second = 1000, that's a 1ms SysTick interrupt.
*/
static void systick_init(uint32_t ticks_per_second)
{
/*
* SysTick is clocked by SystemCoreClock (default 16 MHz if using HSI).
* RELOAD value = (SystemCoreClock / ticks_per_second) - 1
*/
uint32_t reload_val = (SystemCoreClock / ticks_per_second) - 1U;
/* Program the SysTick reload and current value registers */
SysTick->LOAD = reload_val;
SysTick->VAL = 0U;
/*
* Enable SysTick:
* - CLKSOURCE = 1 (use processor clock)
* - TICKINT = 1 (enable SysTick interrupt)
* - ENABLE = 1 (counter is enabled)
*/
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk
| SysTick_CTRL_TICKINT_Msk
| SysTick_CTRL_ENABLE_Msk;
}
/**
* @brief Simple blocking delay in milliseconds
*/
static void delay_ms(uint32_t ms)
{
uint32_t start = msTicks;
while ((msTicks - start) < ms)
{
/* just wait */
}
}
/*
* @brief SysTick interrupt handler
* Called automatically every 1 ms if configured by systick_init(1000).
*/
void SysTick_Handler(void)
{
msTicks++; // increment global millisecond counter
}
/**
* @brief Initialize USART2 for 115200 baud, 8N1 on PA2 (TX) and PA3 (RX).
*/
static void uart2_init(void)
{
/* 1. Enable clock for GPIOA and USART2 */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock
RCC->APB1ENR |= RCC_APB1ENR_USART2EN; // Enable USART2 clock
/*
* 2. Configure PA2 and PA3 to Alternate Function 7 (USART2) at 115200
* PA2: USART2_TX
* PA3: USART2_RX
*/
// Set PA2/PA3 to AF mode
GPIOA->MODER &= ~((3U << (2 * 2)) | (3U << (3 * 2))); // Clear mode bits for PA2, PA3
GPIOA->MODER |= ((2U << (2 * 2)) | (2U << (3 * 2))); // Set mode to Alternate Function
// Set AF7 for PA2, PA3 (USART2)
GPIOA->AFR[0] &= ~((0xF << (2 * 4)) | (0xF << (3 * 4))); // Clear AF bits
GPIOA->AFR[0] |= ((7U << (2 * 4)) | (7U << (3 * 4))); // AF7 = 7
// Optional: Configure speed, pull-up, etc. as needed
GPIOA->OSPEEDR |= ((3U << (2 * 2)) | (3U << (3 * 2))); // High speed
GPIOA->PUPDR &= ~((3U << (2 * 2)) | (3U << (3 * 2))); // No pull-up/pull-down
/*
* 3. Configure USART2 registers
* Default: 8 data bits, no parity, 1 stop bit
*/
// Reset control register
USART2->CR1 = 0;
USART2->CR2 = 0;
USART2->CR3 = 0;
// Baud rate configuration: 115200
// BRR = fCK / baud. Assuming fCK is APB1 at 16MHz for a default config:
// at 16MHz -> 16,000,000 / 115,200 ~ 138.9 -> 0x8A for Mantissa
// This is approximate if we are running from the HSI (16 MHz).
USART2->BRR = 0x8A; // (You may fine-tune if your clock is different)
// Enable USART2 transmitter and receiver
USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE);
// Finally, enable the UART
USART2->CR1 |= USART_CR1_UE;
}
/**
* @brief Send a null-terminated string via USART2 (blocking).
*/
static void uart2_write_string(const char *str)
{
while (*str)
{
// Wait until TXE (Transmit data register empty) is set
while (!(USART2->SR & USART_SR_TXE))
;
// Write the data to DR
USART2->DR = (*str++ & 0xFF);
}
}
Setting up Renode
Antmicro includes a handy website for accessing resources for supported devices. I downloaded all of the assets for this board.
https://designer.antmicro.com/hardware/devices/stm32f4_disco
After extracting the assets, I copied my custom firmware.elf to the folder and slightly modified the hello_world.resc
(Renode Script) file.
Original
logFile $ORIGIN/hello_world-renode.log True
using sysbus
$name?="stm32f4_disco"
mach create $name
machine LoadPlatformDescription $ORIGIN/hello_world.repl
showAnalyzer usart2
usart2 RecordToAsciinema $ORIGIN/hello_world-asciinema
macro reset
"""
sysbus LoadELF @https://zephyr-dashboard.renode.io/zephyr/1f4874088f17c2d617426aa8cb78b66bbb634989/stm32f4_disco/hello_world/hello_world.elf
cpu0 VectorTableOffset `sysbus GetSymbolAddress "_vector_table"`
cpu0 EnableZephyrMode
cpu0 EnableProfilerCollapsedStack $ORIGIN/hello_world-profile true
"""
runMacro $reset
Modified
By default, they use Zephyr OS it seems for their example, so the vector_table
doesn’t apply and download their example applications for convenience. The $ORIGIN
tag allows relative paths to the Renode script.
logFile $ORIGIN/hello_world-renode.log True
using sysbus
$name?="stm32f4_disco"
mach create $name
machine LoadPlatformDescription $ORIGIN/hello_world.repl
showAnalyzer usart2
usart2 RecordToAsciinema $ORIGIN/hello_world-asciinema
macro reset
"""
sysbus LoadELF $ORIGIN/firmware.elf
cpu0 EnableZephyrMode
cpu0 EnableProfilerCollapsedStack $ORIGIN/hello_world-profile true
"""
runMacro $reset
Running in Renode
Similar to the prior example, I simply ran start <renode script>
, this time pointing to my custom location.
Notes:
I’m not sure what that warning was about, but it did seem to write successfully.
It seemed to write faster than once per second, so there may be a mistake with the ChatGPT generated code, but in general still proved what I intended to prove.