A first glimpse at the code
First, let’s consider the Cargo.toml:
[package]
name = "pico-simple"
version = "0.1.0"
edition = "2024"
[dependencies]
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
cortex-m-rt = { version = "0.7.0" }
cortex-m-rt-macros = "0.7.5"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
defmt = "1.0.1"
defmt-rtt = "1.0.0"
And src/main.rs:
#![no_std]
#![no_main]
use cortex_m::asm;
pub use cortex_m_rt::entry;
use defmt::info;
use {defmt_rtt as _, panic_probe as _};
#[unsafe(link_section = ".start_block")]
#[unsafe(no_mangle)]
static BOOT_ROM_INFO: [u8; 44] = [
0xd3, 0xde, 0xff, 0xff, 0x42, 0x01, 0x21, 0x10, 0xff, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x79, 0x35, 0x12, 0xab, 0xf2, 0xeb, 0x88, 0x71,
0x6c, 0x93, 0x02, 0x10, 0x7c, 0x93, 0x02, 0x10, 0x70, 0xf6, 0x02, 0x10,
0x90, 0xa3, 0x1a, 0xe7, 0x1f, 0x01, 0x02, 0x03,
];
#[entry]
fn main() -> ! {
for _ in 1..100000 {
asm::nop();
}
info!("Hello world!");
loop {
asm::wfi();
}
}
Hopefully this structure will get clearer as we proceed.
Flash it
cargo run to flash your code.
You may get a message that the firmware of the pico probe is outdated. In that case, follow these instructions.
What will be sent to the flash memory
rustc compiles a binary in the ELF format, but probe-rs will convert this to flat binary before flashing it to the Pico. We can also do this conversion ourselves to learn what will be flashed to the Pico using the LLVM tools objcopy:
cargo objcopy --bin pico-simple -- --strip-all -O binary flat.bin
You can then open this with an hex editor. If you have installed the hex editor extension on VSCode, you can use it:
code flat.bin
Also, if you have picotool installed, you can boot the Pico in BOOTSEL mode (hold the button and power on/hard reset the Pico) and run picotool save -a ./pico_dump.bin to obtain what is currently stored in the flash and open and compare with your local file. Since the debug probe is also a Pico, you can also do that with it to see what’s stored in the debug probe flash.
Linker script
Take a look at the memory.x file that is a linker script used to tell the linker how to lay out the memory of our binary, it is extending the configs at cortex-m-rt repo.
I won’t go into the detail of how this works (for that, see this), but encourage you to play with it, changing values and seeing how it affects the binary in the hex editor and also seeing how this is called in the build.rs file.
Startup code
An ordinary ARM flat binary will not work with RPi Pico 2, even though it has the vector table at the beginning and a proper entrypoint and stack start point. The RPi Pico 2 has a lot of possibilities to configure the start code (partitions and images, we won’t go into details here), so it looks for those configurations, instead of going straight to the vector table entrypoint. So in order to drop a minimal startup config that just tells to go the vector table at the begging of the flash memory, we have:
#[unsafe(link_section = ".start_block")]
#[unsafe(no_mangle)]
static BOOT_ROM_INFO: [u8; 44] = [
0xd3, 0xde, 0xff, 0xff, 0x42, 0x01, 0x21, 0x10, 0xff, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x79, 0x35, 0x12, 0xab, 0xf2, 0xeb, 0x88, 0x71,
0x6c, 0x93, 0x02, 0x10, 0x7c, 0x93, 0x02, 0x10, 0x70, 0xf6, 0x02, 0x10,
0x90, 0xa3, 0x1a, 0xe7, 0x1f, 0x01, 0x02, 0x03,
];
Forcing it to be in the .start_block section, makes sure that it’s the first 4kB of the flash where the Pico expects that configuration be.