Today’s article deals with Segger’s embos, a commercial RTOS that has, until now, no Rust binding (as most other RTOS as well). While this article focuses on embos, we’ll explore several techniques on how to deal with third party code and its peculiarities.
Getting Started
In my previous article the C(++) application was the lead and provided the application frame. This time we’ll do it the other way around and have the rust crate be in the lead.
We’ll use:
- Rust 1.63
- embos for Cortex M4
- OpenOCD
- Visual Studio Code with the following extensions
- Rust Analyzer
- Cortex Debug
- Native Debug
- GCC (arm-none-eabi) (make sure it is on the path!)
Project Setup
To make things simple we’ll use the basic embos example as a starting point, however we’ll have our Rust app provide the startup code as well as the actual application code.
We’ll start by creating a new crate. I based this off of the generic “rust app for cortex-m” template, as this already comes with a usable linker script. Our initial directory should look as follows:
(root)
+---- src
| +--- main.rs
+--- Cargo.toml
+--- memory.x
+--- build.rs
Add the following further crates to your Cargo.toml:
cc
as build dependency- cortex-m
- cortex-m-rt
- cortex-m-semihosting
- panic-halt
Writing the build script
One of the things, that make embos a bit painful to use when Rust is the leading language, is the RTOS’ reliance on macros as part of it’s api. Further the board adapation brings a whole lot of C code, that we don’t want to replicate here. In order to have an easier life we’ll use the cc
crate to compile all that as C code.
We’ll pull the code from the embos SDK’s BoardSupport packages. Add the following to your build.rs
cc::Build::new()
.file("./cc/emmain.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/DeviceSupport/system_nrf52840.c")
// .file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/DeviceSupport/startup_nrf52840.S") // code is provided by cortex-m-rt crate
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/Bsp.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/HardfaultHandler.S")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/JLINKMEM_Process.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/OS_ERROR.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/OS_Syscalls.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/OS_ThreadSafe.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/RTOSInit_nRF5x.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Setup/SEGGER_HardFaultHandler.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_RTT_ASM_ARMv7M.S")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_RTT_printf.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_RTT_Syscalls_GCC.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_RTT.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_SYSVIEW_Config_embOS.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_SYSVIEW_embOS.c")
.file("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/Segger/SEGGER_SYSVIEW.c")
.include(Path::new("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/DeviceSupport"))
.include(Path::new("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/CoreSupport"))
.include(Path::new("../Start/BoardSupport/NordicSemi/nRF52840_nRF52840_DK/SEGGER"))
.include(Path::new("../Start/Inc"))
.define("NRF52840_XXAA", None)
.define("DEBUG", "1")
.compile("nrfStartup");
Note: This assumes that you’ve copied the embos SDK to ../ relative to our crate. Also note that I’ve added “emmain.c” right at the top. We’ll come to that further down.
Application Code
Our application for now looks pretty simple: Just your standard main function for an embedded project:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m::asm;
use cortex_m_rt::{entry, exception};
#[link(name = "nrfStartup")]
extern "C" {
fn do_systick() -> i32;
}
#[entry]
fn main() -> !
{
loop {}
}
#[exception]
fn SysTick() {
unsafe
{
let _ = do_systick();
}
}
I’ve added a custom SysTick handler here. The reason for that is, that the cortex-m crate will populate the vector table for us, which clashes with embos’ BSP. That is why we did not include startup_nrf52840.S
into our C build. The embos kernel however needs the systick to work, so we manually forward the interrupt by calling “do_systick”.
C Glue Code
The glue for this example is based off of Segger’s basic embOS example. As stated before we use a file called “emmain.c” for this. For brevity I removed all unnecessary bits:
#include "RTOS.h"
static OS_STACKPTR int StackHP[128], StackLP[128], StackHW[128]; // Task stacks
static OS_TASK TCBHP, TCBLP, TCBHW;
static OS_EVENT HW_Event;
static void HPTask(void) {
OS_EVENT_GetBlocked(&HW_Event);
while (1) {
OS_TASK_Delay(50);
}
}
extern void rust_func();
int do_systick()
{
static int run_cnt = 0;
SysTick_Handler();
return run_cnt++;
}
static void LPTask(void) {
OS_EVENT_GetBlocked(&HW_Event);
while (1) {
OS_TASK_Delay(200);
}
}
static void HWTask(void) {
OS_EVENT_Set(&HW_Event);
while (1) {
OS_TASK_Delay(40);
rust_func();
}
}
int em_init(void) {
OS_Init(); // Initialize embOS
OS_InitHW(); // Initialize required hardware
OS_TASK_CREATE(&TCBHP, "HP Task", 100, HPTask, StackHP);
OS_TASK_CREATE(&TCBLP, "LP Task", 50, LPTask, StackLP);
OS_TASK_CREATE(&TCBHW, "HWTask", 25, HWTask, StackHW);
OS_EVENT_Create(&HW_Event);
OS_Start(); // Start embOS
return 0;
}
So far so good, we got all our pieces together. All that remains is to glue this together. Add em_init
to our main.rs:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m::asm;
use cortex_m_rt::{entry, exception};
#[link(name = "nrfStartup")]
extern "C" {
fn em_init() -> !;
fn do_systick() -> i32;
}
#[entry]
fn main() -> !
{
unsafe
{
em_init();
}
}
#[no_mangle]
pub extern "C" fn rust_func()
{
static mut COUNT: u32 = 0;
unsafe
{
COUNT += 1;
}
}
#[exception]
fn SysTick() {
unsafe
{
let _= do_systick();
}
}
After compiling and flashing the resulting binary to our DK you should see rust_func
being called periodically.
Next Steps
Obviously this example is brutally simplified and avoids dealing with embos’ API in Rust altogether. This is mostly a trick to get around the OS macro use. The OS_start function, for example, is actually a macro that expands to:
#define OS_Start() \
OS_ASSERT((OS_INFO_GetVersion() == OS_VERSION), OS_ERR_VERSION_MISMATCH); \
OS__Start();
The same actually goes for a bunch of functions of the API (e.g. basically all functions that deal with interrupts). The easiest way to deal with those from Rust is to basically wrap the macro expansion into a C function and call that.
In order to get a more realistic application to run we’d have to:
- Add missing interrupt handlers (e.g. the hard-fault handler)
- Add code to make more parts of the OS API visible.
Image by Louis Reed / upsplash