Rust Embedded Frameworks, Hardware, and Setup

Murray Todd Williams
13 min readJun 5, 2024

--

In the series part 2, we disambiguate some of the Crates, starting hardware, and dev environment configuration.

This is the second article in a series where I talk through the adventure of learning Rust via some Embedded (Raspberry Pi Pico) hardware. For some more background, you might want to read the previous article.

For a list of all articles in this series, click the card below:

Murray's (Raspberry Pi Pico W) Embedded Rust Series

5 stories

As with most languages (Node.js, Java, Python, etc.) a lot of the practical value comes from the vast ecosystem of external libraries. As I started diving into Rust, I quickly realized that a significant learning curve comes from understanding the various dependencies I need to add to my Cargo.toml file. Let me start out doing a “bad news / good news” summary.

The bad news is that it’s going to take a while to get a firm grasp of the various layers of hardware abstractions and which crate enables which libraries you need to import with your use statements. The good news is that we are at an exciting moment as the embedded-hal crate hit its version 1.0.0 release on January 9th, 2024 after being under development for four years! On the heels of that, on January 22nd the Embassy project released its crates for the major microcontroller families—Nordic nRF, STM32, and the RP2040 that I’ll be focusing on in my series.

Cool. Yea. Whatever. Why should I care? What does this really mean?

It means that we’ve hit a long anticipated moment when the standards have been able to gel so that there’s a singular set of standards for hardware developers to create drivers for the various microcontroller boards and peripherals instead of having everything be ad hoc.

Let me give a single example. The first peripheral I tried to play around with was a barometric pressure BMP180 sensor. I looked around for any existing Rust crates and found a couple of libraries, one written 7 years ago with no documentation or GitHub link and another written 6 years ago, that dependent on the i2cdev library that is intended as a Linux OS i2c driver. (I assume that’s for a fuller Raspberry Pi board since you need something like that to get the full Linux OS.)

Funny enough, as I was figuring all this stuff out a couple weeks ago, I thought I would try to write a BMP180 driver as one of my upcoming personal projects. Writing this article I see two people beat me to the punch, one published 11 days ago (as of my writing) and another one 3 days ago. So you see, this final release of the 1.0.0 version of embedded-hal is a BFD.

Understanding Embedded-HAL Abstraction Layers

Okay, let’s backtrack and look at what a HAL is, as well as the entire abstraction structure ecosystem.

Embedded abstraction layers (diagram derived from the Embedded Rust Book)

I find the “embedded” microcontroller world to be really interesting, reminiscent of the early PC days where all computers were based on the same basic 8088, 80286, 80386, etc. microprocessor, but you could drop all sorts of graphics or networking or sound cards inside or your could add printers or scanners to the serial ports. Everything worked assuming you had the right drivers and those drivers could work against some internal standards.

All of the modern microcontrollers are based on some lineage of the ARM Cortex family. The RP2040 (the heart of our Raspberry Pi Pico) is specifically a dual-core 32-bit ARM Cortex-M0+ microcontroller, which is streamlined to be able to be fit on a tiny die for a very low price. In comparison, the Nordic nRF chips are based on ARM Cortex-M4 or M33 architectures and STM-M33 is ARM Cortex-M33 based. Their HAL (hardware abstraction layer) crates all depend on the cortex-m and cortex-m-rt crates internally. They also provide the implementations for the PAC (peripheral access crate) standard so that the authors of device driver—like that BMP180 barometer sensor I was mentioning earlier—can write a driver that depends only on portions of theembedded-hal family and simply assume a general HAL create, like rp2040-hal, or a more specific board crate, like rp-pico, will pass along a usable i2c bus reference.

In truth, you don’t really need to understand how all this works. If your specific board has a crate, like rp-pico for the Raspberry Pi Pico or Pico W, you should just use that crate. It will automatically include rp2040-hal to address the components (i2c, uart, spi, and of course the multi-purpose GPIOs) that are built into the RP2040 microcontroller. On top of that, it will include built-in definitions of the specific pin-outs that the Raspberry Pi Pico builds out from the RP2040, i.e.

Raspberry Pi Pico Pinout Diagram

If on the other hand I had a SparkFun Pro Micro RP2040 (which I do), I would use the sparkfun-pro-micro-rp2040 crate which would support the core features of the same RP2040 but the pin-outs would expose this more compressed feature-set:

One final note about the embedded-hal project. For five years there has been a decently stable 0.2.x series that people have been using. Four years ago work had started on the 1.0.0 series that we finally have out today. Interestingly, into the 0.2 series the developers realized they had over-abstracted the problem by trying to create a single unified HAL layer. Trial and error led to finding the right balance to enable them to “use any driver together with any HAL crate, out of the box, and across the entire Rust Embedded ecosystem”. The details can be found in the 0.2-to-1.0 migration guide (here’s a link to the specific section) which I think is worth the read for some more context.

