Rust on BBC micro:bit - starting with a blinky

I’ve decided to give it a go and learn more about how Rust can be used on tiny embedded platforms. I’ve selected micro:bit as target and googled for few possible tutorials how to use one on another. This post summarizes things I’ve experienced, solving various errors I’ve got and learning the hard way what the final minimal set up is.

Getting tools ready

I’ve planed to go for the following set up on my Mac machine:

  • Visual Studio Code as a code editor and a simple IDE (offering debugging support) plus number of plugins to support what follows
  • pyocd as GDB server for micro:bit board (since I’ve was using it already before) and it seemed to be sufficient for simple debugging on the target
  • Rust with nightly channel targeting ARMv6 architecture

Rust for embedded Arm

The first task I’ve tried was a hello world like app equivalent for embedded world - a blinky LED. That requires however some basic board support. Also, I started to have some doubts how much will it take to get things in place, especially when it comes to start up code, linker settings etc.

I’ve found there is one simply called microbit that offers support for Rust running on micro:bit devices. Reading about this one is what I started with then. It turned that the latest version available was 0.5.4 but unfortunately, there was no documentation generated for it. I’ve decided then to go and use 0.5.4 but read the documentation of 0.5.1 (which was available) assuming is close enough to be still useful.

After reading more, I’ve found this board support package follows concept of embedded-hal traits - an initiative, which sounds promising and right to me, in terms of creating larger ecosystems of libraries, unified interfaces for accessing typical resources of embedded platforms. Nice stuff. Appreciate. Let’s move on.

After reading microbit Rust crate documentation, I’ve found a macro entry that suppose to define the entry point for the application. Good. Looked promising.

The docs said:

The specified function will be called by the reset handler after RAM has been initialized. In the case of the thumbv7em-none-eabihf target the FPU will also be enabled before the function is called.

The signature of the specified function must be fn() -> ! (never ending function)

OK. cool. This also gave a hint how my main equivalent would look like, and that I should probably use proper thumbvXXXX target. Knowing that micro:bit runs on Cortex-M0, I assume this is thumbv6m-none-eabi Rust target.

I’ve so far had only experience with using Rust on x86 architecture, targeting Mac OS and compiling system applications, so getting Rust things properly set up for embedded device was a new thing to me.

Executing

rustup target list

gave long list where these were present:

...
thumbv6m-none-eabi
...
rustup target add thumbv6m-none-eabi

gave:

  5.5 MiB /   5.5 MiB (100 %) 783.9 KiB/s ETA:   0 s
info: installing component 'rust-std' for 'thumbv6m-none-eabi'

and quick verification with

rustup target list 

confirms that:

...
thumbv6-none-eabi (installed)
...

OK. Then, let’s create an empty application, using microbit crate, an empty infinite loop passed to the entry macro discovered before and compilable for newly installed ARM target. Adding blinked LED code saved for little, once it compiles and looks boot’able etc.

For that I’ve created new Cargo project:

cargo new --bin microbit-blinky

then, I’ve edited Cargo.toml to give our dependent crate as well as set up target.

...
[dependencies]
microbit = "0.5.1"

Eventually, I’ve edited src/main.rs removing all, and putting this in:

#[macro_use(entry)]
extern crate microbit;

entry!(main_loop);

fn main_loop()->!
{
    loop {
    }
}

Now, trying to compile it with:

cargo build --target=thumbv6-none-eabi

ends up with a failure:

   Compiling cc v1.0.17
   Compiling vcell v0.1.0
   Compiling aligned v0.2.0
   Compiling r0 v0.2.2
   Compiling nrf51 v0.5.0
   Compiling bare-metal v0.2.0
   Compiling void v1.0.2
   Compiling nb v0.1.1
   Compiling cast v0.2.2
   Compiling panic-abort v0.2.0
   Compiling volatile-register v0.2.0
   Compiling embedded-hal v0.2.1
   Compiling cortex-m-rt v0.5.1
   Compiling cortex-m v0.5.2
