Rust Dev Setup & Embedded Terminology

Murray Todd Williams
13 min readJun 5, 2024

--

Series part 3 provides a couple more diversions before we start our first project

Ferris the Crab in the labyrinth of Embedded terms

This is the third part in my blog series about Rust development on a RP2040 microcontroller.

  • In the first article, I provided some motivation behind my own adventure of using embedded development as my first foray into learning Rust—an arguably ambitious goal.
  • In the second article, I discussed some of the important projects and crate libraries that I had to understand before diving into my first successful Rust project, followed by a rundown of the various boards, cables, sensors and other components that I will be using in my examples. (Fortunately, all these elements are rather inexpensive, most in the $5–10 range.)

By the way, for a list of all the articles in the series, click on the card below:

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

6 stories

I feel like there are two things that should be covered before we dive into our first application:

  1. First of all, I’m not assuming readers are experts in embedded development. There are some microcontroller terms and concepts like “GPIO and ADC” that I’ll be talking about in this series that a typical software development might not have encountered before. I think a lot of these are kind of cool and fun, so I thought I would start with these before getting into the more tedious stuff.
  2. Second, it’s time to start setting up your dev environment. Rather than trying to recreate all the useful tutorials out there, I’m going to point out where to go so you can make sure all the tools and libraries are installed so we are ready to dive into our first “blinky” application.

To put it another way: if you just finished my second article and ordered a bunch of components from a retailer like Adafruit or SparkFun, I wanted to give you something to read (and do) while waiting for your new toys to arrive!

A quick primer about your RP2040 Microcontroller

For most of my 40+ years of programming, the center of my universe was a CPU, starting with simple microprocessors—like the 16-bit Texas Instruments TMS9900 in my first TI-99/4A home computer or the little 8-bit 6502 in the Apple II computers that were popular at the time—and eventually evolving into the more powerful cores like the Motorola 68000 series in the Macintosh line or the Intel x86 family and now the ARM chips that Apple bases its computers on.

These CPUs run software that essentially interacts exclusively with our digital world: storage media, networking, user interfaces, etc. They do a lot of computation and IO processing.

Microcontrollers are very similar to CPUs, except that a lot of their focus is on the physical world. There’s still a lot of important IO, pulling data from sensors and driving output to special-purpose LCD displays, but they also have the ability to customize how electrical current is applied to or detected from certain pins. They are the maestros at the center of your broader electronics projects.

Early microcontrollers seemed relatively primitive compared with the CPUs (microprocessors) of the same time, since they had so much focus on controlling electronics and their price point had to be a lot lower. Think about it: it wasn’t historically unusual for a high-end CPU in a computer to cost hundreds of dollars, while the microcontroller inside a typical electrical toy had to be kept in the $10–20 range! The specs of the 8-bit ATmega328 microcontroller in my very first Arduino kit were downright quaint with a 20 MHz chip speed, only 32 KB of flash memory, and only 2 KB of RAM!

The RP2040 is a whole new animal, bleeding the lines between these two families: it has two processing cores and direct memory access (DMA) for increased performance, as well as the foundational bits. As I mentioned in my first article, in January 2021, the Raspberry Pi Ltd company announced that they were designing their first microcontroller. Before that, they had been known for their series of credit-card sized SBCs (single-board computers) that had been designed to promote the teaching of computer science in schools and developing countries.

The chip is pretty inventive, with a cool balance between processing power, low power consumption, and low cost. If you’re interested in diving deeper into the chip, the RP2040 Datasheet is worth skimming.

Anyway, let’s do a quick spin around the chip.

GPIO (General Purpose Input/Output) Pins

The RP2040 has 36 wires (pins) that it uses to interact with the outside world. Some of these pins are used to execute code from an external flash device, leaving 30 pins (GPIO00 through GPIO29) for you to configure and use for a variety of purposes.

This pinout diagram for the Pico W shows all of the board’s pins and which ones are hooked to the central RP2040’s GPIO pins. As the diagram shows, certain pins can be configured to support certain IO interfaces like I²C, SPI and UART. (More on those in a bit.)

They can also be used “standalone” to either drive output voltage (either “off” or “on” at the system board’s 3.3 volt operating voltage) or to detect or “read” whether a voltage is being applied—i.e. detecting whether a switch is on or a button is being pressed. When we develop our first “blinky” program, we will be using a GPIO pin to drive voltage across an LED to make it turn on and off.

Note there are some special purpose GPIO pins that you might come across. Many boards, like the Raspberry Pico (but not the Pico W!) have a tiny onboard LED that is hard-wired to one of the internal GPIO pins—GPIO25. On the Pico W, some pins are reserved for communicating with the onboard CYW43 wifi chip.

ADC (Analog Digital Converter)

