Skip to Content
Course content

183: Cross-Compiling for Microcontrollers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.toml file in the root of the project.
  • Set the default build target to thumbv7em-none-eabihf.
  • Add the necessary rustflags to ensure the linker uses the link.x script.
  • Verify that cargo build no longer requires the --target flag to proceed to the linking stage.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.