error: failed to run custom build command for `cortex-m v0.5.2`
process didn't exit successfully: `/Users/adam/workingCopies/microbit-blinky/target/debug/build/cortex-m-402e54b462d76edc/build-script-build` (exit code: 101)
--- stdout
TARGET = Some("thumbv6m-none-eabi")
OPT_LEVEL = Some("0")
TARGET = Some("thumbv6m-none-eabi")
HOST = Some("x86_64-apple-darwin")
TARGET = Some("thumbv6m-none-eabi")
TARGET = Some("thumbv6m-none-eabi")
HOST = Some("x86_64-apple-darwin")
CC_thumbv6m-none-eabi = None
CC_thumbv6m_none_eabi = None
TARGET_CC = None
CC = None
HOST = Some("x86_64-apple-darwin")
CROSS_COMPILE = None
TARGET = Some("thumbv6m-none-eabi")
HOST = Some("x86_64-apple-darwin")
CFLAGS_thumbv6m-none-eabi = None
CFLAGS_thumbv6m_none_eabi = None
TARGET_CFLAGS = None
CFLAGS = None
DEBUG = Some("true")
running: "arm-none-eabi-gcc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-mthumb" "-march=armv6m" "-Wall" "-Wextra" "-o" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/build/cortex-m-5bef1d7f82e9f85a/out/asm/basepri_r.o" "-c" "asm/basepri_r.s"

--- stderr
thread 'main' panicked at '

Internal error occurred: Failed to find tool. Is `arm-none-eabi-gcc` installed?

', /Users/adam/.cargo/registry/src/github.com-1ecc6299db9ec823/cc-1.0.17/src/lib.rs:2180:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Hmm, drat and double drat. Clearly the target depends on Arm cross-compiler that is not installed by default in the PATH on my console. I luckily have one somewhere in my home directory. I only hope that the version I have is going to fulfil Rust’s requirements (which are not clear at the moment). So let’s fix that and bring the compiler into the environment:

export PATH=$PATH:~/programs/gcc-arm-none-eabi/bin

then, just to verify:

arm-none-eabi-gcc --version

gives:

arm-none-eabi-gcc (GNU Tools for Arm Embedded Processors 7-2017-q4-major) 7.2.1 20170904 (release) [ARM/embedded-7-branch revision 255204]
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

