Getting Started with Rust on a Raspberry Pi Pico (Part 2)
Iterating Rust development faster with cargo run
This is the second in a series of guides exploring the use of Rust on the Raspberry Pi Pico. In this guide, I’ll be showing you how to make your life easier through the use of the cargo run
command which is an important part of developing all kinds of applications with Rust.
If you’re not already familiar with Cargo, then I encourage you to start by reading the official Cargo Book provided by the Rust community. At a minimum, read the first section on what Cargo is and why it exists. You’ll be much better equipped to fully appreciate this guide.
Part 1 of the series - blinking an LED from a Pico
Part 3 of the series - iterating even faster by flashing and debugging with Visual Studio Code
Review from Last Time
From the first guide in this series, I showed you how to blink an LED with Rust on a Pico device. We made use of OpenOCD, gdb and a little bit of Cargo to build the example application’s binary to run on the target Pico device.
The techniques from the guide certainly get the job done in that it gives you all the tooling and setup you need to start building applications for the Pico using Rust. But it’s not a very fun process nor is it one that allows you to iterate quickly when making rapid changes to your source code.
Each time you made a change to your code, you have to call cargo build
, manually start up OpenOCD, manually run gdb, and then type a series of commands in gdb to flash your binary onto the target and begin debugging your code.
This is exactly where cargo run
can speed up and greatly reduce the number of commands you need to type in and run each time you want to test out your changes.
Configuring Cargo
The first step to take to be able to use cargo run
is to modify your project’s Cargo configuration file. We’re still going to be using the example application from the first guide in the series.
To begin, open up the following file in a text editor:
$ vim ~/Projects/rp2040-project-template/.cargo/config.toml
The file should look like this by default:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-run --chip RP2040"
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# Code-size optimizations.
# trap unreachable can save a lot of space, but requires nightly compiler.
# uncomment the next line if you wish to enable it
# "-Z", "trap-unreachable=no",
"-C", "inline-threshold=5",
"-C", "no-vectorize-loops",
]
[build]
target = "thumbv6m-none-eabi"
Notice the second line in the config file:
runner = "probe-run --chip RP2040"
Comment this line out like so:
# runner = "probe-run --chip RP2040"
Then add a new line just under the one you commented out:
If you’re using Ubuntu:
runner = "gdb-multiarch -q -x openocd.gdb"
or if you’re on a Mac:
runner = "arm-none-eabi-gdb -q -x openocd.gdb"
The resulting file should now look like:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "arm-none-eabi-gdb -q -x openocd.gdb"
# runner = "probe-run --chip RP2040"
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# Code-size optimizations.
# trap unreachable can save a lot of space, but requires nightly compiler.
# uncomment the next line if you wish to enable it
# "-Z", "trap-unreachable=no",
"-C", "inline-threshold=5",
"-C", "no-vectorize-loops",
]
[build]
target = "thumbv6m-none-eabi"
When you execute cargo run
from the root directory of the example application source tree, Cargo will know to execute gdb for you - you no longer have to execute gdb manually yourself. It can have different runners for different contexts, but since we didn’t specify a particular —target
, it’ll make use of the one we specified because of the broadly specified platform target from the first line:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
For more information about platform targets, see the Cargo Book documentation.
Saner gdb use
We’re not quite ready to try this out just yet. You’ll notice that at the end of the line specifying the runner, I include a local gdb config file to use. As you’ll see shortly, this file contains the base set of gdb commands that we want to run every time we execute cargo run
. This saves us a lot of repetitive typing allowing us to iterate much more quickly on our development cycle.
Using your text editor, create the following file:
$ vim ~/Projects/rp2040-project-template/openocd.gdb
and then add the following text to the file from your editor and save it:
# connect to OpenOCD on TCP port 3333
target extended-remote :3333
# print demangled function/variable symbols
set print asm-demangle on
# set backtrace limit to not have infinite backtrace loops
set backtrace limit 32
# detect unhandled exceptions, hard faults and panics
break DefaultHandler
break HardFault
# *try* stopping at the user entry point (it might be gone due to inlining)
break main
monitor arm semihosting enable
# load the application binary onto the Pico's flash
load
# start the process but immediately halt the processor
stepi
Based on openocd.gdb from the Embedded Rust Discovery Book
Running and Debugging with Cargo
Change into the root directory where you built openocd in, for example:
$ cd ~/Projects/openocd
In a new terminal run and keep OpenOCD open:
$ src/openocd -f interface/picoprobe.cfg -f target/rp2040.cfg -s tcl
At this point we’re ready to try cargo run
. From the root directory of the example application source tree, run the following in a separate terminal from the one used to run OpenOCD:
$ cargo run
You should see output similar to the following:
Finished dev [optimized + debuginfo] target(s) in 0.06s
Running `arm-none-eabi-gdb -q -x openocd.gdb target/thumbv6m-none-eabi/debug/rp2040-project-template`
Reading symbols from target/thumbv6m-none-eabi/debug/rp2040-project-template...
warning: multi-threaded target stopped without sending a thread-id, using first non-exited thread
0x100001aa in Reset ()
Breakpoint 1 at 0x10001f24: file /Users/jhodapp/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.7.0/src/lib.rs, line 933.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x10003140: file /Users/jhodapp/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.7.0/src/lib.rs, line 923.
Breakpoint 3 at 0x100001f4: file src/main.rs, line 26.
semihosting is enabled
Loading section .boot2, size 0x100 lma 0x10000000
Loading section .vector_table, size 0xa8 lma 0x10000100
Loading section .text, size 0x2f9c lma 0x100001a8
Loading section .rodata, size 0x83c lma 0x10003150
Loading section .data, size 0x30 lma 0x1000398c
Start address 0x100001a8, load size 14768
Transfer rate: 9 KB/sec, 2953 bytes/write.
0x100001aa in Reset ()
(gdb)
If you type continue
into gdb, then it should hit the main()
function breakpoint. If you type continue
again then the LED should start blinking exactly like it did from the first guide in the series. If you press CNTRL+C,
you’ll notice that you’re inside a gdb command shell where you are now set up to debug this application.
Let’s list out the breakpoints that we set in the gdb config file:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x10001f24 in cortex_m_rt::DefaultHandler_ at /Users/jhodapp/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.7.0/src/lib.rs:933
2 breakpoint keep y 0x10003140 in cortex_m_rt::HardFault_ at /Users/jhodapp/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.7.0/src/lib.rs:923
3 breakpoint keep y 0x100001f4 in rp2040_project_template::__cortex_m_rt_main_trampoline at src/main.rs:26
Success! We’ve got an easy-to-iterate embedded Rust setup now.
Note that I did try the instructions specified at here for a non-OpenOCD setup using a CMSIS-DAP debug probe with probe-run-rp, which also uses a second Pico to program a first. I did not have any luck with getting this to run and haven’t had the time to figure out what’s not working. If you have better luck with this than I, please let me know. I’ll add additional instructions to this guide covering the probe-run-rp method to use cargo run
for fast iterative embedded Rust development.
Next
For the next guide in this series I’ll be exploring an even easier way to debug your Rust application on a Pico and well as starting to explore working with more of the Pico’s functionality with Rust.
Please enjoy!
Read Part 3 of this series, iterating even faster by flashing and debugging with Visual Studio Code
Enjoying this series? 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.
Awesome articles, thanks! Had some issues in part 2. I'm on a M1 Mac, Mac OS 13.3.1:
* there was no .cargo/config, but a .cargo/config.toml
* I had to prefix the changed runner with [target.thumbv6m-none-eabi], or it would try to use elf2uf2-rs and fail.
Looking forward to your next article in this series. Would love to be able to debug rust code on Linux with Visual Code Studio and the 4$ PICO as a target!