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
(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"
As we will be compiling for an ARM Cortex-M4F microcontroller, we have to install the adequate target:
rustup target add thumbv7em-none-eabihf
After compilation comes flashing. cargo embed
is the solution we will be using for that purpose. Install it like so:
cargo install cargo-embed
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
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
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"
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
[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:
#![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:
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 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
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();
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) }
}
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 {}
Finally, compile and flash:
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
, 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"
As building the std
crate requires its source code, use rustup to fetch that component:
rustup component add rust-src
In .cargo/config
, add the following lines (panic_abort
is needed here because of a currently unresolved issue):
[unstable]
build-std = ["std", "panic_abort"]
The std
crate provides an allocator, we can therefore remove those lines from src/main.rs
:
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
std
also provides a panic handler, the import and panic-halt
dependency can therefore be removed:
use panic_halt as _;
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
:
#![feature(restricted_std)]
Import HashMap
:
use std::{
collections::{hash_map::DefaultHasher, HashMap},
hash::BuildHasherDefault,
};
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 {}
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
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
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).