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!

cool relevant image
Figure: Our Rust rocket being worked on and getting all riced 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
# 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.

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

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

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 = "thumbv6m-none-eabi" # Chip: Cortex-M0 and Cortex-M0+ (RP2040)
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.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 target
rustflags = [
"-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-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/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.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
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 = "0.3" # Deferred formatting for embedded logging
defmt-rtt = "0.4" # RTT (Real-Time Transfer) transport for defmt
# Debug probe panic handler with defmt support
panic-probe = { version = "0.3", features = ["print-defmt"] }
# Embassy Rust embedded framework dependencies
embassy-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 - 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 - disabled
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: 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.

RegionShort DefinitionStart Memory AddressLength
BOOT2Second-stage bootloader0x100000000x100 (256 bytes)
FLASHProgram flash storage0x100001002048K - 0x100 (~2MB)
RAMWorking memory0x20000000264K

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 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 :-/ )

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

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 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!
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.

Phewww! Now that was something. I’m glad we came this far. Let’s move on and explain our software development workflow.