Embedded Rust on BBC Micro Bit: unlocking Vec and HashMap

Cyril Marpaud - Mar 24 '23 - - Dev Community

As an engineer having spent most of 2022 learning the Rust language, I was a little worried about the no_std side of embedded systems programming.

Embedded systems, like the BBC Micro Bit (a small ARM Cortex-M4F-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 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 and HashMap on a BBC Micro Bit.

The original article and associated examples are available in my Micro Bit Vec and 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
  • 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.

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:

sudo apt install --yes curl gcc libudev-dev=251.4-1ubuntu7 pkg-config
Enter fullscreen mode Exit fullscreen mode

(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 and Cargo. Rustup can take care of that for us:

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

As we will be compiling for an ARM Cortex-M4F microcontroller, we have to install the adequate target:

rustup target add thumbv7em-none-eabihf
Enter fullscreen mode Exit fullscreen mode

After compilation comes flashing. cargo embed is the solution we will be using for that purpose. Install it like so:

cargo install cargo-embed
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

Setting up the project

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

cargo init microbit
cd microbit
cargo add cortex-m-rt microbit-v2 panic_halt
Enter fullscreen mode Exit fullscreen mode

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:

[default.general]
chip = "nrf52833_xxAA"
Enter fullscreen mode Exit fullscreen mode

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:

mkdir .cargo
touch .cargo/config
Enter fullscreen mode Exit fullscreen mode
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
    "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"
Enter fullscreen mode Exit fullscreen mode

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

#![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);
    }
}

Enter fullscreen mode Exit fullscreen mode

Blinking an LED

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

cargo embed
Enter fullscreen mode Exit fullscreen mode

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 as it is actually available in the alloc crate. 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). We also need the cortex-m crate to handle critical sections. Add them to the project's dependencies like so:

cargo add embedded-alloc
cargo add cortex-m --features critical-section-single-core
Enter fullscreen mode Exit fullscreen mode

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

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

use embedded_alloc::Heap;

#[global_allocator]
static HEAP: Heap = Heap::empty();
Enter fullscreen mode Exit fullscreen mode

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

{
    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) }
}
Enter fullscreen mode Exit fullscreen mode

Replace the main loop, using Vec:

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 {}
Enter fullscreen mode Exit fullscreen mode

Finally, compile and flash:

cargo embed
Enter fullscreen mode Exit fullscreen mode

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, 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:

[toolchain]
channel = "nightly"
Enter fullscreen mode Exit fullscreen mode

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

rustup component add rust-src
Enter fullscreen mode Exit fullscreen mode

In .cargo/config, add the following lines (panic_abort is needed here because of a currently unresolved issue):

[unstable]
build-std = ["std", "panic_abort"]
Enter fullscreen mode Exit fullscreen mode

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

#![no_std]
Enter fullscreen mode Exit fullscreen mode
extern crate alloc;
use alloc::vec::Vec;
Enter fullscreen mode Exit fullscreen mode

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

use panic_halt as _;
Enter fullscreen mode Exit fullscreen mode
cargo remove panic-halt
Enter fullscreen mode Exit fullscreen mode

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:

#![feature(restricted_std)]
Enter fullscreen mode Exit fullscreen mode

Import HashMap:

use std::{
    collections::{hash_map::DefaultHasher, HashMap},
    hash::BuildHasherDefault,
};
Enter fullscreen mode Exit fullscreen mode

And use it instead of Vec:

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 {}
Enter fullscreen mode Exit fullscreen mode

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 either does nothing or fails.

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:

cargo embed
Enter fullscreen mode Exit fullscreen mode

Rust's HashMap on a Micro Bit, Hooray !

Actually using HashMap

The alphabet folder 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:

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
Enter fullscreen mode Exit fullscreen mode

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

Crates

Tutorials

Whoami

My name is Cyril Marpaud, I'm an embedded systems freelance engineer and a Rust enthusiast 🦀 I have nearly 10 years experience and am currently living in Lyon (France).

. . . .