Getting started with the STM32F411VE

Here are some notes on how to set up a development environment to work on this project. YMMV.

I recommend following this video tutorial to get started.

We'll use the STM32F411VE as our target system.

Setting up your development environment

  1. Install Rust 1.31 or newer.
  2. Add cortex-m targets to your toolchain. We are using Cortex-M4F, so: rustup target add thumbv7em-none-eabihf
  3. Read the Embedded Rust Book in its entirety. Just kidding, but it's a good idea to bookmark it.
  4. If you're using VSCode or one of its derivatives, install the rust-analyzer extension and the Cortex-Debug extension. There are some more details here for advanced configurations.

Setting up the project

Configuring the compiler target

A target triple is a string that describes the target architecture, vendor, operating system, and environment.

<arch><subarch>-<vendor>-<sys>-<env>
  • arch is the architecture, e.g. arm, x86, aarch64, etc.
  • subarch is the subarchitecture, e.g. v7, v8, v9, etc.
  • vendor is the vendor, e.g. none, apple, nvidia, intel, etc.
  • sys is the operating system, e.g. none, linux, windows, macos, etc.
  • env is the environment, e.g. eabihf, gnu, musl, etc.

Both the MSP432 and the STM32F411E use the Cortex-M4F with a floating point unit. This chip is part of the ARMv7e-M architecture family.

The rustc docs show that the target triple for ARMv7E-M is thumbv7em-none-eabi or thumbv7em-none-eabihf.

  • The thumbv7em prefix indicates that our Cortex-M4F uses the Thumb-2 instruction set and the ARMv7E-M architecture.
  • The none vendor indicates that the target does not have an operating system.
  • The eabi or eabihf suffix indicates the ABI to use. Since we have an FPU, we use the eabihf variant.

Now that we know the target triple, we can add it to the cargo toolchain on our host machine and in our .cargo/config.toml file to tell the compiler how to build code for the project's target.

# add the target architecture to the toolchain
rustup target add thumbv7em-none-eabihf

And to make sure the correct target and flags are used every cargo build, we can add the following to our .cargo/config.toml file so we don't have to type it out every time.

[build]
target = "thumbv7em-none-eabihf"     # Cortex-M4F and Cortex-M7F (with FPU)

[target.thumbv7em-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]

But the IDE might still complain about failing to build the project for the host architecture. We can instruct the IDE extensions to only build for the embedded target by adding some extra configs. The following is an example for VSCode.

// .vscode/settings.json
{
    "rust-analyzer.cargo.allTargets": false,
    "rust-analyzer.cargo.target": "thumbv7em-none-eabihf"
}

The cortex-m-quickstart also includes a build.rs file that sets up the target triple and the linker script, accounting for some of the edge cases that can happen when building to make sure that the linker actually sees the memory.x file and stuff. Be careful though, because the build.rs file also sets the rustflags so we have to remove them from the .cargo/config.toml file if we use the build.rs file.

Configuring the Cortex-M crates

The embedded Rust tools make it easy to build and run programs on the target but require a little extra setup to get running for our specific target.

The first crate to set up is the runtime crate, cortex-m-rt. This crate provides the entry! macro, which is used to mark the entry point of the program, and interrupt handling.

// src/main.rs
#![no_std]
#![no_main]

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {
        // your code goes here
    }
}

The cortex-m-rt crate expects a memory.x file that specifies the flash and RAM memory layouts of the target. According to the datasheet, the flash memory is at 0x0800 0000 - 0x0807 FFFF and the RAM is at 0x2000 0000 - 0x2002 0000.

stm32f411ve memory map

Another thing that is required by the cortex-m-rt runtime is a panic handler. A panic handler accepts a PanicInfo argument and never returns. This code will run when the program (our code or a dependency) panics. It is required!

#![allow(unused)]
#![no_std]

fn main() {
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // do something
}
}

There are more advanced ways to handle panics, including a number of third-party crates, but for now we'll just set up a basic one: panic-halt. This crate makes an infinite loop happen when a panic occurs, which can be helpful when debugging. We should replace it later. Now we're not actually using the crate's functions, but we need to declare it so the compiler knows it exists.

// src/main.rs
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    loop {
        // your code goes here
    }
}

Flashing the device

There are a few more toolchains to install for ergonomics. llvm-tools enables some low-level debugging and inspection features, and cargo-binutils is a more ergonomic wrapper around llvm-tools.

rustup component add llvm-tools
cargo install cargo-binutils

Here's an example of how to use cargo-binutils to inspect the binary.

❯ cargo size -- -Ax
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
ahab_stm32f11ve  :
section                size         addr
.vector_table         0x400    0x8000000
.text                  0x70    0x8000400
.rodata                   0    0x8000470
.data                     0   0x20000000
.gnu.sgstubs              0    0x8000480
.bss                      0   0x20000000
.uninit                   0   0x20000000
.debug_abbrev        0x145d          0x0
.debug_info         0x23eef          0x0
.debug_aranges       0x1338          0x0
.debug_ranges       0x1b720          0x0
.debug_str          0x3d91c          0x0
.comment               0x99          0x0
.ARM.attributes        0x3a          0x0
.debug_frame         0x4218          0x0
.debug_line         0x2184d          0x0
.debug_loc             0x29          0x0
Total               0xa5691

One last tool is needed to flash the device. probe-rs is a tool that allows you to interact with embedded devices, including flashing. Installation can vary by OS, see probe-rs docs for details. If you're on Windows, you might need to install some drivers.

Let's see if probe-rs supports the STM32F411VE.

❯ probe-rs chip info stm32f411ve
stm32f411ve
Cores (1):
    - main (Armv7em)
NVM: 0x08000000..0x08080000 (512.0 kiB)
RAM: 0x20000000..0x20020000 (128.0 kiB)
NVM: 0x1fffc000..0x1fffc004 (4 B)
NVM: 0x1fff7800..0x1fff7a10 (528 B)

Yep! Now we want to use cargo embed to flash the device. cargo-embed is the big brother of cargo-flash. It can also flash a target just like cargo-flash, but it can also open an RTT terminal as well as a GDB server.

❯ cargo embed --chip stm32f411ve
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
      Config default
      Target E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve
      Erasing ✔ 100% [####################]  16.00 KiB @  40.12 KiB/s (took 0s)
  Programming ✔ 100% [####################]   2.00 KiB @   4.08 KiB/s (took 0s)
     Finished in 0.49s
        Done processing config default

We can cache the chip info so we don't have to type it out every time using an Embed.toml file that cargo embed will look for in the project root.

# Embed.toml
chip = "stm32f411ve"

Now we can run cargo embed and it will pull the arguments from the config.

❯ cargo embed
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
      Config default
      Target E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve
      Erasing ✔ 100% [####################]  16.00 KiB @  40.25 KiB/s (took 0s)
  Programming ✔ 100% [####################]   2.00 KiB @   4.15 KiB/s (took 0s)
     Finished in 0.48s
        Done processing config default

😎