STM32 to Blinky with Rust – Part 2 – Development and Debug Toolchain

In the first part of this series, we connected the dev board and installed the probe-rs tool. This time we’ll create a basic project that prints “Hello, world!” over the debug interface. This should prove we have a valid compilation toolchain, can successfully flash our own firmware to a STM32F4 and then connect to it for debugging.

A screenshot of a successful compilation and flashing of a rust based, “Hello, world!”, firmware.

Since we’ll be building for the STM32F407, a Cortex-M4F chip, we need the correct rust target installed. This is done using the rustup command we installed in the previous post in the series.

$ rustup target add thumbv7em-none-eabihfCode language: Shell Session (shell)

There are a number of embedded Arm templates that can be used with the cargo generate tool, but the ones I tried were quite out of date and were not compatible with the RTT debug messages used by probe-rs. Instead, we’ll create our project from scratch, including only the bare minimum of files to get us up and running. If you feel ambitious, you can follow along with the snippets in this blog post, like a true child of the 80’s typing away at your Sinclair Spectrum. Alternatively, I’ve posted the completed code from this post to a repo on Codeberg, under the debug tag.

The repo can be cloned and the tag checked out by running these three commands.

$ git clone
$ git fetch --all --tags
$ git checkout tags/debug -b debug-branchCode language: Shell Session (shell)

To define the project you need a Cargo.toml manifest file. The package details like name, author and version aren’t important, but the dependencies section and a binary target are needed to get a compiled and linked firmware built.

# [...]

cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.3"
panic-rtt-target = "0.1.3"
rtt-target = "0.5.0"

name = "stm32-to-blinky"
test = false
bench = falseCode language: TOML, also INI (ini)

At the time of writing, these packages were up to date and the correct features had been enabled. I’ve found some older documentation and blog posts that worked with the then-current packages, but newer versions have both added and deprecated features that prevent compilation and linking.

Next up, we need a memory map for our chip. Our ST Discovery board has an STM32F407VGT6 so, referring to its datasheet, we know it has 1M of flash starting at 0x0800_0000 and 128K of RAM starting at 0x2000_0000.

The memory map for a STM32F407VGT6. The SRAM blocks are clearly visible but the Flash section is hidden in a wall of text, so has been highlighted.
  FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   : ORIGIN = 0x20000000, LENGTH =  128K
}Code language: plaintext (plaintext)

We need a build script to accompany our memory map to ensure the map is used by the linker, placing the appropriate sections of our binary at the correct locations. Your build script could get more complicated over time, but this is enough to get us going for now. All we need to build “Hello, world!” is to include the linker script from cortex-m-rt in the compiler’s link arguments.

fn main() {
}Code language: Rust (rust)

Rust will compile and link for the host system’s architecture by default, such as x86_64 or aarch64. We must, then, supply a config file to tell it to build for our STM32F4’s Cortex-M4F thumbv7em architecture instead. We can also tell cargo to allow its run command to flash and debug our chip with probe-rs by setting up the target’s runner.

target = "thumbv7em-none-eabihf"

runner = "probe-rs run --chip STM32F407VGTx" Code language: TOML, also INI (ini)

With all the project configuration out of the way, we can finally write our “Hello, world!” program. There are some important lines in the code to point out here.

One line 1, we’re telling rust to not use the standard library, which is important as without an OS running on the chip there won’t be a standard library available. Line 2 tells rust not to emit a “main” entrypoint, which is only useful when operating systems try to launch your program. Instead, on line 9, we indicate where our entrypoint is. This way we tell the linker which function needs to be placed at the matching location in the STM32’s memory.


use cortex_m as _;
use cortex_m_rt::entry;
use rtt_target::{rtt_init_print, rprintln};
use panic_rtt_target as _;

fn main() -> ! {
    rprintln!("Hello, world!");

    loop {}
Code language: Rust (rust)

The rtt libraries need a “critical section” to be linked or they will, in turn, fail to link. The cortex_m package is included on line 4 so that its critical section implementation is linked but, as we don’t directly use any of its contents, we can discard the result of the import.

The final two lines of interest are 10 and 14. The method signature -> ! indicates that the function never returns and the infinite loop {} ensures that it will never do so.

With all the pieces in place we can now build our binary. Running cargo build will download and compile all of our dependencies, along with our code, and output our program formatted for Cortex-M4F chips, linked to match our STM32F4’s memory map.

$ cargo build
   Compiling cortex-m v0.7.7
   Compiling cortex-m-rt v0.7.3
   Compiling rtt-target v0.5.0
   Compiling stm32-to-blinky v0.1.0 (/home/mike/projects/stm32-to-blinky)
   Compiling panic-rtt-target v0.1.3
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.47sCode language: Shell Session (shell)

If at this point you get a linker error instead of success, there’s a chance you’re missing some of the binary tools needed to manipulate compiled code for other target architectures. Adding the llvm-tools component gives us versions of the tools which support all of the architectures that rust itself supports.

$ rustup component add llvm-toolsCode language: Shell Session (shell)

Now that we’ve successfully built the project, we can deploy it to our bare-metal chip. The previously configured cargo run command will flash the program to the STM32F407VGT6 and attach the debugger, ready to receive output from the chip. Finally, “Hello, world!” appears on our console.

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s          
     Running `probe-rs run --chip STM32F407VGTx target/thumbv7em-none-eabihf/debug/stm32-to-blinky`
      Erasing ✔ [00:00:01] [##########################] 16.00 KiB/16.00 KiB @ 12.20 KiB/s (eta 0s )
  Programming ✔ [00:00:06] [###########################] 16.00 KiB/16.00 KiB @ 2.51 KiB/s (eta 0s )
    Finished in 7.904s                                                           
Hello, world! Code language: Shell Session (shell)

Done! Part 2 is complete! Building on part 1, we’ve now got a toolchain installed, the hardware connected, a project built, the binary flashed to the chip and a message returned via the debugger. In part 3 we need to access the GPIO pins on the STM32F407VGT6 and blink the attached LEDs on the STM32F407G-DISC1 Discovery board.





2 responses to “STM32 to Blinky with Rust – Part 2 – Development and Debug Toolchain”

  1. […] connected the debugger and confirmed we can see the ARM chip ready for programming. Next we need to spin up a small project and send it to the […]

  2. […] part of this series, we connected the dev board and installed the probe-rs tool. In part 2, we created a basic project that compiled and built successfully, flashed it to the chip and […]

Leave a Reply

Your email address will not be published. Required fields are marked *