Getting Started with Rust on a Raspberry Pi Pico (Part 1)
Everything you need to blink the Pico's onboard LED from Rust
This is the first in a series of guides exploring the highly productive and exciting world of Raspberry Pi Foundation’s first microcontroller (RP2040) + board (Pico) with firmware written in Rust. This first one will cover how to use a Raspberry Pi Picoprobe (RPi Pico + debugger firmware) to flash and debug embedded Rust firmware on another Pico board.
True to the original charter of this Substack, I’ll also be discussing some of the human relational aspects of these guides. That might sound confusing or even a bit of a stretch, but trust me when I say there’s more than just a technical reason I’m posting a guide like this. Publishing a technical guide is inherently a human relational act. I’ll explain more about this in a future guides in this series.
Part 2 of the series - iterating faster with cargo run
Part 3 of the series - iterating even faster by flashing and debugging with Visual Studio Code
Make sure to subscribe so you don’t miss any of the future guides in this series of working with Rust on the Raspberry Pi Pico.
This guide covers using two Raspberry Pi Pico boards, one as the target and one as a hardware programmer and debugger. You may purchase these boards from several places, but I ordered mine with pre-soldered headers from Elektor’s store.
Note You may find the latest source version of Picoprobe firmware from the Raspberry Pi Foundation.
If you’re not already familiar with Rust, it’s the first language that has truly gotten me excited to work with for embedded applications since I first started working with C/C++ many years ago. There have been others that have tried to bring performance, safety and convenience to embedded programming, but none have delivered on this quite as well as Rust. It truly is the first language that I believe can fully replace C/C++ without sacrificing much of anything while giving you many things that C/C++ will never provide. This blog post covering some of C vs Rust is well worth a read.
Hardware Setup
Before getting the required software installed, flashed and configured, we must first get our hardware wired and ready to be flashed. You need two identical Raspberry Pi Picos ideally with pre-soldered headers attached.
Note: For the skilled solderer you may attach your own headers, but do know it’s not very easy to get them soldered without accidentally melting or damaging the board in some way. When I attempted it, even with some prior soldering experience, I melted the plastic button top without realizing it until after.
Pico to Pico Wiring
Figure 1 - Pico A and Pico B (source)
Figure 2 - Pico A and Pico B
As you can see in Figure 1, having the soldered headers attached to both Pico boards makes it much easier to wire together for flashing and debugging because you can use a breadboard to make all of the pin connections very straightforward.
If you’re looking for some jumper wires like what I used you may order them from SparkFun here:
Here’s a written summary of how to wire the two Pico boards to each other, referring to the Pico board on the left as Pico A and the one on the right as Pico B:
Connect Pico B GND (Pin 38) to Pico A GND (Pin 38)
Connect Pico B VSYS (Pin 39) to Pico A VSYS (Pin 39)
Connect Pico B UART0_TX (Pin 1) to Pico A UART1_RX (Pin 7)
Connect Pico B UART0_RX (Pin 2) to Pico A UART1_TX (Pin 6)
Connect Pico B SWCLK to Pico A I2C1 SDA (Pin 4)
Connect Pico B GND to Pico A GND (Pin 3)
Connect Pico B SWDIO to Pico A I2C1 SCL (Pin 5)
Note: See this pin diagram (also shown above) that shows a Pico board pin diagram containing both pin numbers and pin names. Also note that in the photo of my breadboard wiring, I use the (+) and (-) columns for convenience, otherwise I literally follow the above wiring summary.
Pico to Development Host Wiring
Connect the included micro USB cable from Pico A to your development host computer which will allow the host computer to use Pico A as the programmer/debugger as well as provide power to both Pico B and Pico A.
Software Setup
Ubuntu Linux
These instructions were tested with Ubuntu Linux 21.04 and will likely continue to work correctly for several newer versions of Ubuntu. Make sure that your installed software is fully up to date via the Software Updater utility before following this guide.
macOS
These instructions were tested with macOS Big Sur v11.5.x and will likely continue to work correctly for newer versions of macOS. Make sure that your base macOS is fully up to date via the Software Update utility under System Preferences before following this guide.
Note to complete the installation of the toolchain on Mac please make sure you have Homebrew installed before proceeding.
Installing the Toolchain
In order to flash your Rust application onto your Pico target board, you must first install the GNU Debugger (gdb) and OpenOCD. The debugger is a widely used piece of software that provides a way to flash a binary onto many different targets, set breakpoints in your code on specific lines, and step through your code helping you better understand exactly what’s happening.
To install gdb, open a terminal on your development machine and enter the following:
$ sudo apt install git gdb-multiarch
If you’re on a Mac:
$ brew install git armmbed/formulae/arm-none-eabi-gcc
If you’re on an M1-based Mac or any Arm-based Apple Silicon that arrived after the M1, make sure to install Rosetta support. This is required because at the time of writing, native Arm support for gdb via Homebrew does not yet exist:
$ softwareupdate --install-rosetta
OpenOCD is a piece of software that exists to translate between specific debugging protocols on many different types of microprocessors and SoCs, and then presents a unified protocol interface for gdb to connect to.
At the time of writing support for the RP2040 (the microprocessor on the Pico board) has not been upstreamed and released in Ubuntu’s openocd package yet, so it must be built and installed from the Raspberry Pi Foundation’s GitHub repository:
$ sudo apt install automake autoconf build-essential texinfo libtool libftdi-dev libusb-1.0-0-dev
$ git clone https://github.com/raspberrypi/openocd.git --branch picoprobe --depth=1
$ cd openocd
$ ./bootstrap
$ ./configure --disable-werror --enable-ftdi --enable-sysfsgpio --enable-bcm2835gpio --enable-picoprobe
$ make -j4
$ sudo make install
On a Mac:
$ brew install automake libtool libusb pkg-config texinfo wget
$ git clone https://github.com/raspberrypi/openocd.git --branch picoprobe --depth=1
$ export PATH="/usr/local/opt/texinfo/bin:$PATH"
# or if you see an error like: "./doc/openocd.texi:10934: Unknown command `raggedright'."
$ export PATH="/opt/homebrew/opt/texinfo/bin:$PATH"
$ cd openocd
$ ./bootstrap
$ ./configure --enable-picoprobe --disable-werror
$ make -j4
$ sudo make install
Note If your development host is Ubuntu on a full Raspberry Pi (e.g. v3 or v4) the first three options allow for bitbanging over the SWD port on the Pico. These flags aren’t required nor useful if you’re working from a regular laptop machine.
Checking for the version of openocd should verify a successful build and installation looking something like:
$ openocd -v
Open On-Chip Debugger 0.10.0+dev-g18b4c35-dirty (2021-08-17-22:32)
On a Mac:
$ openocd -v
Open On-Chip Debugger 0.10.0+dev-g18b4c35 (2021-09-03-13:32)
And now install a program that can connect to the UART (serial) port on the Pico allowing you to see console log messages that your application prints out.
$ sudo apt install minicom
On a Mac:
$ brew install minicom
For the latest information as well as some great tips and tricks for working with the Pico see the official Raspberry Pi Getting Started Guide.
Flashing Picoprobe Firmware
In order to use one Pico to flash and debug another one, you must first flash firmware created by the Raspberry Pi Foundation onto one Pico of your choice.
To do that, first download this UF2 binary onto your development machine
Then, holding down the BOOTSEL (short for boot select) button on the Pico that will become the Picoprobe, plug in the USB cable into your development machine
A window of the flash storage of the Pico should pop up no matter what operating system you’re running on your development machine
Drag and drop the
picoprobe.uf2
onto this windowThe Pico should now be fully flashed and ready to be used as a Picoprobe
Setting Picoprobe User Device Access
Note: This section is for Ubuntu Linux only and does not apply to macOS.
Unless you set up special user level access to the Picoprobe, you’ll be required to use sudo
every time you try to connect OpenOCD to it. Instead, we can create a udev rule that’ll allow you to connect as a non-privileged user.
To get the necessary parameters required to set up the udev rule (i.e. the probe’s USB PID/VID), run the following after plugging in the Pico acting as a debug probe into your host development machine:
$ lsusb | grep -i pico
You’ll see output that looks like the following:
Bus 001 Device 018: ID 2e8a:0004 Raspberry Pi Picoprobe
In this case the PID is 2e8a
and the VID is 0004
.
Next, open up an editor creating the following udev rules file if it doesn’t already exist. If it does exist, just add to the bottom.
$ sudo vim /etc/udev/rules.d/99-openocd.rules
Adding the following two lines:
# Raspberry Pi Picoprobe
ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
And to apply the udev rules to your running system:
$ sudo udevadm trigger
Installing Rust
Of course you can’t do much in the way of writing applications for the Pico without a language and toolchain. So let’s review how to get Rust installed on your development machine.
Even though this is based on the Embedded Rust book and the standard Rust installation guide, and you could technically follow those instructions there, bringing everything into one guide ensures that everything is convenient and the instructions have all been tested together as a set.
To begin, in your terminal, start by installing rustup which will manage your entire Rust toolchain:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup will attempt to automatically configure your path. If it fails to do so automatically you can manually add your Rust installation to your $PATH environment variable if you’re using Bash:
$ echo ‘~/.cargo/bin’ >> ~/.bashrc
$ source ~/.bashrc
Or to add it if you’re using Zsh:
$ echo ‘~/.cargo/bin’ >> ~/.zshrc
$ source ~/.zshrc
Verify your Rust toolchain installation:
$ rustc --version
Should output something like:
rustc 1.54.0 (a178d0322 2021-07-26)
Updating Rust
Note that for the future, you will manage updating Rust via rustup. You can update to the latest stable version of your Rust toolchain via:
$ rustup update
Embedded Rust Example Application
Now that we’ve got a full development and target device setup, let’s move on to how to use it to compile, flash and debug an example Rust application. This example comes from an open source GitHub repository providing all of the needed pieces written in Rust to target a Raspberry Pi Pico and onboard peripherals. You may find the original source code here which continues to evolve, so it may or may not look the same as what’s listed here.
This example simply blinks the onboard LED on the Pico, cycling every ½ second. It does this by writing a high (1) or a low (0) to the I/O port GP25.
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use defmt::*;
use defmt_rtt as _;
use embedded_hal::digital::v2::OutputPin;
use embedded_time::fixed_point::FixedPoint;
use panic_probe as _;
use rp2040_hal as hal;
use hal::{
clocks::{init_clocks_and_plls, Clock},
pac,
io::Sio,
watchdog::Watchdog,
};
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
#[entry]
fn main() -> ! {
info!("Program start");
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let sio = Sio::new(pac.SIO);
// External high-speed crystal on the pico board is 12Mhz
let external_xtal_freq_hz = 12_000_000u32;
let clocks = init_clocks_and_plls(
external_xtal_freq_hz,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer()
);
let pins = hal::gpio::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let mut led_pin = pins.gpio25.into_push_pull_output();
loop {
info!("on!");
led_pin.set_high().unwrap();
delay.delay_ms(500);
info!("off!");
led_pin.set_low().unwrap();
delay.delay_ms(500);
}
}
Figure 3 - Rust source that blinks the onboard LED
This example makes use of several Pico-specific abstractions that are also a part of the same open source project.
The first is the RP2040 microcontroller hardware abstraction layer (HAL). What’s a HAL used for exactly?
Start by cloning the git repository for the example Rust application from above:
$ git clone https://github.com/rp-rs/rp2040-project-template
Next we’ll build the Rust application into a binary image. To do this, use cargo to build:
$ cd rp2040-project-template
To install Rust development dependencies needed to compile run:
$ rustup target install thumbv6m-none-eabi
$ cargo install probe-run
$ cargo install flip-link
Now to actually build the example application binary:
$ cargo build
Loading the App onto the Target with gdb
Change into the root directory where you built openocd in, for example:
$ cd ~/Projects/openocd
In one terminal run and keep open:
$ src/openocd -f interface/picoprobe.cfg -f target/rp2040.cfg -s tcl
In another terminal run:
$ cd ~/Projects/rp2040-project-template
Ubuntu:
$ gdb-multiarch -q -ex "target extended-remote :3333" target/thumbv6m-none-eabi/debug/rp2040-project-template
Or on Mac:
$ arm-none-eabi-gdb -q -ex "target extended-remote :3333" target/thumbv6m-none-eabi/debug/rp2040-project-template
Now to load the example application onto the target Pico A, run the following from the gdb shell:
(gdb) load
To test that the application binary loaded onto the target flash successfully, run it from the gdb shell:
(gdb) continue
Note: if this is your first time using gdb, you can issue abbreviated forms of almost every command. For example, instead of typing continue
I could simply type c
.
At this point you should see the main LED on the Pico board flashing on and off in an infinite loop. This application is loaded onto the flash, so even if you exit out of gdb, unplug the USB cable and re-plug it in, the same application will automatically run again once it receives power from the USB port.
Pressing CNTRL+C
will cause gdb to interrupt execution on the target and you’ll see what line of code from the example application that it was about to execute next.
Helpful Related Resources
Rust Book - A great intro to Rust as a programming language
Embedded Rust Book - A great intro to embedded programming using Rust
Embedded Rust Discovery Book, How to Use GDB - A great overview of the most common gdb commands you’ll want to know about in working with Rust on an Embedded device
Embedded Rust Discovery Book, Debugging a Target - Pairs well with the previous How to Use GDB and shows you several of these GDB commands in practice, debugging a different target device (the Micro:bit)
Getting Starting with Raspberry Pi Pico - From the Raspberry Pi Foundation (C/C++ centric but related)
Embedded Software Engineering 101 - If you’re brand new to embedded concepts, this is an amazingly accessible introduction
Next
Future guides in this series will dive more into working with Rust on the Pico, exploring the current capabilities of the Raspberry Pi Pico hardware abstraction layer using what we learned here.
I welcome all feedback about this guide and any future ones in the series. And if you’d like to see me cover a specific aspect of Rust and the Pico, let me know!
As always, thanks for reading and passing this along to anyone else that would find value from reading the Relational Technologist Substack.
Please enjoy!
Read Part 2 of this series, iterating on development faster with the use of Cargo
Last updated: March 24, 2022
Are you struggling to learn Rust?
I work with engineers just like you as a software engineer coach, and I’d love to help you master Rust, or something else.
I’m currently offering 1 free month* of 1:1 software engineer coaching, let me show you how we can grow your software engineer mastery more effectively together
Looking for a community to learn Rust in?
If a software engineer coach currently doesn’t sound like what you want for yourself, I also have an open source community dedicated to learning Rust, mastering Rust and doing both while building real open source software.
We offer weekly live meeting times and a Slack community to interact with other engineers any time, day or night.
It’s completely free and I welcome you to come check us out. We call ourselves Rust Never Sleeps.
To get involved further, simply fill out this short questionnaire.
*Offer good for May 2022
Hey,
Great tutorial Jim. I got it working quickly. Some notes from me during going through your tutorial:
Wiring - the image is correct but description might have errors:
>2. Connect Pico B VSYS (Pin 39) to Pico A VSYS (Pin 3) // I think it's 39 not 3 (3 - is GND)
>3. Connect Pico B UART0_TX (Pin 1) to Pico A UART1_RX (Pin 6) // err 1 go to 7 (7 - is TX)
>4. Connect Pico B UART0_RX (Pin 2) to Pico A UART1_TX (Pin 7) // err 2 go to 6 (6 - is RX)
udev rules (on Ubuntu). I dont know why but:
>sudo udevadm control --reload-rules
didn't work
However i did
sudo udevadm trigger
and it worked after that
Thanks for the guide,
Cheers
Great article! Had an issue on M1 Mac with Mac OS 13.3.1 where openocd couldn't find the pico running the picoprobe firmware.
I was able to fix this by applying the simple config changes mentioned here: https://github.com/raspberrypi/openocd/issues/80