week 4. Individual: Writing Programs for RP2040
Table of Contents
Program #1: Simple Delay Program in Rust#
I decided to first write a program that just calls delay. Obviously I cannot see anything but at least I can see the workflow succeeding and the code compiling.
Connecting the Pico#
To write the program, we need to use a microUSB cable to connect the Pico the computer.

While connecting, we need to press and hold the BOOTSEL button so that we can flash the program on the Pico via USB.

Now I can see the USB drive called RPI-RP2 of the Pico board.

Code#
I chose to write the program in Rust, and hence used the tooling and workflow described in the group assignment.
As I learnt from the previous failed attempts, a Rust program for the RP2040 has to be written with this structure:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // Panic handler
#[entry]
fn main() -> ! {
// e.g.
loop {
// Delay for approximately 1 million CPU cycles
cortex_m::asm::delay(1_000_000);
}
}
Here we simply use the low-level cortex_m library that calls the delay instruction. From it’s documentation, we see that it is not a time delay but a delay in terms of instruction cycles so that the actual time depends on the frequency of the processor.
Cargo Configuration#
I save the above file as delay.rs and configured Cargo.toml with these lines:
[[bin]]
name = "delay"
path = "delay.rs"
Flashing the firmware#
And then with RPI connect in BOOTSEL mode, I ran:
cargo run --bin delay
but I got this error:
➜ embedded-programming-week3 git:(main) ✗ cargo run --bin delay
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
error: linking with `rust-lld` failed: exit status: 1
|
= note: "rust-lld" "-flavor" "gnu" "/var/folders/tn/zxfh2bqj0fj1dn7dtf886dz80000gn/T/rustcjgQ14I/symbols.o" "<2 object files omitted>" "--as-needed" "-Bstatic" "/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3/target/thumbv6m-none-eabi/debug/deps/{libcortex_m-783bed34a9270c27.rlib,libembedded_hal-1c5a48dd3253adec.rlib,libvoid-4d28b3ca54e54dc1.rlib,libnb-dec891d560c072bd.rlib,libnb-177b7dadb39c4b9c.rlib,libvolatile_register-740f2b318fdb8a1d.rlib,libvcell-c4b4b32c33790337.rlib,libbare_metal-009d6e9973f02064.rlib,libpanic_halt-aa440d0ad57a6ca5.rlib,libcortex_m_rt-6dd700e433012c95.rlib}.rlib" "<sysroot>/lib/rustlib/thumbv6m-none-eabi/lib/{librustc_std_workspace_core-*,libcore-*,libcompiler_builtins-*}.rlib" "-Bdynamic" "--eh-frame-hdr" "-z" "noexecstack" "-L" "/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3/target/thumbv6m-none-eabi/debug/build/cortex-m-8334e04901e57613/out" "-L" "/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3/target/thumbv6m-none-eabi/debug/build/cortex-m-rt-1afaf5724c22794d/out" "-L" "/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3/target/thumbv6m-none-eabi/debug/build/rp2040-pac-62e8dd975a3448f1/out" "-o" "/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3/target/thumbv6m-none-eabi/debug/deps/delay-1a7db8df1c0e0fe4" "--gc-sections" "-Tlink.x"
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: rust-lld: error:
ERROR(cortex-m-rt): The interrupt vectors are missing.
Possible solutions, from most likely to less likely:
- Link to a svd2rust generated device crate
- Check that you actually use the device/hal/bsp crate in your code
- Disable the 'device' feature of cortex-m-rt to build a generic application (a dependency
may be enabling it)
- Supply the interrupt handlers yourself. Check the documentation for details.
rust-lld: error:
ERROR(cortex-m-rt): The interrupt vectors are missing.
Possible solutions, from most likely to less likely:
- Link to a svd2rust generated device crate
- Check that you actually use the device/hal/bsp crate in your code
- Disable the 'device' feature of cortex-m-rt to build a generic application (a dependency
may be enabling it)
- Supply the interrupt handlers yourself. Check the documentation for details.
rust-lld: error:
ERROR(cortex-m-rt): The interrupt vectors are missing.
Possible solutions, from most likely to less likely:
- Link to a svd2rust generated device crate
- Check that you actually use the device/hal/bsp crate in your code
- Disable the 'device' feature of cortex-m-rt to build a generic application (a dependency
may be enabling it)
- Supply the interrupt handlers yourself. Check the documentation for details.
In turned out I needed to:
- pull in the interrupt vector table (basically a list of memory addresses, where each entry says: “if interrupt X fires, jump to this address”
- and then use it in the main function (
_pacvariable): this forces the linker to include the crate and claims ownership of the hardware peripherals. This is Rust’s way of enforcing at compile/runtime that only one part of the code controls the hardware, to prevent conflicts like two functions trying to configure the same pin differently.
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // Panic handler
// ==== START ADDED
use rp2040_hal::pac; // Pull in the interrupt vector table
#[unsafe(link_section = ".boot2")]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
// ==== END ADDED
#[entry]
fn main() -> ! {
// ==== START ADDED
let _pac = pac::Peripherals::take().unwrap();
// ==== END ADDED
loop {
// Delay for approximately 1 million CPU cycles
cortex_m::asm::delay(1_000_000);
}
}
Now, we run again:
➜ embedded-programming-week3 git:(main) ✗ cargo run --bin delay
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `elf2uf2-rs -d target/thumbv6m-none-eabi/debug/delay`
Found pico uf2 disk /Volumes/RPI-RP2
Transfering program to pico
8.50 KB / 8.50 KB [===================================================================================================================] 100.00 % 91.22 KB/s
I successfully transferred everything to the Pico but obviously we don’t see anything because it’s just a delay.
Program #2: Blink the onboard-LED#
I decided to first write a program that blinks the onboard LED on the Raspberry Pi Pico H board with the RP2040 microcontroller. This way I can actually verify that my program works without having to use any additional LEDs or peripherals .
Code#
the program is sort of similar but effectively accesses the LED pin, and sets it high and low with delays in between.
I didn’t actually calculate the exact time for the delay but just played around until I could see that the time delay for the blinking was what I wanted.
The code below is explained with comments:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use rp2040_hal::{
gpio::Pins,
pac,
sio::Sio,
};
use embedded_hal::digital::v2::OutputPin;
#[unsafe(link_section = ".boot2")]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
#[entry]
fn main() -> ! {
// get a singleton struct representing all the RP2040's
// hardware peripherals (timers, GPIO banks, SPI, etc.)
let mut pac = pac::Peripherals::take().unwrap();
// Initialize the Single-cycle I/O block.
// On the RP2040, GPIO reads/writes go through the SIO,
// it's a fast, single-clock-cycle interface between the CPU
// cores and GPIO pins
let sio = Sio::new(pac.SIO);
// Create a struct with all 30 GPIO pins (.gpio0 through .gpio29),
// each in an unconfigured state
let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
// GPIO25 is the onboard LED on the Pico board
let mut led_pin = pins.gpio25.into_push_pull_output();
loop {
// LED on for x time (6_000_000 instruction cycles, dependent on the freq of the processor)
led_pin.set_high().unwrap();
cortex_m::asm::delay(6_000_000);
// LED off for 2x time
led_pin.set_low().unwrap();
cortex_m::asm::delay(12_000_000);
}
}
Flashing the firmware#
now we compile and flash it to the connected Pico in BOOTSEL mode:
➜ embedded-programming-week3 git:(main) ✗ cargo run --bin blinky
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `elf2uf2-rs -d target/thumbv6m-none-eabi/debug/blinky`
Found pico uf2 disk /Volumes/RPI-RP2
Transfering program to pico
24.00 KB / 24.00 KB [================================================================================================================] 100.00 % 145.42 KB/s
Result#
Yay, now it blinks:

Program #3: Cycle three external LEDs#
now we’ll write a program that connects three LEDs in series with 220 ohm resistors on three of the GPIO pins of the Pico and turns them on one after the other (along with the on-board LED, so that we can test our circuit’s not wrong).
here’s a picture of how we’ve connected things. as you can see the cathodes of the LEDs are to the ground rail, which is connected to pin number eight of the RP2040. GPIO pins 2, 3 and 4 (or pin out 4, 5, and 6 using the layout in the datasheet) are each connected to the resistor, which is in turn connected to the anode of the LED.

Code#
Here is our code now for this program, which simply extends the logic of the previous program to access the new GPIO pins now we’re using and turn them on and off in sequence:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use rp2040_hal::{
gpio::Pins,
pac,
sio::Sio,
};
use embedded_hal::digital::v2::OutputPin;
#[unsafe(link_section = ".boot2")]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let sio = Sio::new(pac.SIO);
let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
// GPIO25 is the onboard LED on the Pico board
let mut led0 = pins.gpio25.into_push_pull_output();
let mut led1 = pins.gpio2.into_push_pull_output();
let mut led2 = pins.gpio3.into_push_pull_output();
let mut led3 = pins.gpio4.into_push_pull_output();
loop {
led0.set_high().unwrap();
cortex_m::asm::delay(6_000_000);
led0.set_low().unwrap();
led1.set_high().unwrap();
cortex_m::asm::delay(6_000_000);
led1.set_low().unwrap();
led2.set_high().unwrap();
cortex_m::asm::delay(6_000_000);
led2.set_low().unwrap();
led3.set_high().unwrap();
cortex_m::asm::delay(6_000_000);
led3.set_low().unwrap();
}
}
Flashing the firmware#
Now let’s compile and flash it as usual (after updating the Cargo.toml to add the tree blink bin directive):
➜ embedded-programming-week3 git:(main) ✗ cargo run --bin threeblink
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `elf2uf2-rs -d target/thumbv6m-none-eabi/debug/threeblink`
Found pico uf2 disk /Volumes/RPI-RP2
Transfering program to pico
37.50 KB / 37.50 KB [================================================================================================================] 100.00 % 135.17 KB/s
Result#
and now it works!

