Big Button - Part 8 - Rust Project Configuration
Tue, Jan 28, 2025
|12 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 to have your project set up 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 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
# Secrets files**/*secrets.*
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-M0 devices# Format: <ARCHITECTURE>-<PLATFORM_VENDOR>-<OPERATING_SYSTEM>-<APP_BINARY_INTERFACE>targets = ["thumbv6m-none-eabi"]
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
thumbv6m-none-eabi
for our Raspberry Pi Pico W microcontroller. - The format and values are defined in the table below.
Target Name Part | Explanation |
---|---|
<ARCHITECTURE> | thumbv6m |
thumb : Thumb instruction set, a compact 16-bit encoding for ARM processors (Link) | |
v6m : ARMv6-M architecture for Cortex-M0+ microcontrollers (RPi Pico W) (Link) | |
<PLATFORM_VENDOR> | none - No specific vendor. Bare-metal target |
<OPERATING_SYSTEM> | Not specified. Bare-metal. |
<APP_BINARY_INTERFACE> | eabi - Standard embedded ABI for ARM systems (Link) |
.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 = "thumbv6m-none-eabi" # Chip: Cortex-M0 and Cortex-M0+ (RP2040)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.thumbv6m-none-eabi]14 collapsed lines
runner = [ "probe-rs", "run", "--chip=RP2040", "--log-format", "{[{L}]%dimmed%bold} {{f:dimmed}:{l:dimmed}%30}: {s}",]# Custom flags for compiler for this targetrustflags = [ "-C", "link-args=-Tlink.x -Tlink-rp.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/manifest.html[package]6 collapsed lines
authors = ["Your Own Name"] # HEY YOU! UPDATE ME!edition = "2021" # Reference: 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 microcontrollerscortex-m-rt = "0.7" # Startup code and minimal runtime for Cortex-Mpanic-halt = "1.0" # Basic panic handler that halts on panicdefmt = "0.3" # Deferred formatting for embedded loggingdefmt-rtt = "0.4" # RTT (Real-Time Transfer) transport for defmt
# Debug probe panic handler with defmt supportpanic-probe = { version = "0.3", features = ["print-defmt"] }
# Embassy Rust embedded framework dependenciesembassy-executor = { git = "https://github.com/embassy-rs/embassy.git", branch = "main", features = [ "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "task-arena-size-98304",] }embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", branch = "main", features = [ "defmt", "unstable-pac", "time-driver", "critical-section-impl", "rp2040",] }embassy-time = { git = "https://github.com/embassy-rs/embassy.git", branch = "main", features = [ "defmt", "defmt-timestamp-uptime",] }embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", branch = "main", 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 - disabledpanic = "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: https://github.com/embassy-rs/embassy/blob/main/examples/rp/memory.x */
MEMORY { BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 RAM : ORIGIN = 0x20000000, LENGTH = 264K}
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 |
---|---|---|---|
BOOT2 | Second-stage bootloader | 0x10000000 | 0x100 (256 bytes) |
FLASH | Program flash storage | 0x10000100 | 2048K - 0x100 (~2MB) |
RAM | Working memory | 0x20000000 | 264K |
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 compilation//! 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 on any changes `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 with editing one last and existing 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)] // for development#![allow(unused_imports)] // 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 W and debug probe setup are both plugged into your computer’s USB port and run the following command.
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 4.54s Running `probe-rs run --chip=RP2040 --log-format '{[{L}]%dimmed%bold} {{f:dimmed}:{l:dimmed}%30}: {s}' target/thumbv6m-none-eabi/release/big-button` Erasing âś” [00:00:00] [##########################################################################################################################################] 12.00 KiB/12.00 KiB @ 63.22 KiB/s (eta 0s ) Programming âś” [00:00:00] [##########################################################################################################################################] 12.00 KiB/12.00 KiB @ 32.72 KiB/s (eta 0s ) Finished in 0.589s[INFO ] main.rs:22 : Hello World![INFO ] main.rs:25 : Goodbye World![INFO ] main.rs:22 : Hello World![INFO ] main.rs:25 : 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.
Phewww! Now that was something. I’m glad we came this far. Let’s move on and explain our software development workflow.