Rust is a fairly new language that has gotten to be very popular in recent years. And as the language matures, it has started to support a wider set of features, including compilation and linking for bare-metal targets. There is an excellent “Embedded Rust” ebook being written which covers the concepts that I’ll talk about here, but it’s still in-progress and there aren’t many turn-key code examples after the first couple of chapters.
The Rust language is less than 10 years old and still evolving, so some features which might change in the future are only available on the nightly
branch at the time of writing; this post is written for rustc
version 1.36. And the language’s documentation is very good, but it can also be a little bit scattered in these early days. For example, after I had written most of this post I found a more comprehensive “Discovery ebook” which covers hardware examples for an STM32F3
“Discovery Kit” board. That looks like a terrific resource if you want to learn how to use the bare-metal Rust libraries from someone who actually knows what they’re talking about.
As a new Rustacean, I’ll admit that the syntax feels little bit frustrating at times. But that’s normal when you learn a new language, and Rust is definitely growing on me as I learn more about its aspirations for embedded development. Cargo looks promising for distributing things like register definitions, HALs, and BSPs. And there’s an automated svd2rust
utility for generating your own register access libraries from vendor-supplied SVD files, which is useful in a language that hasn’t had time to build up an extensive set of well-proven libraries. So in this post I’ll talk about how to generate a “Peripheral Access Crate” for a simple STM32L031K6
chip, and how to use that crate to blink an LED.
The target hardware will be an STM32L031K6 Nucleo-32 board, but this should work with any STM32L0x1
chip. I also tried the same process with an STM32F042
board and the STM32F0x2
SVD file, which worked fine. It’s amazing how easy it is to get started with a new chip compared to C, although you do still need to read the reference manuals to figure out which registers and bits you need to modify. This post will assume that you know a little bit about how to use Rust, but only the very basics – I’m still new to the language and I don’t think I would do a good job of explaining anything. The free Rust ebook is an excellent introduction, if you need a quick introduction.
Step 1: Toolchain Setup
Like with the bare-metal C examples, our first step is to set up a toolchain. The Rust maintainers have made installing and updating the toolchain fairly painless, so I’ll just point you to the “Installation” page of the Rust ebook.
Are you back? Great – the next step is to install the core “embedded Rust” dependencies, and you should also switch to the nightly
branch of Rust’s toolchain. Like I mentioned earlier, some useful features haven’t made it into the stable
branch quite yet. You’ll see an example in the next section, but for now just switch branches with this command:
rustup install nightly rustup default nightly
You can switch back by replacing nightly
with stable
in the above commands. You can also use rustup update
instead of rustup install
to download and build a newer version of the toolchain for a given branch. Anyways, now that you’re on the nightly
branch, follow both the “Tooling” and “Installation” instructions in the embedded Rust ebook.
And finally, you’ll need to install a few extra utilities for auto-generating and -formatting a “Peripheral Access Crate” from the SVD files that most microcontroller vendors distribute:
rustup component add rustfmt cargo install svd2rust form
This would also be a good time to read through the first couple of chapters of the embedded Rust ebook. Chapter 2 is pretty concise, and it’ll help you get a feel for how the language intends to handle core embedded concepts like register definitions, bitfields, interrupts, HALs, and BSPs.
If you run into problems building the example cortex-m-quickstart
template, it might be because Rust’s linker is not distributed on some platforms, like aarch64
. Hopefully that won’t be an issue for much longer, but for now you can use GCC’s linker instead by un-commenting this line in .cargo/config
:
# "-C", "linker=arm-none-eabi-ld",
You should be able to create projects for other architectures as well, but it looks like the svd2rust
utility currently only supports SVD files describing an ARM Cortex-M, MSP430, or RISC-V core. Also, some chips such as the popular ESP32 do not currently have a free toolchain which supports LLVM. As far as I know, you cannot build Rust programs for those chips.
Step 2: Generate a Peripheral Access Crate
Now that you can write, flash, and debug a ‘Hello World’ program for a microcontroller (thanks to chapter 2 of the ’embedded Rust’ book), you’ll need a way to access the microcontroller’s peripheral registers before you can make it do anything interesting. The ‘Memory-mapped Registers‘ section of the book goes over the basic idea of how this works, but the examples are written for a TM4C123
chip which I do not have.
To create a new Peripheral Access Crate (PAC from now on) for the STM32L031
which I’ve used in previous tutorials, we only need an SVD file describing the microcontroller. You can download them for free from ST; on the chip’s product page, click the ‘Resources’ tab and download the “System View Description” .zip
file under the “HW Model, CAD Libraries & SVD” section. Unzip it and find the STM32L0_svd_<version>/STM32L0x1.svd
file.
Next, create a new ‘library crate’ using Cargo, copy the SVD file into it, and remove the default src/
directory:
cargo new stm32l0x1_pac --lib --name stm32l0x1 cd stm32l0x1_pac/ cp [...]/STM32L0x1.svd . rm -rf src/
I should mention that there are already a couple of STM32L0x1
PACs available on crates.io, and I’m sure they work well, but this is still a useful learning exercise. The svd2rust
documentation page has a good explanation of how to create a PAC from an SVD file, but before you actually generate the code, check the recommended dependencies and make sure that they are in your new crate’s Cargo.toml
file. Your version numbers might look different in the future, but at the time of writing it looks something like:
[dependencies] bare-metal = "0.2.4" cortex-m = "0.5.8" vcell = "0.1.0" [dependencies.cortex-m-rt] optional = true version = "0.6.5" [features] rt = ["cortex-m-rt/device"]
In addition to utilities like svd2rust
, the ‘embedded devices Working Group‘ has written packages like cortex-m
and cortex-m-rt
to provide ‘glue’ code like linker scripts and startup logic. Interrupt tables for each chip are actually included in the SVD files, so they will be included in your auto-generated PAC. Once you’ve added those dependencies, you can run svd2rust
and format the large lib.rs
output file using the commands recommended by the documentation:
svd2rust --nightly -i STM32L0x1.svd form -i lib.rs -o src/ rm lib.rs cargo fmt
And that’s it – your new library crate is located under src/
, with a directory for each peripheral and a source file for each type of register. You can test that it builds with cargo build --target thumbv6m-none-eabi
.
If you don’t use the --nightly
option, svd2rust
might not be able to generate mappings for registers which share the same memory address. That happens when a register’s functionality can change based on the situation, and the STM32L0x1
SVD file does have a few of those registers. This is one example of why I didn’t want to use the stable
branch for this tutorial, but it also means that your crate might cause build issues if you aren’t on the nightly
branch when you run cargo build
. You can double-check your current branch with rustup default
.
Step 3: Setup an Embedded Rust Application
Now that we have a library capable of reading and writing peripheral registers, we should be able to write a simple ‘blinking LED’ program. Let’s start by generating an empty Cortex-M project from the same cortex-m-quickstart
template that the ebook used. I ran this command one directory above the stm32l0x1
crate so that both projects were in the same directory, but you can put it anywhere:
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart --name rust-blink cd rust-blink rm -r examples/
We’ll need to make a few changes to target a Cortex-M0+
core, because the default template targets a Cortex-M3
. You can find the end result in this post’s GitHub repository, but I’ll also describe each change here.
First, look in the .cargo/config
file. Un-comment the GNU linker line if you don’t have rust-lld
installed, as discussed at the end of Step 1. Also un-comment the # runner = "gdb-multiarch -q -x openocd.gdb"
line; that will make cargo run
automatically open a debugging session in GDB. And at the end of the file, replace target = "thumbv7m-none-eabi"
with target = "thumbv6m-none-eabi"
.
Next, add the following dependency to your Cargo.toml
file:
[dependencies.stm32l0x1] path = "../stm32l0x1_pac" features = ["rt"] version = "0.0.1"
The path
value provides a local path to the PAC which you generated in Step 2; I put the test program in the same directory as the stm32l0x1
crate. And the "rt"
feature overrides the default ARM Cortex-M interrupt table with the one defined in the stm32l0x1
crate. Our simple test program won’t use peripheral interrupts, so the "rt"
feature doesn’t actually matter in this case, but that’s why it’s there. Next, delete these two lines from openocd.gdb
:
break DefaultHandler break HardFault
These chips only have two hardware breakpoints, and the default ‘run’ script sets four; GDB will give you warnings if you don’t get rid of the extras. You’ll also need to update openocd.cfg
– replace target/stm32f3x.cfg
with target/stm32l0.cfg
since we’re using an STM32L0
chip instead of an STM32F3
.
Finally, the last step before we get to the main program is to update the memory sections in memory.x
:
MEMORY { FLASH : ORIGIN = 0x08000000, LENGTH = 32K RAM : ORIGIN = 0x20000000, LENGTH = 8K }
Note that the default memory.x
file puts the Flash ORIGIN
label at 0x00000000
, and we want 0x08000000
. They look very similar. Before you move on, you can run cargo build
to make sure that the application builds properly with an empty main
method.
Step 4: Write a Blinking-LED Program
Finally, we can move on to writing code in src/main.rs
. You can replace the default contents with this simple minimal program, so we have a common starting point:
#![no_std] #![no_main] // Halt when the program panics. extern crate panic_halt; // Includes. use cortex_m_rt::entry; #[entry] fn main() -> ! { // Main loop. loop { } }
That should look about the same as the template, but with fewer comments. The #![no_std]
attribute is similar to the -nostdlib
flag from C, and #![no_main]
/ #[entry]
tell the program that it has a non-default entry point. The fn main() -> !
syntax indicates that main
should not return; !
is used instead of specifying a return type.
We can get a timed delay for our blinking LED by setting up the SysTick timer using the same syntax from the embedded Rust ebook:
// ... // Includes. use cortex_m::peripheral::syst::SystClkSource; use cortex_m_rt::entry; #[entry] fn main() -> ! { // Check out the 'Cortex-M Peripherals' singleton. let cm_p = cortex_m::Peripherals::take().unwrap(); // Set up the SysTick peripheral. let mut syst = cm_p.SYST; syst.set_clock_source( SystClkSource::Core ); // ~1s period; STM32L0 boots to a ~2.1MHz internal oscillator. syst.set_reload( 2_100_000 ); syst.enable_counter(); // ...
But unlike the SysTick timer, GPIO peripherals are not the same across all Cortex-M cores. To set those up, you’ll need to use the PAC that you generated in Step 2. You can refer to the ‘Embedded Rust’ ebook for the basic syntax, but since the ebook doesn’t cover this chip, it might be easier to just look at the auto-generated .rs
files to see how the names of registers and bits and things are laid out. They should look familiar from the bare-metal C examples:
// ... // Includes. use cortex_m::peripheral::syst::SystClkSource; use cortex_m_rt::entry; use stm32l0x1; #[entry] fn main() -> ! { // ... // Set up GPIO pin B3 as push-pull output. let p = stm32l0x1::Peripherals::take().unwrap(); let rcc = p.RCC; rcc.iopenr.write( |w| w.iopben().set_bit() ); let gpiob = p.GPIOB; unsafe { gpiob.moder.write( |w| w.mode3().bits( 0b01 ) ); } gpiob.otyper.write( |w| w.ot3().clear_bit() ); // Restart the SysTick counter. syst.clear_current(); // Main loop. loop { // Toggle the LED every SysTick tick. while !syst.has_wrapped() {}; gpiob.odr.write( |w| w.od3().set_bit() ); while !syst.has_wrapped() {}; gpiob.odr.write( |w| w.od3().clear_bit() ); } }
It’s interesting how the writes are performed with a closure-like syntax; you can also chain them together, like w.od3().clear_bit().od4().set_bit()
. But you can see that writing multiple bits at once requires an unsafe
block (highlighted above), because it performs a read/modify/write sequence and the register could change in between the ‘read’ and ‘write’ steps.
Usually those sorts of operations would happen inside of a HAL crate, or be abstracted into another method. The stm32f30x
crate, for example, lets you call w.moder3().output()
without using an unsafe
block. The operation is still technically ‘unsafe’, but there’s not much risk because it is rare for any one peripheral to be configured from two different code sections without being gated behind something like a mutex or semaphore. Plus, I’m still not clear on how Rust’s concepts of ownership and borrowing apply to values like rcc
and gpiob
in the above example.
Anyways, the rest of the program is pretty simple. It resets the SysTick counter after the LED pin is initialized, then toggles the pin each time that the counter ticks over in an infinite loop.
Step 5: Build, Flash, Run
Once you’ve written the program, you can build it with cargo build
. It’s sort of like running make
, but instead of a Makefile
you have Cargo.toml
and an optional .cargo/
configuration directory. There’s more to it than that, but you can find more information in the Cargo Book.
The cortex-m-quickstart
template does a good job of setting up a smooth workflow, so uploading and debugging your program should be easy. Run openocd
from the project directory, followed by cargo run
. That should run the commands listed in the openocd.gdb
file, which will upload your program and set a breakpoint at main. Enter continue
a couple of times, and the LED on your board should start blinking.
If you run into problems, double-check that you updated all of the files mentioned in Step 3. The GitHub repository also has finished examples of those configuration files.
Conclusions
This was surprisingly easy and fun! I’ve been skeptical about how tightly-coupled Cargo and Rust seem to be, but the built-in dependency management and the possibility of adding things like an embedded HAL API to the language are definitely appealing. Plus, it seems like a good way to manage modular boilerplate code that you want to use in a lot of different projects. But there was also definitely some tedium and frustration involved in figuring out the right configurations and syntax to make Cargo happy, and NPM has shown us in stark terms that centralized library repositories can easily go bad.
I’m also not sure how the performance works out – are all of those chained a().set_bit().b().clear_bit()...
operations collected into one operation by the compiler? I have to assume they are, but I’m not completely sure and these are still early years for embedded Rust.
One thing that I really like about Rust and Cargo is that they encourage using MIT and Apache licenses, and the existence of an online repository makes it possible to distribute and manage libraries for embedded devices similar to the library managers used by Arduino / Keil / PlatformIO / etc. Having a package manager tightly-coupled to a language can bring a whole host of gnarly problems, but you can always configure Cargo to use local offline files, and I’ll welcome anything that might help to break the stranglehold of expensive and proprietary vendor lock-in that suffocates most microcontrollers and shuts small-time developers out of the market.
Also, Rust has a habit of suffixing its documentation titles with ‘-nomicon’ (‘book of …’), and obviously I’m a fan of that. Here are a couple of other documents which I haven’t had a chance to read, but which are probably very relevant to this post:
- The Discovery Book walks through some examples using the ~$15 STM32F3 Discovery Kit as target hardware. It looks like a much better resource than this post, but I didn’t know about it until I had already written everything up. It looks like it probably would have saved me a lot of time, but that’s the learning process for you.
- The Embedonomicon looks like it covers some of the concepts presented in the ‘Embedded Rust’ ebook in more detail.
Greg Woods
March 4, 2020 at 2:22 pm
Great article. Building my own PAC from the SVD filled in a load of missing knowledge which had me really stumped trying to figure out the simplest things. By looking at the generated pac, as well as comparing the stm32f0 and stmf1 reference manuals, has helped me port it to the “blue pill”
Vivonomicon
March 16, 2020 at 1:53 pm
Thanks, I’m glad you found it helpful!
Greg Woods
March 4, 2020 at 2:39 pm
I may have spotted an error.
Line 17 of main.rs should, I think be 0b01 (2 bits) instead of 0x01 (a byte)
I assume that using 0x01 will clear ALL other bits of the register, which was not intended. I may not have caused a problem for this simple example on the discovery board, but when ported to the blue pill it somehow disables the LED output
Vivonomicon
March 16, 2020 at 1:52 pm
Ah, thank you – good point. The `0x01` value does represent 8 bits, but in this case, the code generated by `svd2rust` performs a `value & MASK` operation in the `.bits(…)` function, so only the bits within the `mode3` field should actually be affected by the function call.
At least, I hope so…I could be reading it wrong, but you can take a look in the generated code:
https://github.com/WRansohoff/svd2rust_example_stm32l0x1/blob/4c91c6d082b20acd919f973eecad83349720d74d/svd_lib/stm32l0x1/src/gpiob/moder.rs#L408
You’re right, though; that is confusing and it’s better to be precise with bitfield definitions. Thanks.
MURLIDHAR ROY
July 12, 2020 at 9:01 am
I am working with Nucleo L053R8. I made all the changes for STM32l0x3 in cargo.toml,cargo and in code also. My code is given below:-
#![no_std]
#![no_main]
// Halt when the program panics.
extern crate panic_halt;
// Includes.
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::entry;
use stm32l0x3;
#[entry]
fn main() -> ! {
// Set up SysTick peripheral.
let cm_p = cortex_m::Peripherals::take().unwrap();
let mut syst = cm_p.SYST;
syst.set_clock_source( SystClkSource::Core );
// ~1s period; STM32L0 boots to a ~2.1MHz internal oscillator.
syst.set_reload( 2_100_000 );
syst.enable_counter();
// Set up GPIO pin B3 as push-pull output.
let p = stm32l0x3::Peripherals::take().unwrap();
let rcc = p.RCC;
rcc.iopenr.write( |w| w.iopben().set_bit() );
let gpioa = p.GPIOA;
unsafe { gpioa.moder.write( |w| w.mode5().bits( 0b01 ) ); }
gpioa.otyper.write( |w| w.ot5().clear_bit() );
// Restart the SysTick counter.
syst.clear_current();
// Main loop.
loop {
// Toggle the LED every SysTick tick.
while !syst.has_wrapped() {};
gpioa.odr.write( |w| w.od5().set_bit() );
while !syst.has_wrapped() {};
gpioa.odr.write( |w| w.od5().clear_bit() );
}
}
While “cargo run” the Nuclep board does not blinking the LED13. Please suggest to work the above code.
Vivonomicon
July 26, 2020 at 1:06 pm
It looks like you are toggling pin A5, but you enable the GPIOB peripheral instead of GPIOA. Does it work if you replace this line:
rcc.iopenr.write( |w| w.iopben().set_bit() );
With this?
rcc.iopenr.write( |w| w.iopaen().set_bit() );