Program #4: Using a button to speed up LEDs#
Now let’s extend the program that we wrote and add a button to the circuit that will speed up how fast the LEDs cycle. we will connect one end of the button to a GPIO pin, and the other to the ground rail.
Code#
Here’s the program below. We’ll use an interrupt, I.e. a capability of the ERP2040 which allows us to change the speed as soon as the button is pressed. Because if we were to detect the button press only at the beginning, that would be a problem, and some button presses would be missed.
- To use an interrupt we must use a name that is the exact name expected by the RP2040’s interrupt vector table (IO_IRQ_BANK0) and we use a Rust macro to specify that as an interrupt (
#[interrupt]) - We call
let button = pins.gpio28.into_pull_up_input();to make the GPIO28 a pull-up, i.e., by activating its internal resistance - We call
button.set_interrupt_enabled(EdgeLow, true);to trigger the interrupt when we press the button - Note that we use critical sections and atomic variables since this is a shared variable, although I doubt that is a relevant problem in this particular case
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use rp2040_hal::{
gpio::{self, Pins},
pac::{self, interrupt},
sio::Sio,
};
use embedded_hal::digital::v2::OutputPin;
use core::cell::RefCell;
use core::sync::atomic::{AtomicU32, Ordering};
use critical_section::Mutex;
use rp2040_hal::gpio::Interrupt::EdgeLow;
#[unsafe(link_section = ".boot2")]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
// Atomic counter for speed level — shared between interrupt and main loop.
// Atomics don't need a Mutex, so the main loop can read this cheaply.
static SPEED: AtomicU32 = AtomicU32::new(0);
// The button pin needs to be accessible from the interrupt handler
// so we can clear the interrupt flag.
type ButtonPin = gpio::Pin<gpio::bank0::Gpio28, gpio::FunctionSioInput, gpio::PullUp>;
static BUTTON_PIN: Mutex<RefCell<Option<ButtonPin>>> = Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let sio = Sio::new(pac.SIO);
let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
// LED outputs
let mut led0 = pins.gpio25.into_push_pull_output();
let mut led1 = pins.gpio2.into_push_pull_output();
let mut led2 = pins.gpio3.into_push_pull_output();
let mut led3 = pins.gpio4.into_push_pull_output();
// Button on GP28 with internal pull-up, other leg to GND (pin 34)
let button = pins.gpio28.into_pull_up_input();
// Trigger interrupt on falling edge (button press pulls pin low)
button.set_interrupt_enabled(EdgeLow, true);
// Move the button pin into the global so the interrupt handler can access it
critical_section::with(|cs| {
BUTTON_PIN.borrow(cs).replace(Some(button));
});
// Enable the GPIO interrupt in the NVIC (do this last)
unsafe {
pac::NVIC::unmask(pac::Interrupt::IO_IRQ_BANK0);
}
// 4 speed levels: slow -> medium -> fast -> fastest
let delays: [u32; 4] = [6_000_000, 3_000_000, 1_500_000, 750_000];
loop {
let d = delays[SPEED.load(Ordering::Relaxed) as usize];
led0.set_high().unwrap();
cortex_m::asm::delay(d);
led0.set_low().unwrap();
let d = delays[SPEED.load(Ordering::Relaxed) as usize];
led1.set_high().unwrap();
cortex_m::asm::delay(d);
led1.set_low().unwrap();
let d = delays[SPEED.load(Ordering::Relaxed) as usize];
led2.set_high().unwrap();
cortex_m::asm::delay(d);
led2.set_low().unwrap();
let d = delays[SPEED.load(Ordering::Relaxed) as usize];
led3.set_high().unwrap();
cortex_m::asm::delay(d);
led3.set_low().unwrap();
}
}
#[interrupt]
fn IO_IRQ_BANK0() {
// Access the button pin to check and clear the interrupt
critical_section::with(|cs| {
if let Some(button) = BUTTON_PIN.borrow(cs).borrow_mut().as_mut() {
if button.interrupt_status(EdgeLow) {
// Cycle speed: 0 -> 1 -> 2 -> 3 -> 0
let current = SPEED.load(Ordering::Relaxed);
SPEED.store((current + 1) % 4, Ordering::Relaxed);
// Clear the interrupt so it doesn't fire again immediately
button.clear_interrupt(EdgeLow);
}
}
});
}
We also add the critical-section crate to clear up some IDE errors telling us that it’s not present.
cargo add critical-section
Flashing the firmware#
Now we flash the program:
➜ embedded-programming-week3 git:(main) ✗ cargo run --bin speedup
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
Running `elf2uf2-rs -d target/thumbv6m-none-eabi/debug/speedup`
Found pico uf2 disk /Volumes/RPI-RP2
Transfering program to pico
50.00 KB / 50.00 KB [================================================================================================================] 100.00 % 129.41 KB/s
➜ embedded-programming-week3 git:(main) ✗
Result#
And yay it works!

