Embedded Rust - Raspberry Pi Pico
In this chapter, we'll be looking at programming a Raspberry Pi Pico board. This board is relatively cheap and is great to learn embedded development. The Raspberry Pi Pico development board is equipped with an ARM Cortex M0+ processor. The full specifications can be found here: raspberry-pi-pico.
Getting prepared
Hardware
In order to build your first embedded project you need the following:
- At least two Raspberry Pi Pico development boards,
- Two breadboards with some common components; look for a bundle:
- Jumper wire cables,
- Some LEDs, resistors and push buttons.
- One Micro USB cable.
It also helps to have some soldering gear.
If you are not soldering the pins to the Pico board, do make sure the connections to the breadboard are solid and stable. You can easily lose a few hours debugging your source code, before figuring our that one of the connections is broken, or unstable.
Software
First we'll install probe-rs
which is a tool to flash and debug embedded devices.
If you have cargo-binstall installed, you can install probe-rs
with:
$ cargo binstall probe-rs
If not, you can build from source with:
$ cargo install probe-rs --locked --features cli
I had to restart my terminal to get the
probe-rs
command to work, i.e. to be found in the path. It should be in~/.cargo/bin/probe-rs
.
Rust ARM target
We'll also need the build toolchain for the ARM Cortex M0 processor. This includes the ARM cross-compile tools, OpenOCD and the Rust ARM target.
Make sure you have the thumbv6m-none-eabi
target installed. We'll use the rustup
tool to install it:
$ rustup target add thumbv6m-none-eabi
Connect the Raspberry Pi Pico
In order to program the Raspberry Pi Pico board, you need to connect it to your PC using the USB cable. You need two Pico boards for this task. The first board will be the programmer, and the second board will be the target.
Schematic:
The schematic for connecting the two Pico boards is as follows:
Pinout of the Raspberry Pi Pico
The pinout of the Raspberry Pi Pico is as follows:
Wiring
- White: Connect pin 4 (
SPIO SCK
) on the programmer Pico to theSWDCLK
pin on the target Pico, - Purple: Connect pin 5 (
SPIO TX
) on the programmer Pico to theSWDIO
pin on the target Pico, - Green: Connect pin 6 (
UART1 TX
) on the programmer Pico to pin 2 (UARTO RX
) on the target Pico, - Yellow: Connect pin 7 (
UARTI RX
) on the programmer Pico to pin 1 (UARTO TX
) on the target Pico, - Red: Connect pin 39 (
VSYS
) on the programmer Pico to pin 39 (VSYS
) on the target Pico, - Black: Connect pin 38 (
GND
) pin on the programmer Pico pin 38 (GND
) on the target Pico. - The ground (
GND
) pin on the target Pico should be grounded; this is the black wire between the purple and white one,
Flash the programmer Pico
Once you have everything wired up you need to install the debugprobe firmware on the programmer Pico board. This is done
by downloading the debugprobe_on_pico.uf2
file from
the Debugging using another Raspberry Pi Pico
website.
Hold down the BOOTSEL button when you plug in your programmer Pico. You should now see the pico appear as a USB drive.
Drag and drop the debugprobe_on_pico.uf2
file onto the USB drive. The Pico will reboot and you should now be able to
see the debug probe when you run the probe-rs
command.
$ probe-rs list
The following debug probes were found:
[0]: Debugprobe on Pico _CMSIS_DAP_ (VID: 2e8a, PID: 000c, Serial: E6612483CB5A612A, CmsisDap)
Quick start
We'll be using the excellent embassy
crate to program the Pico. The embassy
crate is a framework for building
concurrent firmware for embedded systems. It is based on the async
and await
syntax, and is designed to be used with
the no_std
environment.
Create a new hello_pico_world
project with the following files; take care to put them in the correct directories:
cargo new hello_pico_world
cd hello_pico_world
In the project root:
Cargo.toml
[package]
name = "hello_pico_world"
version = "0.1.0"
edition = "2021"
[dependencies]
embassy-embedded-hal = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy", features = ["defmt"] }
embassy-sync = { version = "0.5.0", git = "https://github.com/embassy-rs/embassy", features = ["defmt"] }
embassy-executor = { version = "0.5.0", git = "https://github.com/embassy-rs/embassy", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] }
embassy-time = { version = "0.3", git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime"] }
embassy-rp = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl"] }
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
cortex-m-rt = "0.7.0"
panic-probe = { version = "0.3", features = ["print-defmt"] }
defmt = "0.3"
defmt-rtt = "0.4"
Next to the Cargo.toml
, create a build script:
build.rs
//! This build script copies the `memory.x` file from the crate root into //! a directory where the linker can always find it at build time. //! For many projects this is optional, as the linker always searches the //! project root directory -- wherever `Cargo.toml` is. However, if you //! are using a workspace or have a more complicated build setup, this //! build script becomes required. Additionally, by requesting that //! Cargo re-run the build script whenever `memory.x` is changed, //! updating `memory.x` ensures a rebuild of the application with the //! new memory settings. use std::env; use std::fs::File; use std::io::Write; use std::path::PathBuf; fn main() { // Put `memory.x` in our output directory and ensure it's // on the linker search path. let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); File::create(out.join("memory.x")) .unwrap() .write_all(include_bytes!("memory.x")) .unwrap(); println!("cargo:rustc-link-search={}", out.display()); // By default, Cargo will re-run a build script whenever // any file in the project changes. By specifying `memory.x` // here, we ensure the build script is only re-run when // `memory.x` is changed. println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rustc-link-arg-bins=--nmagic"); println!("cargo:rustc-link-arg-bins=-Tlink.x"); println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); }
Add this memory.x
file:
memory.x
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
/* Pick one of the two options for RAM layout */
/* OPTION A: Use all RAM banks as one big block */
/* Reasonable, unless you are doing something */
/* really particular with DMA or other concurrent */
/* access that would benefit from striping */
RAM : ORIGIN = 0x20000000, LENGTH = 264K
/* OPTION B: Keep the unstriped sections separate */
/* RAM: ORIGIN = 0x20000000, LENGTH = 256K */
/* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */
/* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */
}
Then add a project-specific configuration file:
.cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip RP2040"
[build]
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
[env]
DEFMT_LOG = "debug"
And finally edit the src/main.rs
file to look like:
src/main.rs
#![no_std] #![no_main] use defmt::*; use embassy_executor::Spawner; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; #[embassy_executor::main] async fn main(_spawner: Spawner) { let _p = embassy_rp::init(Default::default()); loop { info!("Hello Pico World!"); Timer::after_secs(1).await; } }
Your project structure should look like this:
.
├── Cargo.toml
├── memory.x
├── .cargo
│ └── config.toml
├── build.rs
└── src
└── main.rs
First run
Now that you have the project set up, you can build and flash it to the Pico board:
$ cargo run --release
After the standard build steps are complete, you should see something like this:
Erasing ✔ [00:00:00] [##########################################################################################################################################################] 16.00 KiB/16.00 KiB @ 90.04 KiB/s (eta 0s )
Programming ✔ [00:00:00] [##########################################################################################################################################################] 16.00 KiB/16.00 KiB @ 41.75 KiB/s (eta 0s ) Finished in 0.587s
0.002258 INFO Hello Pico World!
└─ <mod path> @ └─ <invalid location: defmt frame-index: 4>:0
1.002317 INFO Hello Pico World!
└─ <mod path> @ └─ <invalid location: defmt frame-index: 4>:0
2.002338 INFO Hello Pico World!
└─ <mod path> @ └─ <invalid location: defmt frame-index: 4>:0
If you encounter any build issues, double-check that the config.toml
, memory.x
and build.rs
files are in the
correct
locations.
If you see the Hello Pico World!
message, you have successfully flashed the Pico board with your first Rust
program.
This project structure will be the foundation for the next steps in building more complex embedded projects. You may want to make a copy of this project structure to use as a template for future projects.
Running without the debugger.
Once loaded, your application is automatically executed when you power up the Pico board.
- Connect a USB charger to the USB port on the board. Make sure to disconnect the programmer Pico board when doing this
- Connect a 3.3v battery to the
V3
andGND
pins (red & black)
Next steps
Next we'll be building our first Blinky project using the breadboard and a few components.