Table of Contents
Introduction
💻 HACKER Ok let’s dive into setting up our new baby Rust project and get it off the ground! This post will guide you through setting up your project to start adding Rust configuration and logic specifically for a embedded project. By the end of this post, we will be able to run a short “Hello world” within our Raspberry Pi Pico 2 W microcontroller!

Adding Files and Configurations
We will now edit and add a variety of configuration files. Some of these files are general to a software project, while some are very specific to our embedded Rust project.
There will be many more files and directories on our journey here within this project. This will get us to a good point where we can start adding custom logic and functionality.
NOTE
Each one of these files and configurations can be an entire blog article on its own. Here we are attempting to give you a good knowledge starting point for this project and in case you want to dive in deeper.
README.md
Documentation is important for any project. As a minimum, let’s add a markdown file to the root of our directory such that anyone can understand what this project is about and how to interpret it.
We will add a README.md
to our project’s root directory (same level as Cargo.toml
) with
the following content:
# big-button
Just some interesting embedded Rust tutorial series I found floating aroundthe internet that I wanted to try.
Run me with `cargo run --release`
.gitignore
Not all files and directories are meant to be version controlled and stored within our remote
GitHub repository. For example, typically we do not want any Rust compiled binaries (within target/
),
specific IDE configurations, or secrets/passwords send up to GitHub.com.
Let’s add the following to our existing .gitignore
file. This is a good set of
gitignore rules for most Rust projects.
# Generated by Cargo - will have compiled files and executablesdebug/target/vendor/
# Removing Cargo.lock from gitignore if creating an executable, leave it for libraries# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.htmlCargo.lock
# These are backup files generated by rustfmt**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information*.pdb
# General Items.DS_StoreThumbs.db*.log*.tmp*.swp*.env*.bak
# IDE Specific Configurations.idea/.vscode/*.sublime-project*.sublime-workspace.projectSession.vim.Session.vim
.rustfmt.toml
This configuration file defines how our code is automatically formatted when running
cargo fmt
. By keeping a shared set of formatting rules in the project, we ensure
that everyone working on the codebase follows the same style. This removes debates
over code style, makes diffs easier to read, and keeps the project consistent
and professional.
Here are some sensible default formatting values for our project. Note that our expanded width of 120 characters allows us to have a little more width without stacking the code vertically.
# Rust Formatter Configurations
max_width = 120 # Maximum line width before wrappingtrailing_comma = "Vertical" # Put trailing commas on vertical lists
rust-toolchain.toml
Ok. Here we are getting to the serious stuff!
As very briefly mentioned before, a Rust toolchain is a single installation of the Rust compiler and its associated tooling. Just think of a Rust toolchain as a separate Rust instance.
Add the following to a new file named rust-toolchain.toml
within the root of your
project directory.
[toolchain]
# Specifies using the nightly version of Rust, which has the latest experimental featureschannel = "nightly"
# Lists the Rust components to include in the toolchaincomponents = ["rust-src", "rust-std", "rustc-dev", "cargo", "rustfmt", "clippy"]
# Defines the cross-compilation target for bare-metal ARM Cortex-M33 (ARMv8-M) devices# Format: <ARCHITECTURE>-<PLATFORM_VENDOR>-<OPERATING_SYSTEM>-<APP_BINARY_INTERFACE>targets = ["thumbv8m.main-none-eabihf"] # "hf" indicates floating-point support
This file that we just added will specify which Rust toolchain should be used for this project and which components/tools will the toolchain have.
Ok let us define what each one of these entries really mean and what we specified here.
-
channel
- The Rust programming language releases to three different channels:
stable
,beta
, andnightly
. - A full explanation of Rust release channels can be seen here
- We select
nightly
due to its recent developments in the embedded ecosystem. Sorry for this short explanation, this is a larger discussion that is related embedded device dependence on#![no_std]
, which will make sense later.
- The Rust programming language releases to three different channels:
-
components
- This is a list of available components/tools to be added to the Rust toolchain.
- A full list of available components and their description can be found here
-
targets
- This specifies where (what platform) our Rust program will ultimately run.
- We select
thumbv8m.main-none-eabihf
for our Raspberry Pi Pico 2 W microcontroller. - The format and values are defined in the table below.
- Note that the
hf
at the end refers to single-precision floating-point support on the RP2350 chip.
Target Name Part | Explanation |
---|---|
<ARCHITECTURE> | thumbv8m.main |
thumb : Thumb instruction set, a compact 16-bit encoding for ARM processors (Link) | |
v8m : ARMv8-M architecture for Cortex-M33 microcontrollers (RP2350) (Link) | |
main : Main extension profile with full instruction set support | |
<PLATFORM_VENDOR> | none - No specific vendor. Bare-metal target |
<OPERATING_SYSTEM> | Not specified. Bare-metal. |
<APP_BINARY_INTERFACE> | eabihf - Embedded ABI with hardware with floating-point (hf ) support for RP2350 chip |
NOTE
Note that when running cargo run
, the compile target thumbv8m.main-none-eabihf
will be read from rust-toolchain.toml
installing any tools, if needed. However,
we can explicitly install these compile tools with rustup target add thumbv8m.main-none-eabihf
.
.cargo/config.toml
By default and with a typical Rust project, cargo
will know how to handle running
the project. That is, it can infer what architecture to compile for,
what compiler options to use, and so on.
Embedded projects require more guidance. Cargo will not inherently know how
to handle our project. For example, cargo
has to know
where the code will have to run (the target platform), what tool we will be
used to flash our program onto the microcontroller (probe-rs), and so forth.
Here we are adding configurations for our cargo
tool. We are telling cargo
what to do when we ultimately run cargo run
.
Let’s create the directory and file for these configurations. Run the following in the terminal and within your project’s root directory.
# Create the directory for our cargo configuration filemkdir -p .cargo
# Create a blank cargo configuration filetouch .cargo/config.toml
Add the following configurations to the config.toml
file we just created.
(Expand lines of code to view full file)
# Reference: https://doc.rust-lang.org/cargo/reference/config.html
############################################################################### Build and Compiler Settings# Reference: https://doc.rust-lang.org/cargo/reference/config.html#build[build]4 collapsed lines
target = "thumbv8m.main-none-eabihf" # Chip: Cortex-M33 (ARMv8-M) (RP2350)rustc = "rustc" # Rust Compilerrustdoc = "rustdoc" # Rust Documentation Generatorjobs = 1 # Number of parallel jobs to run
############################################################################### Run Specification for Spcific Target Platform# Reference: https://doc.rust-lang.org/cargo/reference/config.html#target[target.thumbv8m.main-none-eabihf]9 collapsed lines
runner = [ "probe-rs", "run", "--chip=RP235x", "--log-format", "[{t}] {[{L}]%dimmed%bold} {{f:dimmed}:{l:dimmed}%30}: {s}",]# Custom flags for compiler for this targetrustflags = ["-C", "link-args=-Tlink.x -Tdefmt.x", "-C", "no-vectorize-loops"]
############################################################################### Environmental Variables# Reference: https://doc.rust-lang.org/cargo/reference/config.html#env[env]2 collapsed lines
# Logger Formatting: https://defmt.ferrous-systems.com/custom-log-outputDEFMT_LOG = { value = "debug", force = true }
Please review the comments within the following configuration file
to get familiar on what is being set. For complete documentation on cargo
configurations, visit this link.
Cargo.toml
This file is arguably the most important file in a Rust project.
Cargo.toml
is like a recipe book for your Rust project. It tells Rust what
ingredients (dependencies) your code needs to run. This manifest file
defines the project’s meta data, dependencies, runner profiles, and much more.
We should already have the Cargo.toml
file in our project, so let’s edit it.
Copy and paste the following into this file before we explain the contents.
######################################################################################### This Project/Package Metadata# Reference: https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table[package]6 collapsed lines
authors = ["YOUR OWN NAME"] # HEY YOU! UPDATE ME!edition = "2024" # Rust code edition: https://doc.rust-lang.org/edition-guide/rust-2021/index.htmllicense = "MIT"name = "big-button"version = "0.1.0" # This project versionreadme = "README.md"
######################################################################################### Rust program dependencies# Reference: https://doc.rust-lang.org/cargo/guide/dependencies.html[dependencies]31 collapsed lines
cortex-m = "0.7" # Core support for ARM Cortex-M microcontrollers (M33 for RP2350)cortex-m-rt = "0.7" # Startup code and minimal runtime for Cortex-Mpanic-halt = "1.0" # Basic panic handler that halts on panicdefmt = "1.0" # Deferred formatting for embedded loggingdefmt-rtt = "1.0" # RTT (Real-Time Transfer) transport for defmt
# Debug probe panic handler with defmt supportpanic-probe = { version = "1.0", features = ["print-defmt"] }
# Embassy Rust embedded framework dependenciesembassy-embedded-hal = { version = "0.5", features = ["defmt"] }embassy-executor = { version = "0.9", features = [ "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt",] }embassy-rp = { version = "0.8", features = [ "critical-section-impl", "time-driver", "defmt", "unstable-pac", "rp235xb", "binary-info",] }embassy-time = { version = "0.5", features = [ "defmt", "defmt-timestamp-uptime",] }embassy-sync = { version = "0.7", features = ["defmt"] }embassy-futures = { version = "0.1", features = ["defmt"] }
######################################################################################### Compiler profile for when running cargo run --release# Reference: https://doc.rust-lang.org/cargo/reference/profiles.html[profile.release]9 collapsed lines
codegen-units = 1 # Number of codegen units - 1debug = 2 # Debug info included in compiled binary - 0: none, 1: line numbers, 2: fulldebug-assertions = false # Debug assertion - disabledincremental = false # Incremental compilation - disabledlto = 'fat' # Link-time optimization - fat: Perform optimization across all cratesopt-level = "s" # Optimize compile - s: optimize for binary sizeoverflow-checks = true # Integer overflow checks - enabledpanic = "abort" # Panic strategy - abort: terminate process upon panicrpath = false # Relative path - disabled
Let’s discuss what each section here is and why it is so special.
Section | Description | Details | Documentation |
---|---|---|---|
[package] | Defines project/package metadata | How to identify and publish the project | Link |
[dependencies] | External libraries/packages used | Each used package defined by name, version, and/or source location (cargo.io, git, local) | Link |
[profile] | Compiler settings and optimizations | Alters behavior based on build type (e.g. --release ). Useful for different environments like development versus production | Link |
NOTE
In order to successfully run our starter embedded project on the microcontroller
we are adding dependencies cortex-m
, dfmt
, and embassy
.
In subsequent posts we will go in to much more details why these are there and
why we added them the way we did. For now, please review the comments left
within the Cargo.toml
file.
memory.x
We want to tell our Raspberry Pi Pico W microcontroller how its memory will generally be laid out. That is, we want to specify where different types of memory (ie. FLASH, RAM) start and how big they are. This file is ultimately processed at flashing of the microcontroller.
Add the following to a new file named memory.x
within the root of your
project directory.
/* memory.x - Tell linker how much memory is available and where it is *//* Note that these values are specific to our microcontroller *//* Source (Thank you!): https://github.com/embassy-rs/embassy/blob/main/examples/rp/memory.x */
MEMORY {20 collapsed lines
/* * The RP2350 has either external or internal flash. * * 2 MiB is a safe default here, although a Pico 2 has 4 MiB. */ FLASH : ORIGIN = 0x10000000, LENGTH = 2048K /* * RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping. * This is usually good for performance, as it distributes load on * those banks evenly. */ RAM : ORIGIN = 0x20000000, LENGTH = 512K /* * RAM banks 8 and 9 use a direct mapping. They can be used to have * memory areas dedicated for some specific job, improving predictability * of access times. * Example: Separate stacks for core0 and core1. */ SRAM8 : ORIGIN = 0x20080000, LENGTH = 4K SRAM9 : ORIGIN = 0x20081000, LENGTH = 4K}
SECTIONS {12 collapsed lines
/* ### Boot ROM info * * Goes after .vector_table, to keep it in the first 4K of flash * where the Boot ROM (and picotool) can find it */ .start_block : ALIGN(4) { __start_block_addr = .; KEEP(*(.start_block)); KEEP(*(.boot_info)); } > FLASH
} INSERT AFTER .vector_table;
/* move .text to start /after/ the boot info */_stext = ADDR(.start_block) + SIZEOF(.start_block);
SECTIONS {16 collapsed lines
/* ### Picotool 'Binary Info' Entries * * Picotool looks through this block (as we have pointers to it in our * header) to find interesting information. */ .bi_entries : ALIGN(4) { /* We put this in the header */ __bi_entries_start = .; /* Here are the entries */ KEEP(*(.bi_entries)); /* Keep this block a nice round size */ . = ALIGN(4); /* We put this in the header */ __bi_entries_end = .; } > FLASH} INSERT AFTER .text;
SECTIONS {10 collapsed lines
/* ### Boot ROM extra info * * Goes after everything in our program, so it can contain a signature. */ .end_block : ALIGN(4) { __end_block_addr = .; KEEP(*(.end_block)); } > FLASH
} INSERT AFTER .uninit;
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
The comprehensive nature and logic behind the contents of this memory.x
file is beyond
the scope of this tutorial. However, let’s roughly define what we see in this file.
Region | Short Definition | Start Memory Address | Length |
---|---|---|---|
FLASH | Program flash storage | 0x10000000 | 2048K (~2MB) |
RAM | Working memory (striped banks 0-7) | 0x20000000 | 512K |
SRAM8 | Direct-mapped memory bank 8 | 0x20080000 | 4K |
SRAM9 | Direct-mapped memory bank 9 | 0x20081000 | 4K |
BOOT2 | N/A - RP2350’s Boot ROM is smarter | N/A | N/A |
build.rs
build.rs
is a special Rust build script that runs before compiling your project.
For embedded projects, it’s often used to:
- Generate linker scripts
- Set up memory layouts
- Process hardware description files
- Configure chip-specific settings
It runs on the host machine (not the target device) during compilation. Hence, we have std
tools/code available for processing.
//! This build script runs during code complilation//! Runs on host computer, where `std` Rust tools are available here
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());
// Re-run for any changes to `memory.x` and `build.rs` println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=memory.x");}
src/main.rs
Let’s finalize our changes by editing one last file.
Remember, in this blog tutorial step we are mainly concerned on laying out and testing the project base setup and configuration. Hence, we will save the discussion on what these dependencies are or what they do for a subsequent post.
However, generally speaking, all we are doing here is have our defmt
logger
display out a message every one second to our terminal. (Sorry no fancy LED or push button action
in this post yet. The princess is in another castle :-/
)
#![no_std]#![no_main]#![allow(unused_variables)] // only used for development#![allow(unused_imports)] // only used for development
use defmt::*;use defmt_rtt as _;use panic_probe as _;
use embassy_executor::Spawner;use embassy_rp::config::Config;use embassy_rp::gpio;use embassy_time::Timer;
#[embassy_executor::main]async fn main(spawner: Spawner) { // Initialize system with proper clock configuration let config = Config::default(); let _peripherals = embassy_rp::init(config);
// Start an infinite loop loop { info!("Hello World!"); Timer::after_secs(1).await;
info!("Goodbye World!"); Timer::after_secs(1).await; }}
Summary
Here is what our project currently looks like. Ensure your project’s directory and its containing files look just like this.
.├── build.rs├── .cargo│  └── config.toml├── Cargo.toml├── .git├── .gitignore├── memory.x├── README.md├── rust-toolchain.toml└── src   └── main.rs
Ensure that you copied or typed out the entire files listed above. Make sure you didn’t accidentally forget a closing brace or something stupid small like that.
3, 2, 1, Take off!
If we did everything correctly so far, we should be able to run everything without issues.
Ensure that your Raspberry Pi Pico 2 W and the debug probe setup are both plugged into
your computer’s USB port and connecting (probe-rs list
)
Run the following command in the terminal and within the project’s root directiory:
cargo run --release
After some compiling and running, you should get something like this as terminal output:
.... Compiling big-button v0.1.0 (/home/you/projects/big-button) Finished `release` profile [optimized + debuginfo] target(s) in 0.77s Running `probe-rs run --chip=RP235x --log-format '[{t}] {[{L}]%dimmed%bold} {{f:dimmed}:{l:dimmed}%30}: {s}' target/thumbv8m.main-none-eabihf/release/big-button` Erasing âś” 100% [####################] 12.00 KiB @ 52.50 KiB/s (took 0s) Finished in 0.62s[0.000358] [INFO ] main.rs:23 : Hello World![1.000402] [INFO ] main.rs:26 : Goodbye World![2.000413] [INFO ] main.rs:23 : Hello World![3.000425] [INFO ] main.rs:26 : Goodbye World![4.000437] [INFO ] main.rs:23 : Hello World![5.000449] [INFO ] main.rs:26 : Goodbye World!

Whenever you are ready to stop the running program, you can either
press CTRL+c
on your Keyboard (preferred),
or even just pull the USB cable to the Pico and debug probe out of
the computer USB port (discouraged).
NOTE
Just because you are not seeing the logs being streamed to your computer terminal, does not mean the code/logic is not still running on your microcontroller. The whole point here is to have the microcontroller independent from the computer.
Phewww! Now that was something. I’m glad we came this far. Let’s move on and explain our software development workflow.