There’s a lot you can do with the 3.3 volt “on/off” nature of the GPIO pins, but sometimes you need to read or write (detect or generate) a variable voltage. This variable voltage problem isn’t trivial to do! If you want to read variable voltages, such as the signal from a microphone, you’ll need to turn to three special-purpose ADC pins (GPIO26–GPIO28) for the task.

In part 5 in this blog series, we read temperatures off of a TMP36 sensor to demonstrate the application. (There’s also an internal temperature sensor in the RP2040 that can be read a similar way.)

PWM (Pulse Width Modulation)

I was first introduced to PWM when I first wanted to try dimming an LED. I had originally thought that I could just raise or lower the voltage to get a brighter or dimmer light, but it turns out things often don’t work that way.

If you have LED lights on a dimmer in your home, that dimmer is actually turning the power off and on extremely rapidly. 50% brightness comes from the light being on for 50% of the time and off for the alternating intervals. If you set it to 25% brightness, you are having the LED power off for time intervals that are 3 times longer than the on intervals.

RPWM examples, taken from the PWM Wikipedia page

Getting these cycles precisely timed would be a full-time job for a processor, so the RP2040 has a special-purpose PWM block designed to help with the tough task of simulating variable voltages with these rapid off/on switching cycles. The simple dimming duty cycles used for dimming an LED is just a super simple example of what PWM can do. It is really designed to help do the variable-voltage output equivalent of what the ADC does for input. For example, sending MP3 audio to a speaker.

Communication Protocols (I²C, SPI, UART)

Over the years, a few different protocols have emerged for digital devices to communicate with each other. Depending on your application or specific hardware, you may need to pick the right protocol:

  • UART: The oldest communication protocol, this used to be called “serial communication” and in the old PC and pre-PC days, computers would sport a “serial port” that you could use to attach things like modems. In order to communicate via UART, you just need two wires: a “transmit” wire and a “receive” wire. (Written TX and RX in pinout diagrams.) One of the simplest ways to make a project write outputs is to have a console output where you send debugging messages, and you use your laptop to read and output that log.
  • SPI (Serial Peripheral Interface): Introduced in early 1980s, this became the de facto standard for synchronous short-distance communications between devices, especially in the embedded realm. When devices are communicating with flash memory or SD cards, sensors, real-time clocks, ethernet or USB, etc., SPI is usually at the heart of things. It’s that ubiquitous! That said, there are what I consider a few drawbacks. First, each device you want to communicate with will require its own SPI interface, and second, each SPI connection requires four wires: CS, SCLK, MOSI (TX) and MISO (RX). As you can imagine, a project with a lot of devices could eat up your GPIO pins quickly!
  • I²C (Inter-Integrated Circuit): Where SPI can be frustrating (number of wires, one bus per device) I²C thrives. It requires only two wires (SDL and SCL) and allows many devices to be chained together! There are some limitations: each I²C device must have a 7-bit unique device ID, so there’s a chance two devices out there will have conflict with each other. (The MCP9808 temperature sensor has three added connection wires solely to allow you to customize its ID!) The ability to use a single I²C bus for multiple accessories inspired SparkFun to create their “Qwiic” connect system and line of products for easy prototyping. One last note about I²C: it’s much slower compared with SPI. For most applications where you’re sending or receiving at most a few dozen byte of data, it’s not going to matter.

The RP2040 can also use the USB bus for applications. I tend to forget that because I think of USB as the way I power and program my Pico, rather than the way a running application might work with other devices. When I was looking around the Rust Embedded embedded-hal libraries, I couldn’t find any evidence of USB examples or overall support, but Embassy (the newly released async API) appears to have some rich support for a variety of classes of USB devices. (If you look at the Embassy RP2040 examples here, there are programs for USB Ethernet, USB Mouse and Keyboard, USB MIDI, and the more basic USB Serial.)

IRQ / Interrupts

Interrupts are another one of those things that make me smile because it’s one of those things that remind me of the old PC days that you just don’t think about when you write an application that’s running on an advanced operating system like Linux or Windows or Mac OS.

An interrupt is a way to tell the microcontroller to “interrupt” whatever program it was running, briefly do some specific thing (i.e. run a small snippet of code), and then usually resume where you left off. They can also be configured to do things like put the processor to sleep (low power mode) and then wake up when voltage is applied to a pin or when a timer on the clock goes off.

A lot of the async functionality in the Embassy library will abstract things so you don’t have to juggle processes with interrupts (much like how advanced OS’s abstract away the details), but it’s worthwhile to understand some of the basics.

DMA (Direct Memory Access)

Applications can involve a lot of IO—data being stored on a drive, sensor telemetry being read, buffers of data being sent or received via the network, and IO can be pretty darned slow compared to the processing capacity of the CPU. If your processor focuses on doing all this IO, you can be slowing things down dramatically as th processor waits for every byte to come and go.

