Bare Metal Rust on the Teensy 3.1

24 Jan 2016

Now that we have a good understanding of basic bare metal programming using the traditional languages I wanted to look at rust. In this post I will port the bare metal c example to rust with cargo, rusts dependency manager and build manager.

The final source can be found in this github repository. It contains a fair few more files and cargo enforces a stricter layout (ie the source must be in the src/ directory. Most of the addition files are for rust and are meant to make life simpler in the long run. The final project structure is as follows:

├── build.rs
├── .cargo
│   └── config
├── Cargo.toml
├── layout.ld
├── src
│   └── main.rs
└── thumbv7em-none-eabi.json

The linker script layout.ld is identical to the c version so we will skip over it, I recommend reading my previous post for more details about it.

The Rust Code

This is a simple port of blink.c from the c example. We have moved it to src as this is where cargo looks for our code and have renamed it to main.rs to tell cargo we want to build a binary.

Rust makes use of feature guards to stop accidental use of unsable/experimental features. Unfortunately bare metal programming still requires a few of these features so we must state which ones we want to use.

  • lang_items: so we can define some functions that are needed by rust to work (these are normally defined in the standard libraries)
  • no_std: to disable the standard libraries as they require an operating system to work
  • core_intrinsics: to make use of the core::intrinsics::volatile_store which is normally wrapped by the standard libraries.
  • asm: to allow us to call inline assembly directly
  • start: to allow us to override the entry point to our program

We then disable the standard library with #![no_std]. Then tell rust we want a statically linked executable #![crate_type="staticlib"] and declare we want to use volatile_store from core::intrinsics.

src/main.rs
#![feature(lang_items,no_std,core_intrinsics,asm,start)]
#![no_std]
#![crate_type="staticlib"]

use core::intrinsics::{volatile_store};

And now some required language functions which just cause the code to halt if we encounter an error.

src/main.rs
#[lang="stack_exhausted"] extern fn stack_exhausted() {}
#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"]
#[no_mangle]
pub extern fn rust_begin_unwind(_fmt: &core::fmt::Arguments, _file_line: &(&'static str, usize)) -> !
{
    loop {}
}

#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr0() -> ()
{
    loop {}
}

#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr1() -> ()
{
    loop {}
}

The last bit of new code is the lang_start function. In truth the actual start point is the function that reset vector points to: the second ISRVector for the teensy 3. In this example it is the startup function, which unfortunately needs a different signature then the one rust expects. Instead we create a simple wrapper to satisfy the compiler.

src/main.rs
#[start]
fn lang_start(_: isize, _: *const *const u8) -> isize {
    unsafe {
        startup();
    }
    0
}

The rest of the code is a direct port from the C example. The only real difference is the syntax, its function is identical the the C version and is described in my previous post.

One thing to note however is there is no volatile keyword in rust. Instead we define the macros without it and use volatile_store to write the value and stop the compiler optimizing out the code.

Target Specification: thumbv7em-none-eabi.json

The rust compiler needs some information to tell it how to compile for the arm architecture, the thumbv7em-none-eabi.json file is what does this. This file was obtained from the zinc project. More details on the options can be found here but you only need to edit it if you want to target a different platform.

Cargo

At this point we are able to build a rust program by downloading the rust core, compiling it then compiling src/main.rs against the new rust core. If you want to take this approach I recommend reading this blog post but in this post we are going to look at compiling our program with cargo.

Cargo is the dependency manager/rust build tool. It will help automate some of the steps required to build the application including downloading and building our dependencies (currently only rust-core).

Cargo Configuration: Cargo.toml

This file tells cargo some basic details about our project, such as its name, version, authors as well as the dependencies required to build the project (rust-libcore in our case). It also tells cargo to use build.rs as our build script.

Build Script: build.rs

This file allows us to preform any custom build steps before cargo tried to build our application. This is useful for building c components or running other tools that generate files required to build our program. In this case we simply tell rust to use the OUT_DIR as part of the linker search path.

Cargo Options: .cargo/config

This file contains options used to customize cargo's behavior. It is used here to specify the linker and archive tool for the arm processor. We could specify these on the command line, but since they wont change it is nicer to put them in a configuration file instead.

Compile and upload

Compiling is very simple and unlike the c and assembly examples it doesn't get more complex as the project grows as cargo handles this for us.

cargo build --target thumbv7em-none-eabi
arm-none-eabi-objcopy -O ihex -R .eeprom target/thumbv7em-none-eabi/debug/blink blink.hex
echo "Reset teensy now"
teensy-loader-cli -w --mcu=mk20dx256 blink.hex

Note that this compiles a debug version of the application, to compile for release you simply pass the --release flag to cargo. However when I tried to do this rustc decided none of my code was 'used' and optimized it all away. I could not find any satisfying solution to this. Most of the example I found used some c code to handle the startup code and rust to handle the application logic but I wanted to see if it was possible to avoid. I believe the zinc project have gotten around it some how, but I was unable to get their example to compile correctly and I cannot see from their source how/if they achieved it.

Conclusion

Overall this project took quite a bit longer then I expected. Rust has a higher learning curve then I thought it had and it takes a while to get use to its ownership model. It feels like getting your head around pointers for the first time in c, except the compiler refuses to compile when you get it wrong. Which in a way is better then your program randomly crashing for no apparent reason.

This use case for rust is still highly experimental, hence the need for rust-nightly and prone to randomly breaking in newer version. I have already seen this problem in most of the examples out there, they now just refuse to compile against the latest version of rust-nightly, which will likely happen to this one at some point. This makes the whole process more tedious then it should be as allot of the information out there is out of date.

I don't feel bare metal/embedded rust is quite ready yet for a serious project. It is still a very interesting language which I hope to play around with a bit more on a more stable branch.