OK, repeating the build with Cargo, now goes a step further, but still fails with:

   Compiling cc v1.0.17
   Compiling vcell v0.1.0
   Compiling r0 v0.2.2
   Compiling bare-metal v0.2.0
   Compiling nrf51 v0.5.0
   Compiling aligned v0.2.0
   Compiling nb v0.1.1
   Compiling void v1.0.2
   Compiling cast v0.2.2
   Compiling panic-abort v0.2.0
   Compiling volatile-register v0.2.0
   Compiling embedded-hal v0.2.1
   Compiling cortex-m-rt v0.5.1
   Compiling cortex-m v0.5.2
   Compiling nrf51-hal v0.5.1
   Compiling microbit v0.5.4
   Compiling microbit-blinky v0.1.0 (file:///Users/adam/workingCopies/microbit-blinky)
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 `microbit-blinky`.

Partial success. The GCC compiler for Arm seems to work. Now it’s time to find more about how to bring std crate in place.

Googling for the problem, reveals that I was too quick. Since Rust is adding std create dependency automatically, and this crate contains lots of OS dependent utils, it becomes an unwanted passenger here, while we’re creating a truly bare metal application. A special declaration at the top of the main.rs file should solve the problem:

#![no_std]
...

Then, compilation give another error:

rror[E0601]: `main` function not found in crate `microbit_blinky`
  |
  = note: consider adding a `main` function to `src/main.rs`

Hmm. That’s not quite as expected. I definitely prefer to rely on entry! macro, expecting it will be correctly called from whatever start up code the board support crate (or others) have there. Let’s try to rename the src/main.rs into src/lib.rs instructing Cargo to compile it as lib, hoping that this will give correct binary eventually.

Again:

cargo build --target=thumbv6m-none-eabi

Builds successfully. But no interesting file is created apart from libmicrobit-blinky.rlib that seems to be unusable for flashing the target.

Reading more on internet suggests that I should rather be building executable intstead (so reverting to where I started), but with #[no_main] directive instead. Let’s do that then. After adding it to the top of main.rs, and building again, I get:

   Compiling microbit-blinky v0.1.0 (file:///Users/adam/workingCopies/microbit-blinky)
error: language item required, but not found: `panic_impl`

error: aborting due to previous error

error: Could not compile `microbit-blinky`.

Hmm. Another thing missing. It took some minutes of again reading in the posts and internet resources, to figure out that I should use panic_abort crate as an explicit dependency and that my main source file should also contain extern crate panic_abort. Dependency listed on crates.io show that microbit crate depends ont 0.2.0 version of it, so let’s use that one:

[dependencies]
microbit = "0.5.4"
panic-abort = "0.2.0"

Compilation attempt ends up with another failure:

   Compiling microbit-blinky v0.1.0 (file:///Users/adam/workingCopies/microbit-blinky)
error: linking with `arm-none-eabi-gcc` failed: exit code: 1
  |
  = note: "arm-none-eabi-gcc" "-L" "/Users/adam/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/thumbv6m-none-eabi/lib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/microbit_blinky-4869aab4822badaa.4z7ec9o5pn3hdnqv.rcgu.o" "-o" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/microbit_blinky-4869aab4822badaa" "-Wl,--gc-sections" "-nodefaultlibs" "-L" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps" "-L" "/Users/adam/workingCopies/microbit-blinky/target/debug/deps" "-L" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/build/cortex-m-5bef1d7f82e9f85a/out" "-L" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/build/cortex-m-rt-3596bea14ea81553/out" "-L" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/build/cortex-m-rt-3596bea14ea81553/out" "-L" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/build/nrf51-7de3448122cc482f/out" "-L" "/Users/adam/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/thumbv6m-none-eabi/lib" "-Wl,--start-group" "-Wl,-Bstatic" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libpanic_abort-074e43b8f97afa16.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libmicrobit-61187ae4374826f9.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libnrf51_hal-1eb8a87f2591f3da.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libnrf51-130e6761ae6c06ec.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libcortex_m_rt-f6f1b6e6aea3ffd0.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libr0-6ea449223a99715c.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libembedded_hal-53fb5298b03df9f6.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libvoid-7c4b7824b1a90377.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libnb-b4bb0bb2af3de732.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libcortex_m-4bf69a11506a6285.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libvolatile_register-bac15e42df1355f1.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libvcell-a90db53dbcf815a6.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libaligned-f6628ebd8d9421a7.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libcast-4135eaa7004f4323.rlib" "/Users/adam/workingCopies/microbit-blinky/target/thumbv6m-none-eabi/debug/deps/libbare_metal-a932c2a37cfb2c53.rlib" "/Users/adam/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/thumbv6m-none-eabi/lib/libcore-fb37a4ea1db1e473.rlib" "-Wl,--end-group" "/Users/adam/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/thumbv6m-none-eabi/lib/libcompiler_builtins-f2357c0397dd7e0d.rlib" "-Wl,-Bdynamic"
  = note: /Users/adam/programs/gcc-arm-none-eabi-7-2017-q4-major/bin/../lib/gcc/arm-none-eabi/7.2.1/../../../../arm-none-eabi/lib/crt0.o: In function `_start':
          (.text+0xe0): undefined reference to `__libc_init_array'
          (.text+0xf0): undefined reference to `exit'
          collect2: error: ld returned 1 exit status
          

error: aborting due to previous error

error: Could not compile `microbit-blinky`.

Ok. Now compilation went fine, the error comes from linker, looks like we’re missing some symbols. To me it looks like there should be a way to stub those with some empty ones, since I plan no exit from the bare metal application (infinite loop) as well as I probably will net rely on any __libc_init_array functionality, because no constructor and initialization of static objects are expected. Both probably come from standard startup code, having some C/C++ heritage. I’m surprised they are not stubbed by microbit create somehow. As first attempt I’ll try to stub them directly in my main.rs file:

...
#[no_mangle]
pub fn exit(){}

#[no_mangle]
pub fn __libc_init_array(){}
...

Ok. Compilation and ….

…Success!

Let’s examine what we’ve ended up with:

ls -l target/thumbv6m-none-eabi/debug/

shows:

total 88
drwxr-xr-x   5 adam  staff    160 10 Jul 20:12 build
drwxr-xr-x  35 adam  staff   1120 10 Jul 20:33 deps
drwxr-xr-x   2 adam  staff     64 10 Jul 20:12 examples
drwxr-xr-x   4 adam  staff    128 10 Jul 20:13 incremental
-rwxr-xr-x   2 adam  staff  38920 10 Jul 20:33 microbit-blinky
-rw-r--r--   1 adam  staff    145 10 Jul 20:33 microbit-blinky.d
drwxr-xr-x   2 adam  staff     64 10 Jul 20:12 native

where microbit-blinky looks like our bare metal executable candidate. Let’s examine it a bit closer then:

nm target/thumbv6m-none-eabi/debug/microbit-blinky

gives:

00000000 n 
00000000 n 
00000041 n 
0000004d n 
00000077 n 
00000087 n 
000000ba n 
000000c4 n 
000000c9 n 
000000db n 
000000e2 n 
000000e7 n 
000000e9 n 
000000f1 n 
000000f3 n 
00000136 n 
00000144 n 
00000156 n 
0000015a n 
00000161 n 
00000169 n 
00000170 n 
00008230 r 
000081a0 t _ZN15microbit_blinky9main_loop17hf1fec39a9744497aE
00008230 r __FRAME_END__
000081e8 t ____libc_init_array_from_arm
00018258 B __bss_end__
0001823c B __bss_start
0001823c B __bss_start__
0001823c T __data_start
0000801c t __do_global_dtors_aux
00018238 t __do_global_dtors_aux_fini_array_entry
00018258 B __end__
000081d0 t __exit_from_arm
00018234 t __frame_dummy_init_array_entry
000081a4 T __libc_init_array
000081dc t __main_from_arm
000081f4 t __memset_from_arm
00018258 B _bss_end__
0001823c T _edata
00018258 B _end
00008210 T _fini
00008000 T _init
0000808c T _mainCRTStartup
00080000 N _stack
0000808c T _start
0001823c b completed.8654
00008018 T exit
0000805c t frame_dummy
000081a8 T main
000081bc T memset
00018240 b object.8659

Looks good. We have a main there (I guess provided by microbit crate), we have number of symbols linker gave us as well as mangled _ZN15microbit_blinky9main_loop17hf1fec39a9744497aE which is my ‘real’ entry point main_loop.

I should be able to flash it to the microbit board. The issue is the program still does nothing. Let’s get going and try to come up with a code that lights up a single LED anywhere on the board.

Making something blink

My intention is to blink one of the LEDs on the buil-in LED matrix of microbit board. It wasn’t easy to figure out based on the existing documentation, how-to approach to the code that drives GPIO. I’ve found excellent reference on microbit crate github repository example and decided to reuse plenty of code from there as a starting point.

The simple delay loop I’m going to use to have some human visible blinking, is going to be using for loop and thousands of NOP assembly instructions. It appeared that to get the NOP instruction, useful for basic delay loop, we need to use cortex-m create. Once available, the invokation would look like this:

cortex_m::asm::nop();

Another goodie needed is a recipe to control the GPIO port to drive LED matrix.

This is the way to get access to peripherals trait:

let p = microbit::Peripherals::take().take().unwrap();

and that is how it can be configured as output

p.GPIO.pin_cnf[4].write(|w| w.dir().output());

and that is how the pins (represented by bits in a byte value) can be set/cleared:

p.GPIO.out.write(|w| unsafe { w.bits(1 << N) });

My main.rs looks like this now:

#![no_std]
#![no_main]

#[macro_use(entry)]
extern crate microbit;
extern crate panic_abort;
extern crate cortex_m;

entry!(main_loop);

fn main_loop() -> ! {
    // OK, this is a very very optimistic code here.
    let p = microbit::Peripherals::take().take().unwrap();

    // configuring GPIO  pin P0.4 and P0.13 as output
    // to control the matrix LED point (COL1, ROW1)
    p.GPIO.pin_cnf[4].write(|w| w.dir().output());
    p.GPIO.pin_cnf[13].write(|w| w.dir().output());


    let mut state: bool = true;
    loop {
        // some simple delay with busy waiting
        for _ in 0..1_000_000 {
            cortex_m::asm::nop();
        }

        // toggling the state variable that represents the blinking
        state = !state;

        if state {
        // turn the LED on, but setting P0.4 to LOW and PO.13 to HIGH
        p.GPIO.out.write(|w| unsafe { w.bits(1 << 13) });

        } else {
        // turn the LED off by setting both GPIO pins to LOW.
        p.GPIO.out.write(|w| unsafe { w.bits(0) });

        }
    }
}

#[no_mangle]
pub fn exit() {}

#[no_mangle]
pub fn __libc_init_array() {}

and the Cargo.toml dependency section looks like this now:

...

[dependencies]
microbit = "0.5.4"
panic-abort = "0.2.0"
cortex-m = "0.5.2"

Fixing linking

After investingating the end result, it turns that building resulted however some weird results. It lookes like the microbit crate requires that our build explicitly uses a proper memory.x linker script (as suggested on this page) as well as proper linker configuration flags.

We have to specify the linker script by overriding linker config flags.

The linker script located in memory.x file should look like this:

MEMORY
{
  /* NOTE K = KiBi = 1024 bytes */
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 16K
}

/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

If you have ever came across linker scripts used by C/C++ code targetting similar processor family, you could recognize that it’s as minimalistic as can possibly be, but that’s fine. Rust should not need more thant that.

Also the .cargo/config file residing in the project directory will do the right set up of linker flags for the project:

[target.thumbv6m-none-eabi]
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "linker=arm-none-eabi-ld",
  "-Z", "linker-flavor=ld",
]

Building with it, reveals however that there are new things we need to define in our code, to have it compiled/linked successfully.

The main.rs file has to define exception handler defaults like this:

#[macro_use(entry, exception)]
extern crate microbit;
extern crate cortex_m;
extern crate panic_abort;
extern crate cortex_m_rt;

use cortex_m_rt::ExceptionFrame;

exception!(*, default_handler);

fn default_handler(_irqn: i16) {}

exception!(HardFault, hard_fault);

fn hard_fault(_ef: &ExceptionFrame) -> ! {
    loop {}
}
...

Eventually, my main.rs file looked like this:

#![no_std]
#![no_main]

#[macro_use(entry, exception)]
extern crate microbit;
extern crate cortex_m;
extern crate panic_abort;
extern crate cortex_m_rt;

use cortex_m_rt::ExceptionFrame;

exception!(*, default_handler);

fn default_handler(_irqn: i16) {}

exception!(HardFault, hard_fault);

fn hard_fault(_ef: &ExceptionFrame) -> ! {
    loop {}
}

entry!(main_loop);

fn main_loop() -> ! {
    // OK, this is a very very optimistic code here.
    let p = microbit::Peripherals::take().take().unwrap();

    // configuring GPIO  pin P0.4 and P0.13 as output
    // to control the matrix LED point (COL1, ROW1)
    p.GPIO.pin_cnf[4].write(|w| w.dir().output());
    p.GPIO.pin_cnf[13].write(|w| w.dir().output());
    p.GPIO.out.write(|w| unsafe { w.bits(1 << 13) });

    let mut state: bool = true;
    loop {
        // some simple delay with busy waiting
        for _ in 0..1000 {
            cortex_m::asm::nop();
        }

        // toggling the state variable that represents the blinking
        state = !state;

        if state {
            // turn the LED on, but setting P0.4 to LOW and PO.13 to HIGH
            p.GPIO.out.write(|w| unsafe { w.bits(1 << 13) });
        } else {
            // turn the LED off by setting both GPIO pins to LOW.
            p.GPIO.out.write(|w| unsafe { w.bits(0) });
        }
    }
}

Compiling with cargo build again, reports no problems this time, and inspecting the produced executable with nm again, reveals plenty of linked-in functions that look like responsible for booting the system, driving GPIO, etc, so sounds good to me at this point.

Let’s make a HEX file out of it and send the code to the board:

arm-none-eabi-objcopy -O ihex target/thumbv6m-none-eabi/debug/microbit-blinky out.hex
cp out.hex /Volumes/MICROBIT/

Wow. There it is. This has been a journey. A blinking LED on a micro:bit board, purely with Rust!

Summary

The microbit github repository turns to be a great reference of howto use it and there are interesting examples out there on howto access microbit platform resources directly as well as with help of embedded-hal abstraction.

Simiralily, the program can be compiled using release variant settings:

cargo build --release

Note that the delay loop would need to be changed to approx 10x longer, to actually see the LED blinking, since the optimized code will be leader and much quicker.

comments powered by Disqus