DMA is a mechanism by which a peripheral can access data directly from (or write data directly to) RAM rather than asking to be fed byte-by-byte from the CPU. The peripheral works with the DMA controller to manage the data interchange, and the DMA controller will raise an interrupt (ta da!) when it’s done.

Once again, the Embassy async framework will help us to take advantage of the RP2040’s DMA capabilities while protecting us from the nitty gritties. From the Embassy website:

…because DMA is more complex to set-up, it is less widely used in the embedded community. Embassy aims to change that by making DMA the first choice rather than the last. Using Embassy, there’s no additional tuning required once I/O rates increase because your application is already set-up to handle them.

PIO (Programmable Input/Output)

One the most interesting and versatile aspects of the RP2040 are the PIO blocks. The microcontroller has essentially eight “state machines” which act a little bit like super-simple CPUs. They can run their own custom-written “programs” that use an extremely pared-down set of only nine machine instructions. These PIO state machines can be configured to work with GPIO pins and the DMA controller to perform special purpose tasks.

I first read about the PIO feature when reading about this product on the Adafruit website (a custom RP2040 board) where it enabled potentially thousands of NeoPixel lights to be driven in realtime applications. Your application could manage the “image data” that you wanted to drive across these LED strings, and the PIO system would “just make it so” in terms of constantly managing the communications with the strings without bogging down the CPU.

From a Raspberry Pi blog:

PIO allows you to create additional hardware interfaces, or even new types of interface. If you’ve ever looked at the peripherals on a microcontroller and thought “I need four UARTs and I only have two,” or “I’d like to output DVI video,” or even “I need to communicate with this accursed serial device I found, but there is no hardware support anywhere,” then you will have fun with PIO.

Writing PIO programs is definitely a master-wizard sort of task and goes beyond what we’re going to do as Embedded Rust beginners, but it’s not unreasonable to find pre-written PIO code and use it for special purposes. And in fact, we’re going to do that on our second blinky project because the Pico W’s onboard LED isn’t easily accessible via GPIO25 like it is on the non-wifi Pico board. It is actually driven by the CYW43 wifi chip. Strangely, we don’t use the onboard SPI interfaces to talk to the CYW43 (I guess they didn’t want to cannibalize one of precious dedicated SPI channels just to drive wireless networking!) but instead we requisition the PIO system and use it to simulate an SPI channel through which we talk to the wifi chip and tell it to turn the LED on and off.

If that strikes you as contorted, yes, I agree. But we’re going to do it anyway because you just can’t NOT do a blinky example. (And truthfully, I use the onboard LED a lot as a way to know what my program is doing.)

Okay, that’s all the tour I’m going to give now. There are so many additional tidbits in the RP2040, but it would be unrealistic to try to cover them all. I would definitely say it’s worth periodically skimming the RP2040 Datasheet—all 644 pages of it!—in order to keep an eye on all the obscure features you migth someday want to play with. (Like using the ROSC ring to generate random numbers!)

Setting up your dev environment

In order to get my development environment up and working, I depended on a three-part series “Getting Started with Rust on a Raspberry Pi Pico” of blog articles by Jim Hodapp in Substack’s The Rational Technologist series. Here are links to Part 1, Part 2, and Part 3. Although the articles were very helpful, they were two and a half years old, and it looks like more than a few things have changed, notably that Jim was using the OpenOCD “Open On-Chip Debugger” which itself was having some recent breaking version changes that didn’t work when I first tried to get things to work.

Long story short: it looks like things are more streamlined today, and it’s easier to get everything running.

  1. Install your basic Rust development environment.
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Add ~/.cargo/bin to your path and then run rustup update.

2. Install Rust development dependencies needed to compile:

$ rustup target install thumbv6m-none-eabi

3. Install the flip-link crate. I’ve seen some templates use the flip-link linker which flips the memory layout in order to create some protection against stack overflows. (I don’t fully understand how it works or how important it is, but there’s not hard installing it.)

$ cargo install flip-link

3. Install Probe-RS following instructions from its documentation here. Separate instructions exist for Linux, Mac OS, andWindows, but regardless they involve executing one or two commands from a terminal window.

4. We’re going to use Visual Studio Code as our IDE because it’s the interface that probe-rs supports. (It looks like RustRover has feature requests for OpenOCD and probe-rs, but no support exists yet.) In VS Code, install the rust-analyzer and Debugger for probe-rs extensions. Additionally, I’ve found a VS Code extension called Crates that is rather clever in helping you keep your crate versions up to date.

In part 4 of this series, we will finally get to run our first program! I’ll show how to wire up your Raspberry Pi Pico W to your boot controller—for both scenarios where you either use another Pico or the Debug Probe. We’ll write both a straight embedded-hal program (no Embassy) blinking an external LED light, and then we’ll rewrite blinky using Embassy, still using the same external LED. (We’ll have to wait a few more entries before we get to master that onboard LED!)

--

--

Murray Todd Williams

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