Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
183: Cross-Compiling for Microcontrollers
I've spent way too many afternoons staring at a terminal, wondering why my code compiled perfectly but refused to actually link, all because I treated a microcontroller like it was just another Linux server. When you first move into the embedded world with Rust, the instinct is to treat cross-compilation as a one-line command. You've probably already used rustup target add for WASM or maybe a different OS, so it feels natural to assume that's the whole story.
The "Just Add the Target" Trap
The naive approach looks like this: you decide to target an STM32F4 (which uses the thumbv7em-none-eabihf architecture), you run rustup target add thumbv7em-none-eabihf, and then you try to build your project with cargo build --target thumbv7em-none-eabihf. On the surface, this looks correct. Rust's compiler, rustc, is a beast—it can generate the machine code for that ARM Cortex-M4 core without breaking a sweat.
But then you hit a wall. You'll see a cryptic error from the linker—something about ld not finding a specific symbol or, more commonly, the linker simply failing because it's trying to use your host machine's linker (like the one for macOS or Ubuntu) to create a binary for a chip that doesn't even have an operating system. I remember the first time this happened; I thought I had a corrupted toolchain installation. In reality, I was asking my Mac's linker to build a binary for a piece of silicon that doesn't know what a "Mach-O" file is.
Why the host linker fails you
Here is the distinction that often trips people up: compilation and linking are different stages. rustup gives you the compiler (the part that turns Rust into assembly), but it doesn't provide the linker (the part that organizes that assembly into a final binary image the hardware can actually boot). For bare-metal microcontrollers, you need a cross-linker—usually something from the GNU Arm Embedded Toolchain—that knows exactly how to layout the memory for a specific chip's Flash and RAM.
If you keep going the naive route, you'll end up manually typing out massive --target flags every time you build, and you'll be manually managing paths to arm-none-eabi-gcc in your shell environment. It's brittle, it's tedious, and it's a nightmare for anyone else trying to clone your repo.
The professional way: Project-local configuration
The better way is to move that configuration out of your head and into the project. I always use a .cargo/config.toml file. This tells Cargo: "Whenever I'm in this directory, use these specific settings, regardless of what the global defaults are." Instead of fighting the CLI, you define the target and the linker right there in the repo.
# .cargo/config.toml
[build]
target = "thumbv7em-none-eabihf" # No more --target flags!
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=-Tlink.x", # Tell the linker to use our memory layout script
]
By specifying the link.x script (which is usually provided by a Board Support Package or a HAL crate), you're telling the linker exactly where the stack starts and where the interrupt vector table lives in the chip's memory. I also highly recommend using probe-rs. Instead of manually building a binary and using an external tool to flash it, probe-rs integrates with Cargo. You just run cargo run, and it handles the compilation, linking, flashing, and opens a RTT console for your logs in one go. It turns the "cross-compilation headache" into a seamless experience that feels like developing for a desktop.
📋 Practical Task
Exercise: Configuring a Bare-Metal Build Pipeline for the STM32F401
You have been given a basic project structure for an STM32F401 microcontroller. The code compiles, but the build fails because the linker is attempting to use the host system's linker, and the --target flag is missing from the build command.
Your goal: Setup the project so that a simple cargo build (without any flags) correctly targets the ARM Cortex-M4F architecture and utilizes a linker script.
- Create a
.cargo/config.tomlfile in the root of the project. - Set the default build target to
thumbv7em-none-eabihf. - Add the necessary
rustflagsto ensure the linker uses thelink.xscript. - Verify that
cargo buildno longer requires the--targetflag to proceed to the linking stage.
There are no comments for now.