Doing Async Development with Embassy

When I start working on my first project examples, I’m actually not going to be using rp-pico directly because I’m developing against Project Embassy instead. To be specific, we’ll go through the simplest “hello world” application—blinking a light—against rp-pico, but then we’re going to rewrite it with Embassy.

Let me explain why Embassy is so awesome. It’s all about async.

When people first write programs to learn the fundamentals of electronics, they do simple tasks—blinking a light, reading the voltage on a pin, pulling a reading off a sensor. These are the things I did with my first Arduino with simple C programs, and it seemed perfectly okay to write a basic synchronous/blocking program. But the moment you want to do anything meaningful, you’ll find yourself putting a whole bunch of complex logic in you main execution loop.

The amazing little RP2040 chip is stuffed with a whole bunch of stuff that’s designed, like a modern operating system, to do things simultaneously.

  • You’ve got GPIO pins that can be configured to react when a voltage changes. (E.g. a button being pushed)
  • You’ve got communication busses (i2c, spi, uart) that are communicating with sensors.
  • You’ve got clocks that can get set to trigger a variety of activities at certain times.
  • There’s a really amazing set of PIO state machines that can be simulating complex communications protocols.
  • You might have a networking device (like the CYW43 that is built into the Pico W) that is sending out and/or receiving instructions and telemetry.

All of these things may be happening at the same time. A lot of them are potentially blocking operations. If you think about a moderately advanced IoT project, your microcontroller—and the application you write for it—is going to be orchestrating and coordinating a number of tasks simultaneously. It’s almost writing a complete OS that is managing all sorts of processes and resources.

Here’s another motivation for embracing an async executor like Embassy: your RP2040 microcontroller has two cores! Young readers might stifle a yawn here, but I still remember in the 90’s when Digital’s Alpha processor (a slightly obscure but still super-cool PC alternative to the new Intel Pentiums of the time) offered the first foray into both 64-bit computing and dual-processor parallel computing!

My point is: if you have multiple cores, why write single-threaded programs that can only use half of your computing power? You might think that it’s not all that hard to write something where you spawn a second thread with some specific tasks and synchronize on the results, but even that takes a lot of planning and coordination.

The better thing to do is embrace a completely different paradigm. Embrace async and let it change the way that your brain models problems. After all, we’re embarked on the Rust learning journey in order to stretch our brains and become better developers, right?

Preparing your shopping list

It’s time to shift gears. If you want to follow along with my examples, it’s time to go shopping! I’m going to keep the hardware minimal. I am going to provide some links to my favorite online digital stores: Adafruit. It’s founder, MIT engineer Limor “Ladyada” Fried, was an inspiration to a lot of young people who wanted to get into electrical engineering. Her store is a great place to order a whole bunch of tiny electrical components at a time, and they have done a lot of cool engineering of easy-to-use sensor boards. Another alternative that is worth exploring (and buying some products from) is SparkFun. And there are others. I guess what I’m trying to impress is that there is a great ecosystem of sellers and producers of electrical equipment and gadgets, and its worth exploring and frequenting them instead of just grabbing generic components from Amazon.

Alright! That said, let’s get started!

Raspberry Pi Pico W and either Raspberry Pi Pico (with headers) or Raspberry Pi Debug Probe

This is going to sound strange, but you are going to want two RP2040-based Raspberry Pi boards. Believe me, it sounded weird to me too, but for the extra $5 or $12, you’re going to want to do this, and in fact you won’t be able to make things work without both.

Raspberry Pi W (the big silver square is the Wifi chip)

The interesting thing about the RP2040 is that there are three special wires that go into the chip that enable it to be programmed and debugged. Yes, you get a hardware-enabled mechanism to stop the chip, forward incrementally on instructions, read the values of your variables, all from the comfort of your Visual Studio Code IDE! Yowza!

Up until about a year ago (Feb 2023) the way you would setup your development environment was to have one dedicated Raspberry Pi Pico act as your “debug controller”.

My setup, wired together using headers to do debug connection

To make things work, you would flash your debug controller with a special application (firmware) called picoprobe.uf2, plug it into your laptop via USB, and wire it up to some special headers in the middle of the board you’re developing on.

As of February 2023, Raspberry Pi Ltd made things easier by offering a smaller, more compact “Debug Probe” dedicated to this special purpose. Instead of buying a $5 Pico and pressing it into service, you can buy the $12 Debug Probe—it comes with some nice packaging and useful cables!—and make your life a bit easier.

So just to be clear, you want to get at least one Raspberry Pi Pico W plus either a regular Raspberry Pi Pico (no wifi needed if it’s just acting as your debug controller) or the Raspberry Pi Debug Probe. Personally, I think the Debug Probe will be worth getting because otherwise you’re wasting a lot of space on your breadboard if you put both boards on it like I did in the picture above.

Headers or No Headers?

