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.