Integrating Rust with embos

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_initto 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

Leave a Reply

Your email address will not be published. Required fields are marked *