As you are doing your ordering of electrical component shopping, you’ll learn to keep an eye on whether it comes with headers pre-soldered or if you want to solder (and often supply) the headers yourself.

Pico with Header on left. Pico without Headers on right.

If you get a Pico with the headers (Pico H and Pico W H) pre-soldered (as I’m suggesting) then your debug/bootload connection will be attached via a JST socket, as shown on the left arrow above. If this is the case, you will want to get this JST SH Compatible 3-Pin to Premium Male Headers Cable to plug into it.

In contrast, if you don’t get the Pico boards with headers (Pico and Pico W respectively, no “H” in the name) then you will be soldering headers onto the sides (pointing down into your breadboard) as well as a small 3-pin header on the top to do the debugging connection. If that’s the case, you don’t need a JST socket 3-Pin connector. You’ll just be using your own connection of jumper wires. A picture of my own setup is shown here.

Don’t worry too much about the wiring: it’s not as complicated as it looks.

Breadboard and Jumpers

As you saw in the above picture, you’ll use a breadboard to connect your projects together. The breadboard is a super-cool invention that makes it fast and easy to wire components together by putting jumpers (insulated wires that are bare just on the ends) in certain holes. As you could see in the above picture, my two Picos take up about 2/3 of a full-length breadboard, so it might be a good idea to go for that debug probe I’d mentioned earlier. (It won’t take up any rows.)

A full-sized breadboard

You’ll also need some jumpers. You might consider something like this Half Size Breadboard + a 78 piece jumper set, or just get a few cheap sets of jumpers like what’s on this page. (I have some male/male and male/female jumpers.)

Optional: Raspberry Pi Zero 2 W

As I mentioned in my first article in this series, I love having a full Raspberry Pi running in my home as a makeshift server. In this series, I will use a Raspberry Pi Zero 2 W (just $15, not including the case and miniSD card) as a server that my embedded project will send data to.

My Raspberry Pi Zero 2 W with case and color-coded headers

I might write an article in the future about the traditional Raspberry Pi, but working with these is more about installing a Linux operating system than anything related to Rust or embedded microcontrollers, so I’m going to say this is out of scope of this blog series.

In the beginning, I’m going to show some simple (<20 line) Python scripts that can act as a makeshift server that our projects will send telemetry to via the Pico W’s build-in wifi. The point is that, for the sake of learning, you should be able to run this just as easily on your laptop as on a Linux server. Python isn’t my favorite programming language by any stretch, but it simplicity and ubiquity make it suit this purpose. (I will probably try writing a tutorial in the future where I write the server application(s) in Rust.)

LEDs and Resistors

As I’ve mentioned earlier, the embedded version of “hello world” involves blinking an LED light on and off. To do this, you need an LED and a resistor. Now, these components are so cheap and basic that you’re not going to find anyone selling one-offs. My recommendation is to either buy a basic electronics starter kit like this one by Adafruit that can some LEDs and resistors and diodes and transistors, etc. or to buy a mixed pack of LEDs and a set of resistors. For the resistors, you could buy a simple 25 pack of 220 ohm resistors which are a good general range for LEDs and costs all of 75¢ or if you want to satisfy your resistor needs for the foreseeable future, SparkFun has this cool kit that has resistors of ever imaginable spec.

Sensors

Arguably the point of a microcontroller is to interact with the world via electronic devices. We get inputs in the form of sensors and our outputs can be in the form of things that make light or sound or move things via servos. After we do our simple LED example, I’m going to move to two sensor examples, one that gets digital sensor information via the I2C bus, and another that uses an analog pin input to read a value in terms of voltage.

My first Rust I2C project worked with an old BMP180 barometric pressure sensor that is not made anymore, but I’ve found an upgraded BMP390 that has a nice feature of having a plug that is compatible with SparkFun Qwiic I2C connectors. What this means is that you can (and should) get this Qwiic cable to plug the sensor into your breadboard if you aren’t ready to soldier headers onto the board yourself.

Two digital sensors, the Qwiic cable, and an analog TMP36

Another popular sensor is the MCP9808 temperature sensor, so in my blog on sensors, I’ll show code for using both of these. If you want, you can pick one or the other or both. If you get both sensors with the Qwiic connectors, you should get this cable to conveniently daisy-chain them together!

For the analog project, we will use a simple TMP36 sensor that spits out temperature readings in terms of a simple voltage output.

And that’s pretty much it! The only other shopping item you’ll want to consider is getting is a microUSB cable to plug into your Picos if you don’t already have some lying around. (If you get the Pico Debug Control kit, it’ll come with a cable.)

In the next “part 3” article in the series, I’ll do a little tour of some of the terms and concepts (GPIO, I2C, SPI, etc.) that you’ll encounter if you’re a relative newbie to the embedded electronics space. I also provide some quick instructions for setting up your development environment. (It gives you something to do in case you just placed your electronics order.)

--

--

Murray Todd Williams

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.