Learning Embedded Rust


Embedded Rust is relatively immature. Many libraries are machine generated from CMSIS-SVD files and lack documentation. Beginners are advised to stick to a well-documented board, like the MicroBit or the F3DISCOVERY. Thanks to a grant from RS electronics, I was able to work through community written books on embedded rust.

Key Takeaways

The existing tutorial books are very good. It is delightful to see all of my questions premptively addressed by the book, and to feel like I am doing things correctly.

I would suggest reading the books this order:

  1. If you don’t know Rust, read the the rust book
  2. If you know Rust, but not embedded development then read the Discovery book in targeting either the MicroBit or STM32F3 boards. Both books cover similar content, but at the time of writing the F3 book is more complete.
  3. Else, read through whatever interests you on the embedded rust bookshelf

Whilst the rust ecosystem has fantastic resources for beginners, it has clear gaps for serious commercial use. I haven’t yet personally run into these roadblocks, so I am synthesising the below section from Embedded Rust Commercial Gaps and Not Yet Awesome Embedded Rust:

Reading the F3 Discovery book

This book really tries to hold your hand through the steps. Here’s a section towards the beginning of the book:

Connect the STM32F3DISCOVERY using the USB cable to the USB port in the center of edge of the board, the one that’s labeled “USB ST-LINK”.

Two red LEDs should turn on right after connecting the USB cable to the board.

It’s written very well; the book has a tendancy to preempt exactly the kind of questions that you are thinking about. Chapter five is a very good example of this. It’s about debuggers and OpenOCD, and the book read my mind at every step:

Tangential GDB Trivia

GDB’s help screen mentions running help obscure to display obscure commands. This lead me to discover this cool feature.

Did you know you can drop into a python interpreter inside gdb? Just run python-interactive, or it’s shortened form pi:

(gdb) pi
>>> print("Hello from Python".title())
Hello From Python

By the looks of it, you’re running the default system installation of Python (I bet you could change that with a virtual environment):

>>> sys.version
'3.11.5 (main, Sep  2 2023, 14:16:33) [GCC 13.2.1 20230801]'

Naturally, we want to find out what we can use this REPL for. Let’s run globals(), which shows us all global values:

>>> from pprint import pprint
>>> pprint(globals())
{'GdbRemoveReadlineFinder': <class '__main__.GdbRemoveReadlineFinder'>,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>,
 '__doc__': None,
 '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'gdb': <module 'gdb' from '/usr/share/gdb/python/gdb/__init__.py'>,
 'pprint': <function pprint at 0x7fca63d75120>,
 'sys': <module 'sys' (built-in)>}

Wait, there’s a gdb Python module? That’s so cool! You can actually script the debugger:

>>> gdb.execute("continue")

Breakpoint 1, clocks_and_timers::__cortex_m_rt_main_trampoline () at src/09-clocks-and-timers/src/main.rs:22
22      #[entry]

Looking online, it seems like you can go very far with this — here’s an article that creates custom commands to debug multithreaded code.

END TANGENT

Wow that was a rabbit hole. Where was I?

The chapter five challenge was a simple LED spinning exercise, which I solved as below:

fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let step: u16 = 50;
    let mut iter = (0..8).cycle();

    let mut prev = iter.next().unwrap();
    leds[prev].on().ok();

    for new in iter {
        leds[new].on().ok();
        delay.delay_ms(step);

        leds[prev].off().ok();
        delay.delay_ms(step);
        prev = new
    }

    unreachable!()
}

Right around this time, I ran into some out-of-dateness. But it’s literally a FREE book written by contributors - you can’t expect perfection.

In fairness, I had to adapt some previous commands too (copied below for my personal reference):

$ openocd -f interface/stlink.cfg -f target/stm32f3x.cfg
$ ~/.cargo/bin/itmdump -F -f itm.txt

Chapters seven and eight was where I really started to struggle. These chapters introduce the reference manual, a 1141 pages long and unforgiving specification for the STM32F3 development board. I initally struggled with the manual, but I’ve come to appreciate it. Everything is detailed precisely and exactly once, you just need to be patient and find it.

With that said, there’s quite a few times where you’re required to speculate definitions from context. “GPIOx_BRR” is unspecified, but we’re told what BSRR stands for Bitwise Set and Reset Register, so I’m guessing GPIOx_BRR is the Bitwise Set Register for GPIOx. I’m occasionally incorrect with my guesses: AHB stands for Advanced High-performance Bus, but APB is not Advanced Performance Bus like I thought.

Translating the reference manual into Rust code was quite feasible. There’s alot of black magic in the crates used, but we can manipulate individual bits well enough:

// Turn on all the LEDs in the compass
gpioe.odr.write(|w| {
	w.odr8().set_bit();
	w.odr9().set_bit();
	w.odr10().set_bit();
	w.odr11().set_bit();
	w.odr12().set_bit();
	w.odr13().set_bit();
	w.odr14().set_bit();
	w.odr15().set_bit()
});

Embedded programming terseness did come to bite me. For example, I mistook what what rcc.ahbenr.write(|w| w.iopeen()) did. Translating each token, we have

In fact, rust-analyser brings up “Bit 21 - I/O port E clock enable” when hovering over .iopeen(), so I assumed that this would enable GPIOe. It turns out this is not sufficient, and you actually need to run .iopeen().enabled()!

As difficult as they are, the later chapters are really the bread and butter of embedded programming. The book is very good at slowly ramping up in difficulty:

I loved chapter 11. It teaches how USART communication works, lets you make the standard “buffer overrun” mistake, and builds up to the correct solution midway through the chapter:

for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
    // busy wait until it's safe to write to TDR
    while usart1.isr.read().txe().bit_is_clear() {}

    // transmit byte
    usart1.tdr.write(|w| w.tdr().bits(u16::from(*byte)));
}

And now the chapter flips the problem on its head. As a HAL writer, how would you allow users to communicate over USART? After 10 chapters of embedded systems problems, we have a software engineering problem.

The approach they take is far more interesting than “create a function”. They use Rust’s Newtype pattern with a custom fmt::Write implementation, and create a macro to allow you to use Rust’s format syntax:

let mut serial = SerialPort { usart1 };
uprintln!(serial, "The answer is {}", 40 + 2);

I found this very unexpected and refreshing.

Concluding Thoughts

Needless to say, I am very grateful to RS electronics for funding this adventure. I’ve really appreciated the fact that I can pick up a well polished guide, and have it just work for me, because I’ve got the exact same hardware as the book. Thank you so much for funding my project!