Hello, Rust: Blinking LEDs in a New Language

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.

It’s kind of fun when languages have mascots, especially when they’re CC0-licensed.

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.

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:

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

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:

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:

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".

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:

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:

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.

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:

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.

Comments (4):

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

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:

Related posts:

Across the globe, people seem to enjoy decorating their homes, communities, and outdoor spaces with lights and ornaments during the winter holidays. Maybe it helps with the depressingly early sunsets for those of us who don’t live near the equator. Anyways, I thought it’d be fun to make some ornaments with multi-color addressable LEDs last year, and I figured I’d write about what worked and what didn’t.

I didn’t have many microcontrollers at the time because I was visiting family for the holidays, so I ended up coding the lighting patterns for a cheap little STM32F103 “black pill” board which was in the bottom of my backpack. And it’s a convenient coincidence that I just started learning about the very similar GD32VF103 chips with their fancy RISC-V CPUs and nearly-identical peripheral layout, so this also seems like a good opportunity to write about how to cross-compile the same code for two different CPU architectures.

Pretty holiday stars! “Frosted white” acrylic sheets aren’t the best way to diffuse light, but they are cheap and easy to work with.

This was a fun and festive project, and it might not be a bad way to introduce people to embedded development since there are so many ways to drive these ubiquitous “NeoPixel” LEDs. Sorry that this post is a little bit late for the winter holidays – I’ve been traveling for the past few months – but maybe it’ll get you thinking about next year 🙂

I’ll talk about how I assembled the stars and what I might do differently next time, then I’ll review how to light them up with an STM32F103, and how to adapt that code for a GD32VF103. But you could also use a MicroPython or Arduino board to set the LED colors if you don’t want to muck around with peripheral registers.

I’ve written a little bit in the past about how to design a basic STM32 breakout board, and how to write simple software that runs on these kinds of microcontrollers. But let’s be honest: there’s still a bit of a gap between creating a small breakout board to blink an LED, and building hardware / software for a ‘real-world’ application. Personally, I would still want a couple of more experienced engineers to double-check any designs that I wanted to be reliable enough for other people to use, but building more complex applications is a great way to help yourself learn.

So in this post, I’m going to walk through the process of designing a small ‘gameboy’-style handheld with a GPS receiver and microSD card slot, for exploring the outdoors instead of video games. Don’t get me wrong, you could still write games to run on this if you wanted to, and that would be fun, but everyone and their dog has made a Cortex-M-based handheld game console by now; there are plenty of better guides for that, and many of those authors put a lot more time into their designs and firmware than I ever did.

Assembled GPS Doohicky. I left too much room between the ribbon connector footprint and the edge of the board on this first revision, so the display couldn’t fold over quite right. Oh well, you live and learn.

The board design isn’t too complicated, but there are several different parts and it gets easier to make small-but-important mistakes as a design gets larger. It mostly uses peripherals that I’ve talked about previously, but there are a couple of new ones too. The display will be driven over SPI, the speaker uses a DAC, the GPS receiver talks over UART, the battery and light levels will be read using an ADC, and the buttons will be listened to using interrupts. But I haven’t written about the USB or SD card (“MMC”) peripherals, and those will need to go in a future post since I haven’t actually worked them out myself yet. Note that SD cards can technically use either SPI or SD/MMC to communicate, but the microcontroller that I picked has a dedicated SD/MMC peripheral, and I wanted to learn about it.

Several years ago, a company called Future Technology Devices International (FTDI) sold what may have been the most popular USB / Serial converter on the market at the time, called the FT232R. But this post is not about the FT232R, because that chip is now known for its sordid history. Year after year, FTDI enjoyed their successful chip’s market position – some would say that they rested too long on their laurels without innovating or reducing prices. Eventually, small microcontrollers advanced to the point where it was possible to program a cheap MCU to identify itself as an FT232R chip and do the same work, so a number of manufacturers with questionable ethics did just that. FTDI took issue with the blatant counterfeiting, but they were unable to resolve their dispute through the legal system to their satisfaction, possibly because most of the counterfeiters were overseas and difficult to definitively trace down. Eventually, they had the bright idea of publishing a driver update which caused the counterfeit chips to stop working when they were plugged into a machine with the newest drivers.

FTDI may have technically been within their rights to do that, but it turned out to be a mistake as far as the market was concerned – as a business case study, this shows why you should not target your customers in retaliation for the actions of a 3rd party. Not many of FTDI’s customers were aware that they had counterfeit chips in their supply lines – many companies don’t even do their own purchasing of individual components – so companies around the world started to get unexpected angry calls from customers whose toy/media device/etc mysteriously stopped working after being plugged into a Windows machine. You might say that this (and the ensuing returns) left a bad taste in their mouths, so while FTDI has since recanted, a large vacuum opened up in the USB / Serial converter market almost overnight.

Okay, that might be a bit of a dramatized and biased take, but I don’t like it when companies abuse their market positions. Chips like the CH340 and CH330 were already entering the low end of the market with ultra-affordable and easy-to-assemble solutions, but I haven’t seen them much outside of Chinese boards, possibly due to a lack of multilingual documentation or availability from Western distributors. So at least in the US, the most popular successor to the FT232R seems to have been Silicon Labs’ CP2102N.

It’s nice to have a cheap-and-cheerful way to put a USB plug which speaks UART onto your microcontroller boards, so in this post, I’ll review how to make a simple USB / UART converter using the CP2102N. The chip comes in 20-, 24-, and 28-pin variants – I’ll use the 24-pin one because it’s smaller than the 28-pin one and the 20-pin one looks like it has some weird corner pads that might be hard to solder. We’ll end up with a simple, small board that you can plug into a USB port to talk UART:

Drivers for the CP2102N are included in most popular OSes these days, including Linux distributions, so it’s mostly plug-and-play.

It’s worth noting that you can buy minimal CP2102N boards from AliExpress or TaoBao for about $1, but where’s the fun in that?