Rust Blinky application

This tutorial shows you how to convert the Blinky application to Rust. This includes integrating Cargo into newt builder.

Prerequisites

Ensure that you meet the following prerequisites before continuing with this tutorial:

  • You have basic knowledge about Rust.

  • You have basic knowledge about Rust Embedded.

  • You have rust installed using rustup.

  • Follow Blinky tutorial to create a project with a basic application. You will extend that application in this tutorial.

Initialize Rust

The first step is to initialize a crate for developing your application in. You can do this in the existing blinky directory.

$ cargo init --lib apps/blinky
Created library package
$ tree apps/blinky
apps/blinky
├── Cargo.toml
├── pkg.yml
└── src
    ├── lib.rs
    └── main.c

1 directory, 4 files

This creates a Cargo.toml configuration file and src/lib.rs. We will use these files to place our application in. You may notice that the Rust application is not configured as an app, but as an lib. This is needed later to allow linking to the rest of mynewt.

Setup the basics

Now we want to actually convert the application code to Rust. Let’s open src/lib.rs and remove the contents. Then start with some basic setup:

#![no_std]

extern crate panic_halt;

#[no_mangle]
pub extern "C" fn main() {

    loop {
    }
}

The first line states that this program doesn’t use the standard library. This means that only the core library is linked to the program. See rust-embedded book for more information.

The next line specifies the panic handler. Panicking is an important feature of Rust. To ease this tutorial we choose to halt the processor on panic. See rust-embedded book for alternatives.

Then we have the main function. It contains a endless loop as it should never return, but doesn’t do anything useful yet. It is marked no_mangle and extern "C" to make sure it can be called from the mynewt C code.

Converting sysinit

The next step is to do the sysinit. This is implemented as a C macro, which is incompatible with Rust. This could be solved by using a C library, but for this tutorial we will simply execute the macro manually.

#![no_std]

extern "C" {
    fn sysinit_start();
    fn sysinit_app();
    fn sysinit_end();
}

extern crate panic_halt;

#[no_mangle]
pub extern "C" fn main() {
    /* Initialize all packages. */
    unsafe { sysinit_start(); }
    unsafe { sysinit_app(); }
    unsafe { sysinit_end(); }

    loop {
    }
}

First we manually define the three sysinit functions. This is similar to the C header file. Then we execute the sysinit as the macro would do.

We need the unsafe indication because the C code doesn’t have the same memory guarantees as Rust. Normally we need to build a safe Rust wrapper, but that is out of scope for this tutorial.

Doing GPIO and delays

Now it is time to do some GPIO and add a delay. Again we define the functions and then use them in and around the loop. We need some constants that are normally defined by the BSP or MCU. These constants need to move to a better place later.

#![no_std]

extern "C" {
    fn sysinit_start();
    fn sysinit_app();
    fn sysinit_end();
    fn hal_gpio_init_out(pin: i32, val: i32) -> i32;
    fn hal_gpio_toggle(pin: i32);
    fn os_time_delay(osticks: u32);
}

extern crate panic_halt;

const OS_TICKS_PER_SEC: u32 = 128;

const LED_BLINK_PIN: i32 = 23;

#[no_mangle]
pub extern "C" fn main() {
    /* Initialize all packages. */
    unsafe { sysinit_start(); }
    unsafe { sysinit_app(); }
    unsafe { sysinit_end(); }

    unsafe { hal_gpio_init_out(LED_BLINK_PIN, 1); }

    loop {
        /* Wait one second */
        unsafe { os_time_delay(OS_TICKS_PER_SEC); }

        /* Toggle the LED */
        unsafe { hal_gpio_toggle(LED_BLINK_PIN); }
    }
}

Cargo build

Now that the application is converted we need to build it and link it the rest of mynewt. We start with Cargo.toml:

[package]
name = "rust-klok"
version = "0.1.0"
authors = ["Casper Meijn <casper@meijn.net>"]
edition = "2018"

[dependencies]
panic-halt = "0.2.0"

[lib]
crate-type = ["staticlib"]

This adds the panic-halt dependency, which is needed for the panic handler as mentioned earlier. It also configures the crate as staticlib, which causes the application to be build as .a-library. This will be needed in a later step.

Next we need a script for running cargo and moving the library to the correct place. Create a new file named apps/blinky/cargo_build.sh with the following contents:

#!/bin/bash

set -eu

if [[ ${MYNEWT_VAL_ARCH_NAME} == '"cortex_m0"' ]]; then
  TARGET="thumbv6m-none-eabi"
elif [[ ${MYNEWT_VAL_ARCH_NAME} == '"cortex_m3"' ]]; then
  TARGET="thumbv7m-none-eabi"
elif [[ ${MYNEWT_VAL_ARCH_NAME} == '"cortex_m4"' || ${MYNEWT_VAL_ARCH_NAME} == '"cortex_m7"' ]]; then
  if [[ $MYNEWT_VAL_HARDFLOAT -eq 1 ]]; then
    TARGET="thumbv7em-none-eabihf"
  else
    TARGET="thumbv7em-none-eabi"
  fi
else
  echo "The ARCH_NAME ${MYNEWT_VAL_ARCH_NAME} is not supported"
  exit 1
fi

cargo build --target="${TARGET}" --target-dir="${MYNEWT_PKG_BIN_DIR}"
cp "${MYNEWT_PKG_BIN_DIR}"/${TARGET}/debug/*.a "${MYNEWT_PKG_BIN_ARCHIVE}"

The script first sets the name of the target as the Rust compiler knows it. Sadly this is not the same as the names mynewt uses. You need to choose the same type of compiler as mynewt uses. The following targets are available depending on the MCU type:

  • thumbv6m-none-eabi - use this for Cortex-M0 and Cortex-M0+.

  • thumbv7m-none-eabi - use this for Cortex-M3.

  • thumbv7em-none-eabi - use this for Cortex-M4 and Cortex-M7.

  • thumbv7em-none-eabihf - use this for Cortex-M4 and Cortex-M7 with the HARDFLOAT syscfg enabled.

Then it runs cargo build with the target directory set to a path that newt provides. Lastly it copies the generated library to the correct path.

Don’t forget to mark the script as executable:

$ chmod +x apps/blinky/cargo_build.sh

Newt integration

To automatically run cargo we need to add the following to pkg.yml:

...

pkg.pre_build_cmds:
    './cargo_build.sh': 1

pkg.lflags:
    - '-Wl,--allow-multiple-definition'

The first section tells the mynewt build system to run cargo_build.sh as part of newt build. The second section tells the linker to ignore double function definitions. This is needed as the Rust compiler adds some functions that are also in baselibc.

Now we are ready to build a firmware! Remove main.c and start the build:

$ rm apps/blinky/src/main.c
$ newt build nrf52_blinky

If this command complains about a target may not be installed, then you need to install it. You need the same toolchain as configured earlier for the TARGET variable:

$ rustup target add <your-target>

Conclusion

You now have a firmware where the application is written in Rust. It is nicely integrated into newt builder. However it still needs some work: it misses safe Rust wrappers for the mynewt libraries and there are some magic constants that need to be moved to a better location.