Big Button - Part 8 - Rust Project Configuration

Sun, Aug 17, 2025

|

14 min read

|

|

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!

cool relevant image
Figure: Our Rust rocket being worked on and getting fully decked out

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:

README.md
# big-button
Just some interesting embedded Rust tutorial series I found floating around
the 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.

.gitignore
# Generated by Cargo - will have compiled files and executables
debug/
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.html
Cargo.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_Store
Thumbs.db
*.log
*.tmp
*.swp
*.env
*.bak
# IDE Specific Configurations
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
.project
Session.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 wrapping
trailing_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.

rust-toolchain.toml
[toolchain]
# Specifies using the nightly version of Rust, which has the latest experimental features
channel = "nightly"
# Lists the Rust components to include in the toolchain
components = ["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.

Target Name PartExplanation
<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.

Terminal window
# Create the directory for our cargo configuration file
mkdir -p .cargo
# Create a blank cargo configuration file
touch .cargo/config.toml

Add the following configurations to the config.toml file we just created. (Expand lines of code to view full file)

.cargo/config.toml
# 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 Compiler
rustdoc = "rustdoc" # Rust Documentation Generator
jobs = 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 target
rustflags = ["-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-output
DEFMT_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.

Cargo.toml
########################################################################################
# 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.html
license = "MIT"
name = "big-button"
version = "0.1.0" # This project version
readme = "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-M
panic-halt = "1.0" # Basic panic handler that halts on panic
defmt = "1.0" # Deferred formatting for embedded logging
defmt-rtt = "1.0" # RTT (Real-Time Transfer) transport for defmt
# Debug probe panic handler with defmt support
panic-probe = { version = "1.0", features = ["print-defmt"] }
# Embassy Rust embedded framework dependencies
embassy-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 - 1
debug = 2 # Debug info included in compiled binary - 0: none, 1: line numbers, 2: full
debug-assertions = false # Debug assertion - disabled
incremental = false # Incremental compilation - disabled
lto = 'fat' # Link-time optimization - fat: Perform optimization across all crates
opt-level = "s" # Optimize compile - s: optimize for binary size
overflow-checks = true # Integer overflow checks - enabled
panic = "abort" # Panic strategy - abort: terminate process upon panic
rpath = false # Relative path - disabled

Let’s discuss what each section here is and why it is so special.

SectionDescriptionDetailsDocumentation
[package]Defines project/package metadataHow to identify and publish the projectLink
[dependencies]External libraries/packages usedEach used package defined by name, version, and/or source location (cargo.io, git, local)Link
[profile]Compiler settings and optimizationsAlters behavior based on build type (e.g. --release). Useful for different environments like development versus productionLink

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
/* 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.

RegionShort DefinitionStart Memory AddressLength
FLASHProgram flash storage0x100000002048K (~2MB)
RAMWorking memory (striped banks 0-7)0x20000000512K
SRAM8Direct-mapped memory bank 80x200800004K
SRAM9Direct-mapped memory bank 90x200810004K
BOOT2N/A - RP2350’s Boot ROM is smarterN/AN/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:

It runs on the host machine (not the target device) during compilation. Hence, we have std tools/code available for processing.

build.rs
//! 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 :-/ )

src/main.rs
#![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:

Terminal window
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!
cool relevant image

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.