Embedded Rust on BBC Micro Bit: unlocking Vec and HashMap
Tags: rust, embedded, microbit, tutorial
Published: 2023-03-24 09:41:55

As an engineer having spent most of 2022 learning [the Rust language](https://www.rust-lang.org/), I was a little worried about the [`no_std`](https://docs.rust-embedded.org/book/intro/no-std.html) side of embedded systems programming.

Embedded systems, like the [BBC Micro Bit](https://en.wikipedia.org/wiki/Micro_Bit) (a small [ARM Cortex-M4F](https://en.wikipedia.org/wiki/ARM_Cortex-M#Cortex-M4)-based computer designed for educational purposes featuring a 5×5 LED matrix, multiple sensors, Bluetooth Low Energy capabilities and a lot more), are usually programmed as **bare_metal** devices in a `no_std` environment, meaning we can't use the [`std`](https://doc.rust-lang.org/std/index.html) crate where `Vec` and `HashMap`, among others, reside.

While very understandable when considering older devices, the growing specs and capabilities of modern devices make it increasingly tempting to use higher-level abstractions. The purpose of this tutorial is thus to demonstrate how to enable the use of [`Vec`](https://doc.rust-lang.org/std/vec/struct.Vec.html) and [`HashMap`](https://doc.rust-lang.org/std/collections/struct.HashMap.html) on a BBC Micro Bit.

The original article and associated examples are available in my [Micro Bit Vec and HashMap](https://gitlab.com/cyril-marpaud/microbit_vec_hashmap) GitLab repository. Let us now initiate this endeavor.

## Requirements

This tutorial does not require much:

- A computer with internet access
- A [BBC Micro Bit](https://microbit.org)
- A USB cable
- Less than an hour of your time

## Setting up the OS

It is assumed that you have a fully functional 22.10 Ubuntu Linux distribution up and running. If you don't, [detailed instructions to set one up can be found in my previous tutorial](https://gitlab.com/cyril-marpaud/rust_esp_quickstart/-/blob/main/README.md#the-os).

## Setting up the development environment

First of all, we are going to install a few required dependencies. Open a terminal (the default shortcut is `Ctrl`+`Alt`+`T`) and run the following command:

```Bash
sudo apt install --yes curl gcc libudev-dev=251.4-1ubuntu7 pkg-config
```

*(installing version `251.4-1ubuntu7.1` of `libudev-dev` induces a crash on my machine so I'm using version `251.4-1ubuntu7` instead)*

We also need to install [Rust](https://www.rust-lang.org) and [Cargo](https://doc.rust-lang.org/stable/cargo/). [Rustup](https://rustup.rs/) can take care of that for us:

```Bash
curl --proto '=https' --tlsv1.2 --fail --show-error --silent https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
```

As we will be compiling for an ARM Cortex-M4F microcontroller, we have to install [the adequate target](https://doc.rust-lang.org/beta/rustc/platform-support.html):

```Bash
rustup target add thumbv7em-none-eabihf
```

After compilation comes flashing. [`cargo embed`](https://probe.rs/docs/tools/cargo-embed/) is the solution we will be using for that purpose. Install it like so:

```Bash
cargo install cargo-embed
```

Finally, a **udev rule** will take care of granting USB access to the Micro Bit:

```Bash
echo "SUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"0d28\", ATTRS{idProduct}==\"0204\", MODE=\"0660\", GROUP=\"plugdev\"" | sudo tee /etc/udev/rules.d/99-microbit.rules > /dev/null
sudo udevadm control --reload-rules && sudo udevadm trigger
```

## Setting up the project

Cargo makes it easy to create a Rust project and add the adequate dependencies:

```Bash
cargo init microbit
cd microbit
cargo add cortex-m-rt microbit-v2 panic_halt
```

Now, `cargo embed` needs to know which device it has to flash. Create a file named `Embed.toml` at the root of the project with the following content:

```TOML
[default.general]
chip = "nrf52833_xxAA"
```

We can either specify a `--target` flag each time we compile our software or set that up once and for all in a configuration file. Moreover, our device's memory layout needs to be provided to the linker. Create the following `.cargo/config` file which will do just that for us:

```Bash
mkdir .cargo
touch .cargo/config
```

```TOML
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
	"-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"
```

Finally, open `src/main.rs` and copy/paste this LED-blink minimal example inside:

```Rust
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use microbit::{
	board::Board,
	hal::{prelude::*, timer::Timer},
};
use panic_halt as _;

#[entry]
fn main() -> ! {
	let mut board = Board::take().expect("Failed to take board");
	let mut timer = Timer::new(board.TIMER0);
	let mut row = board.display_pins.row1;
	let delay = 150u16;

	board.display_pins.col1.set_low().expect("Failed to set col1 low");

	loop {
		row.set_high().expect("Failed to set row1 high");
		timer.delay_ms(delay);

		row.set_low().expect("Failed to set row1 low");
		timer.delay_ms(delay);
	}
}

```

## Blinking an LED

Plug the board to your computer then compile the program and flash it single-handedly with this simple command:

```Bash
cargo embed
```

When the process ends, you should see the upper-left LED blink. Congratulations!

## Unlocking Vec

I have to admit that I shamefully lied when I told you [`Vec` resides in the `std` crate](https://doc.rust-lang.org/std/index.html?search=vec) as [it is actually available in the `alloc` crate](https://doc.rust-lang.org/alloc/vec/struct.Vec.html). As the name suggests, using it requires an allocator.

Luckily, the `embedded-alloc` crate provides us with one (there is [a complete example in the associated Github repository](https://github.com/rust-embedded/embedded-alloc)). We also need the `cortex-m` crate to handle critical sections. Add them to the project's dependencies like so:

```Bash
cargo add embedded-alloc
cargo add cortex-m --features critical-section-single-core
```

Then, in `src/main.rs`, we need to customize a few things. Import `Vec` and declare a global allocator:

```Rust
extern crate alloc;
use alloc::vec::Vec;

use embedded_alloc::Heap;

#[global_allocator]
static HEAP: Heap = Heap::empty();
```

At the beginning of the `main` function, initialize the allocator and a size for our heap (the Micro Bit has 128KiB of RAM):

```Rust
{
	use core::mem::MaybeUninit;
	const HEAP_SIZE: usize = 8192; // 8KiB
	static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
	unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
```

Replace the main loop, using `Vec`:

```Rust
let mut vec = Vec::new();
vec.push(true);
vec.push(false);
vec.push(true);
vec.push(false);
vec.push(false);
vec.push(false);

vec.iter().cycle().for_each(|v| {
	match v {
		true => row.set_high().expect("Failed to set row high"),
		false => row.set_low().expect("Failed to set row low"),
	}
	timer.delay_ms(delay);
});

loop {}
```

Finally, compile and flash:

```Bash
cargo embed
```

The LED should now be blinking in a heartbeat pattern. You are using Rust's `Vec` on a Micro Bit, congratulations!

## Unlocking HashMap

Unlike `Vec`, [the `alloc` crate does not suffice for `HashMap`](https://doc.rust-lang.org/std/index.html?search=HashMap), full `std` is required (which in turn requires a `nightly` toolchain because `std` is not supported for our platform). To avoid having to type `+nightly` each time we invoke `cargo` or `rustup`, create a file named `rust-toolchain.toml` with the following content:

```TOML
[toolchain]
channel = "nightly"
```

As building the `std` crate requires its source code, use rustup to fetch that component:

```Bash
rustup component add rust-src
```

In `.cargo/config`, add the following lines (`panic_abort` is needed here because of [a currently unresolved issue](https://github.com/rust-lang/rust/issues/83805#issuecomment-812874115)):

```TOML
[unstable]
build-std = ["std", "panic_abort"]
```

The `std` crate provides an allocator, we can therefore remove those lines from `src/main.rs`:

```Rust
#![no_std]
```

```Rust
extern crate alloc;
use alloc::vec::Vec;
```

`std` also provides a panic handler, the import and `panic-halt` dependency can therefore be removed:

```Rust
use panic_halt as _;
```

```Bash
cargo remove panic-halt
```

Now that we are rid of those useless parts, there are a few things we need to add. As we're building `std` for an unsupported (thus flagged *unstable*) platform, we need the `restricted_std` feature. Add it to `src/main.rs`:

```Rust
#![feature(restricted_std)]
```

Import `HashMap`:

```Rust
use std::{
	collections::{hash_map::DefaultHasher, HashMap},
	hash::BuildHasherDefault,
};
```

And use it instead of `Vec`:

```Rust
let mut hm = HashMap::with_hasher(BuildHasherDefault::<DefaultHasher>::default());
hm.insert(0, false);
hm.insert(1, true);
hm.insert(2, false);
hm.insert(3, true);
hm.insert(4, true);
hm.insert(5, true);

hm.values().cycle().for_each(|v| {
	match v {
		true => row.set_high().expect("Failed to set row high"),
		false => row.set_low().expect("Failed to set row low"),
	}
	timer.delay_ms(delay);
});

loop {}
```

The reason we are providing our own hasher is that the default one relies on the `sys` crate which is platform dependent. Our platform being unsupported, [the associated implementation](https://github.com/rust-lang/rust/tree/master/library/std/src/sys/unsupported) either does nothing or [fails](https://github.com/rust-lang/rust/blob/master/library/std/src/sys/unsupported/common.rs#L15).

Therefore, keep in mind that using anything from said `sys` crate will either fail or hang (in particular: threads). `HashMap` is fine though, and the above snippet should make the LED blink in an inverted heartbeat pattern:

```Bash
cargo embed
```

Rust's `HashMap` on a Micro Bit, Hooray !

## Actually using HashMap

The [alphabet folder](https://gitlab.com/cyril-marpaud/microbit_vec_hashmap/-/tree/main/alphabet) of my Gitlab repository demonstrates how to display caracters on the LED matrix using a `HashMap`. You can flash it by running the following commands:

```Bash
cd	# We need to move out of the "microbit" folder we created earlier
sudo apt install --yes git
git clone https://gitlab.com/cyril-marpaud/microbit_vec_hashmap.git
cd microbit_vec_hashmap/alphabet
cargo embed
```

## Conclusion

The ability to use Rust collections on a device as humble as the BBC micro:bit represents a remarkable achievement in embedded programming. Thanks to recent hardware advances, even modest embedded devices can now support high-level abstractions that were once the exclusive domain of larger and more expensive systems.

Rust's efficiency and modern design make it an ideal language for taking advantage of these new capabilities and pushing the limits of what is possible on a microcontroller: developers can create complex and sophisticated projects that once seemed impossible on such small devices, from data-driven sensors to interactive games and applications.

Whether you are a seasoned expert or just getting started, the future of embedded programming is brighter than ever, and Rust is leading the way.

## See also (aka useful links)

### Documentation

- [The Rust Language](https://www.rust-lang.org)
- [BBC Micro Bit](https://en.wikipedia.org/wiki/Micro_Bit)

### Crates

- [microbit](https://docs.rs/microbit-v2/0.13.0/microbit/)
- [LMS303AGR](https://docs.rs/lsm303agr/latest/lsm303agr/index.html)
- [tiny_led_matrix](https://docs.rs/tiny-led-matrix/latest/tiny_led_matrix/)
- [cortex_m](https://docs.rs/cortex-m/0.7.7/cortex_m/)
- [cortex_m_log](https://docs.rs/cortex-m-log/latest/cortex_m_log/)
- [minimult_cortex-m](https://crates.io/crates/minimult_cortex-m)
- [embedded_alloc](https://docs.rs/embedded-alloc/0.5.0/embedded_alloc/)

### Tutorials

- [Discovery](https://docs.rust-embedded.org/discovery/microbit/index.html)
- [`std` on Micro Bit](https://blog.timhutt.co.uk/std-embedded-rust/index.html)

## Whoami

My name is [Cyril Marpaud, I'm an embedded systems freelance engineer and a Rust enthusiast 🦀](https://www.linkedin.com/in/cyrilmarpaud/) I have nearly 10 years experience and am currently living in Lyon (France).