Appendix: Instructions for Running Rust Programs on RP2040#
(repeating from group assignment)
1. Rust Setup#
For minimal setup, you need to install the compiler rustc.
We’ll use the rustup installer which installs a whole set of associated things (such as the cargo package manager), by following https://rust-lang.org/tools/install/
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update
To verify, run the following commands:
➜ embedded-programming-week3 git:(main) ✗ rustc --version
rustc 1.86.0 (05f9846f8 2025-03-31)
2. Compiling sample program main.rs#
As you can see, running rustc with the main.rs file above as input generates a binary file called main with executable permissions (i.e. the x in -rwxr-xr-x@)
➜ embedded-programming-week3 git:(main) ✗ ls
main.rs
➜ embedded-programming-week3 git:(main) ✗ rustc main.rs
➜ embedded-programming-week3 git:(main) ✗ ls -lah
total 944
drwxr-xr-x 4 shaazahm staff 128B Feb 15 15:13 .
drwxr-xr-x@ 18 shaazahm staff 576B Feb 15 14:51 ..
-rwxr-xr-x@ 1 shaazahm staff 466K Feb 15 15:13 main
-rw-r--r-- 1 shaazahm staff 501B Feb 15 15:08 main.rs
On Linux, this is an ELF file. For RP2040’s architecture, we need to specify a target (shown later).
rustc main.rs # → Generates ELF on Linux
# → Generates Mach-O on macOS
# → Generates PE on Windows
3. Running main.rs#
Just call the binary created from the terminal.
➜ embedded-programming-week3 git:(main) ✗ ./main
Hello world!
4. Compiling for the RP2040#
To compile for embedded platforms, you have to specify special target parameters to the compiler:
# → Generates ARM Cortex-M0+ ELF (for RP2040)
rustc main.rs --target thumbv6m-none-eabi
From https://doc.rust-lang.org/beta/rustc/platform-support/thumbv6m-none-eabi.html, we can see that it’s the:
Bare-metal target for CPUs in the Armv6-M architecture family, supporting a subset of the [T32 ISA].
Processors in this family include the:
- Arm Cortex-M0
- Arm Cortex-M0+
- Arm Cortex-M1
In practice we use the cargo package manager instead of rustc to build this target if we configure it right.
5. Compiling the program (using cargo)#
We will use the cargo package manager to configure cargo to build for the ARM Cortex-M0+ target.
Step 1: Verify cargo is installed#
We use the cargo package manager to build our app for the ARM Cortex-M0+ target. We installed this earlier with rustup, verify it is correctly installed
➜ embedded-programming-week3 git:(main) ✗ cargo --version
cargo 1.86.0 (adf9b6ad1 2025-02-28)
Step 2: Initialize cargo package#
From your project directory run:
➜ embedded-programming-week3 git:(main) ✗ cargo init
Creating binary (application) package
This will create a Cargo.toml file where you can display your project dependencies, etc.
[package]
name = "embedded-programming-week3"
version = "0.1.0"
edition = "2024"
[dependencies]
[[bin]]
name = "embedded-programming-week3"
path = "main.rs"
See available configurations here: https://doc.rust-lang.org/cargo/reference/manifest.html
Step 3: Configure ARM-Cortex M0+ compilation target#
Then install the ARM-Cortex M0+ specific target that will tell the compiler how to output the program binary for our RP2040:
➜ embedded-programming-week3 git:(main) ✗ rustup target add thumbv6m-none-eabi
info: downloading component 'rust-std' for 'thumbv6m-none-eabi'
info: installing component 'rust-std' for 'thumbv6m-none-eabi'
Then, we initialize the .cargo/config.toml where configure some config for cargo itself that are not package-scoped (see all options here):
mkdir -p .cargo && touch .cargo/config.toml
Update the config file so that the contents are as below, to configure our current repo to compile for the ARM Cortex M0+ target:
[build]
target = "thumbv6m-none-eabi"
This sets the default target when you run cargo build to the architecture of the RP2040.
Step 4: Compile (attempt #1)#
Let’s try build our Hello World program for the target:
➜ embedded-programming-week3 git:(main) ✗ cargo build
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
error[E0463]: can't find crate for `std`
|
= note: the `thumbv6m-none-eabi` target may not support the standard library
= note: `std` is required by `embedded_programming_week3` because it does not declare `#![no_std]`
error: cannot find macro `println` in this scope
--> main.rs:2:5
|
2 | println!("Hello world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
error: requires `sized` lang_item
For more information about this error, try `rustc --explain E0463`.
error: could not compile `embedded-programming-week3` (bin "embedded-programming-week3") due to 4 previous errors
Whoops, we get a compile error. What’s happening here is that the thumbv6m-none-eabi target is a bare-metal embedded target without an operating system, so it doesn’t support the standard library functions such as println!
Step 5: Adapting code for RP2040 target#
We need to do a few things:
- Add
#![no_std]to the top ofmain.rsto use Rust without the stdlib - Tell Rust we’re bringing our own
main()entry point (because main requires the stdlib):- Adding
#![no_main]to the top ofmain.rs - Adding
#[entry]above themain()to tell Rust that we’ll bring our own entry point
- Adding
- Add required dependencies (packages or ‘crates’):
cortex-m-rt: Startup code and minimal runtime for Cortex-M microcontrollers. See documentation in https://docs.rs/cortex-m-rt/latest/cortex_m_rt/panic-haltwhich halts the program on exception. Other options include panic-abort (reset the RP2040), panic-semihosting (print via debug probe), panic-rtt (print via rtt)cortex-m: for low-level access to Cortex-M processors, just for our example code
- Update
main()function: embedded programs are usually written to run forever- update signature of entry point:
fn main() -> ! {(i.e. never returns) - have a loop forever or until some termination condition
loop {}
- update signature of entry point:
Updated program:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // Panic handler
#[entry]
fn main() -> ! {
// e.g.
loop {
// Delay for approximately 1 million CPU cycles
cortex_m::asm::delay(1_000_000);
}
}
To write a meaningful program, we need to add more dependencies, which we’ll explore in the individual assignment.
Install dependencies:
cargo add cortex-m-rt
cargo add panic-halt
cargo add cortex-m
If you view the Cargo.toml file, you will see the new dependencies:
[package]
name = "embedded-programming-week3"
version = "0.1.0"
edition = "2024"
[dependencies]
cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
panic-halt = "1.0.0"
[[bin]]
name = "embedded-programming-week3"
path = "main.rs"
See available configurations here: https://doc.rust-lang.org/cargo/reference/manifest.html
Step 6: Compile (attempt #2)#
Now, let’s compile the program.
➜ embedded-programming-week3 git:(main) ✗ cargo build
Downloaded rustc_version v0.2.3
Downloaded vcell v0.1.3
Downloaded void v1.0.2
Downloaded semver v0.9.0
Downloaded volatile-register v0.2.2
Downloaded semver-parser v0.7.0
Downloaded nb v0.1.3
Downloaded bare-metal v0.2.5
Downloaded nb v1.1.0
Downloaded bitfield v0.13.2
Downloaded embedded-hal v0.2.7
Downloaded cortex-m v0.7.7
Downloaded 12 crates (276.4 KB) in 0.12s
Compiling semver-parser v0.7.0
Compiling nb v1.1.0
Compiling void v1.0.2
Compiling cortex-m v0.7.7
Compiling vcell v0.1.3
Compiling bitfield v0.13.2
Compiling volatile-register v0.2.2
Compiling nb v0.1.3
Compiling embedded-hal v0.2.7
Compiling semver v0.9.0
Compiling rustc_version v0.2.3
Compiling bare-metal v0.2.5
Compiling embedded-programming-week3 v0.1.0 (/Users/shaazahm/workspace/hq-fabacademy/shaaz-ahmed/embedded-programming-week3)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.29s
Step 7: Testing the program#
This turned out to be tricky. The binary we compiled for the RP2040 won’t obviously work on the Mac. And we can’t compile for the Mac because the programs are written differently for bare-metal microcontrollers.
Options for testing:
- Run the program directly on the hardware: straightforward, preferred in this ecosystem
- Unit tests to test the logic of the program: it doesn’t give full confidence, but Rust has traits, i.e. interfaces, and we could mock out the traits for the RP2040 HAL (hardware abstraction layer)
- Emulate RP2040. There are programs out there that emulate different hardware architectures, listed below. However, someone noted: “emulators have very limited value for MCU work since almost all of the work is related to peripherals and pins”. Some emulators:
- QEMU: can emulate some ARM Cortex-M processors, but RP2040 is unsupported.
- Renode: RP2040 support is marked work-in-progress and frozen, see https://github.com/matgla/Renode_RP2040
- Wokwi (nice!): a browser based emulator that supports RP2040, but Rust compilation is only supported via the VSCode Extension: https://docs.wokwi.com/vscode/getting-started
6. Transferring program to flash memory#
Now that in step 6 we compiled the program for the RP2040 target, to run the program on the RP2040, we have to write it to the Pico flash memory. We will explore two ways to do this.
Option 1: using picotool#
The most straightforward.
# Build the code
cargo build --release
# Flash directly (no file format conversion needed!)
picotool load target/thumbv6m-none-eabi/release/embedded-programming-week3
# Can even reboot into BOOTSEL programmatically
picotool reboot -f -u # Force reboot into USB mode
Option 2: using elf2uf2-rs and copy into USB drive#
We will configure cargo to use the elf2uf2-rs application to convert the built program’s ELF file to UF2 format for copying over USB. Eventually running it all to convert and copy over the files, and the RP2040 will automatically reboot.
Step 1: configure cargo to use elf2uf2-rs#
First we install the elf2uf2-rs tool that will convert the ELF file generated by the compiler into the UF2 file that we can use to copy over to the flash drive:
cargo install elf2uf2-rs --locked
Update the .cargo/config.toml file so that the contents are as below:
[build]
target = "thumbv6m-none-eabi"
[target.thumbv6m-none-eabi]
runner = "elf2uf2-rs -d"
Step 2: compiling & writing program to flash memory#
Then we boot the RP2040 into “USB Bootloader mode”, by rebooting whilst holding the “BOOTSEL” button (explained in previous section).
Then we can just run with cargo run --bin program-name, which will compile the code and started the specified ‘runner’ in .cargo/config.toml. As the ‘runner’ is the elf2uf2-rs tool, it will build an ELF file, use the call the runner with ELF file which will convert it to UF2 file and copy it to the RP2040.