New Project

$ cargo new microrust-start
     Created binary (application) `microrust-start` project
$ cd microrust-start
Cargo.toml  src

This has created a binary crate.

Now we could $ cargo build this, and even $ cargo run it, but everything is being compiled for, and run on, your computer.

Targets

The micro:bit has a different architecture than your computer, so the first step will be to cross compile for the micro:bit's architecture. If you were to do an internet search, you would find a platform support list for Rust. Looking into this page, you will find the micro:bit's nRF51822 Cortex-M0 microprocessor:

thumbv6m-none-eabi [*] [ ] [ ] Bare Cortex-M0, M0+, M1

"thumbv6m-none-eabi" is known a a target triple. Note what the star represents:

These are bare-metal microcontroller targets that only have access to the core library, not std.

To install this target:

$ rustup target add thumbv6m-none-eabi

Build 1

Now how should we use this? Well, if you were to take a look at $ cargo build -h, you would try:

$ cargo build --target thumbv6m-none-eabi
error[E0463]: can't find crate for `std`
  |
  = note: the `thumbv6m-none-eabi` target may not be installed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0463`.
error: Could not compile `microrust-start`.

To learn more, run the command again with --verbose.

The help note is rather unhelpful because we just installed that target. We also just noted that the thumbv6m-none-eabi target does not include std, only the core crate, which is has a platform independent subset of the std features. Why is it still looking for the std crate when we build?

no_std

It turns out, rust will always look for the std crate unless explicitly disabled, so we will add the no_std attribute

src/main.rs

#![no_std]

fn main() {
    println!("Hello, world!");
}

Build 2

$ cargo build --target thumbv6m-none-eabi
error: cannot find macro `println!` in this scope
 --> src/main.rs:4:5
  |
4 |     println!("Hello, world!");
  |     ^^^^^^^

println is a macro found in the std crate. We don't need it at the moment, so we can remove it and try to build again.

Build 3

$ cargo build --target thumbv6m-none-eabi
error: `#[panic_handler]` function required, but not found

This error, is because rustc required a panic handler to be implemented.

panic_impl

We could try and implement the panic macro ourselves, but it's easier and more portable to use a crate that does it for us.

If we look on crates.io for the panic-impl keyword we will find some examples. Let us pic the simplest one, and add it to our Cargo.toml. If you have forgotten how to do this, try looking at the cargo book.

Cargo.toml

[dependencies]
panic-halt = "~0.2"

src/main.rs

#![no_std]

extern crate panic_halt;

fn main() {
}

Build 4

$ cargo build --target thumbv6m-none-eabi
error: requires `start` lang_item

no_main

In the normal command line rust binaries you would be used to making, executing the binary usually has the operating system start by executing the C runtime library (crt0). This in turn invokes the Rust runtime, as marked by the start language item, which in turn invokes the main function.

Having enabled no_std, as we are targeting on a microcontroller, neither the crt0 nor the rust runtime are available, so even implementing start would not help us. We need to replace the operating system entry point.

You could for example name a function after the default entry point, which for linux is _start, and start that way. Note, you would also need to disable name mangling:


# #![allow(unused_variables)]
#![no_std]
#![no_main]

#fn main() {
#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}
#}

This is the end of the road for trying to get this to work on our own. At this point we need the help of a board-specific support crate and a few cargo tweaks to get this working.

microbit crate

Let us add a dependency on the board crate for the micro:bit.

[dependencies]
panic-halt = "~0.2"
microbit="~0.7"

The microbit crate has 2 notable dependencies:

embedded-hal

This crate is a HAL implementation crate, where HAL stands for hardware abstraction layer. As rust becomes more and more popular in embedded development, it is desireable to have as little hardware specific implementation as possible.

For this reason, the embedded-hal crate contains a range of hardware abstraction traits which can be implemented by board specific crates.

cortex-m-rt

This crate implements the minimal startup / runtime for Cortex-M microcontrollers. Among other things this crate provides:

  • the #[entry] attribute, to define the entry point of the program.
  • a definition of the hard fault handler
  • a definition of the default exception handler

This crate requires:

  • a definition of the specific microcontroller's memory layout as a memory.x file. fortunately this is usually provided by the board support crates

To use the #[entry] attribute, we will need to add this as a dependency.

For more detailed information, you can use the helpful cortex-m-quickstart crate and its documentation.

cargo config

Before we go any further, we are going to tweak the cargo's configuration by editing microrust-start/.cargo/config. For more information, you can read the documentation here.

.cargo/config

# Configure builds for our target, the micro:bit's architecture
[target.thumbv6m-none-eabi]
# Execute binary using gdb when calling cargo run
runner = "arm-none-eabi-gdb"
# Tweak to the linking process required by the cortex-m-rt crate
rustflags = [
    "-C", "link-arg=-Tlink.x",
    # The LLD linker is selected by default
    #"-C", "linker=arm-none-eabi-ld",
]

# Automatically select this target when cargo building this project
[build]
target = "thumbv6m-none-eabi"

arm-none-eabi-gdb

This is a version of gdb (the GNU debugger) for the ARM EABI (embedded application binary interface). It will allow us to debug the code running on our micro:bit, from your computer.

Build target

Now, all you need to do is run $ cargo build, and cargo will automatically add --target thumbv6m-none-eabi.

Build 5

Cargo.toml

[dependencies]
panic-halt = "~0.2"
microbit="~0.7"
cortex-m-rt="~0.6"

src/main.rs

#![no_std]
#![no_main]

extern crate panic_halt;

use cortex_m_rt::entry;

#[entry]
fn main() {
}
$ cargo build
error: custom attribute panicked
 --> src/main.rs:7:1
  |
7 | #[entry]
  | ^^^^^^^^
  |
  = help: message: `#[entry]` function must have signature `[unsafe] fn() -> !`

! return type

A little known rust feature, so I will forgive you if you do not know what this means. A return type of ! means that the function cannot return. An easy way to implement this is to use an infinite loop.

src/main.rs

#![no_std]
#![no_main]

extern crate panic_halt;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {}
}

Build 6

If you try building now, you should finally be greeted with Finished!

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s

Build Complete

As a sanity check, let's verify that the produced executable is actually an ARM binary:

$ file target/thumbv6m-none-eabi/debug/microrust-start
target/thumbv6m-none-eabi/debug/microrust-start: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
                                                                            ^^^  ^^^^^