My personal webspace

A webspace for innovation, free thinking, and procrastination

I was gifted a Raspberry Pi Pico a few days ago; this is a very small embedded chip, with no OS. It is very different than anything I’ve worked with before, which has mostly been Linux-based (with the occosional Arduino). This seemed like an awesome place to use Rust, so I decided to make a small ‘simon says’ type blinking game.

To program this chip, you hold a physical button and plug it into your PC. It gets read as a USB drive, which you can drop a UF2 file into. The chip then reboots, and executes your program.

The official docs first have you download their SDK and run some install scripts. Development is geared towards the host being Raspian (or other Debian-like system). It doesn’t really help you get started with Rust, and not from a non-Debian machine.

Instead, I found various tutorials online and some sample code. Working from one of those examples, I synthesized the smallest program to get started (just turn on GPIO25, the onboard LED).

To get started, install the correct Rust target:

rustup self update
rustup update stable
rustup target add thumbv6m-none-eabi

Create a .cargo/config file (or ~/.cargo/config) with:

# Set the default target to match the Cortex-M0+ in the RP2040
target = "thumbv6m-none-eabi"

# Target specific options
# Pass some extra options to rustc, some of which get passed on to the linker.
# * linker argument --nmagic turns off page alignment of sections (which saves
#   flash space)
# * linker argument -Tlink.x tells the linker to use link.x as the linker
#   script. This is usually provided by the cortex-m-rt crate, and by default
#   the version in that crate will include a file called `memory.x` which
#   describes the particular memory layout for your specific chip. 
# * inline-threshold=5 makes the compiler more aggressive and inlining functions
# * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't
#   have SIMD)
rustflags = [
    "-C", "link-arg=--nmagic",
    "-C", "link-arg=-Tlink.x",
    "-C", "inline-threshold=5",
    "-C", "no-vectorize-loops",

Copy the memory.x file from rp-hal into the same directory as Cargo.toml.

You need add some dependencies to Cargo.toml:

# Provides the #[entry] macro, which tells Rust where our main func is.
cortex-m-rt = "0.7"
# Provides access to pico hardware
rp-pico = "0.4"
# Simplifies/makes generic access to hardware
embedded-hal ="0.2"

And finally, fill in your main file:

// Turn off std and main; they assume some features are available
// which aren't (i.e., a heap).

// We need to define our own panic handler
use core::panic::PanicInfo;

// peripheral access crate
use rp_pico::hal::{self, pac};

use embedded_hal::digital::v2::OutputPin;

// The macro for our start-up function
use cortex_m_rt::entry;

fn main() -> ! {
    // take the peripherals
    let mut pac = pac::Peripherals::take().unwrap();

    // get access to the GPIO pins
    let sio = hal::Sio::new(pac.SIO);
    let pins = rp_pico::Pins::new(
        &mut pac.RESETS,

    let mut led_pin = pins.led.into_push_pull_output();
    loop {
        // this function must never return

fn panic(_: &PanicInfo) -> ! {
    // when we panic, do nothing
    loop {}

Now we can build this code:

$ cargo build --release
# If you don't override the default target in .cargo/config, specify the target
# cargo build --release --target thumbv6m-none-eabi

This will create a file at target/thumbv6m-none-eabi/release/blinky (if your package is named blinky); but if we look at it, it’s still an ELF binary.

$ file target/thumbv6m-none-eabi/release/blinky
target/thumbv6m-none-eabi/release/blinky: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

We need to convert that to USF2 format. The Pico SDK shipped with a elf2uf2 tool, or we can use a Rust-based one.

cargo install elf2uf2-rs --locked

With that installed, we can convert the binary:

$ elf2uf2-rs target/thumbv6m-none-eabi/release/blinky blinky
$ ls -lh blinky.*
-rw-r--r-- 1 charles charles 2.0K Jun 22 12:59 blinky.uf2

Hold the BOOTSEL button on the pico, plug it, then let go of the BOOTSEL button. Mount it as a flash drive, and copy that file to it. It should reboot, and the LED will turn on :).

Content © 2022 Charles Hathaway