Embedded Rust - STM32F103

In this chapter, we'll be looking at programming an STM32F103 (STM32F103C8T6), or Blue Pill board. This board is relatively cheap and packs a lot of power. The STM32 development board is equipped with an ARM Cortex M3 processor. The full specifications can be found here: https://stm.com.

If you are looking for details on the STM32F401, or Black Pill board, check this chapter: Embedded Rust - STM32F401.

Getting prepared

Hardware

In order to build your first embedded project you need the following:

  • One or more STM32F103C8T6 development boards; these are also known as Blue Pill boards,
  • ST-Link V2 USB Debugger,
  • A breadboard with some common components; look for a bundle:
    • Jumper wire cables,
    • Some LEDs, resistors and push buttons.

It also helps to have some soldering gear.

If you are not soldering the pins to the STM32 board, do make sure the connections to the breadboard are solid and stable. You can easily lose a few hours debugging your source code, before figuring our that one of the connections is broken, or unstable.

Software

First we'll install probe-rs which is a tool to flash and debug embedded devices.

If you have cargo-binstall installed, you can install probe-rs with:

$ cargo binstall probe-rs

If not, you can build from source with:

$ cargo install probe-rs --locked --features cli

I had to restart my terminal to get the probe-rs command to work, i.e. to be found in the path. It should be in ~/.cargo/bin/probe-rs.

Rust ARM target

We'll also need the build toolchain for the ARM Cortex M3 processor. This includes the ARM cross-compile tools, OpenOCD and the Rust ARM target.

Make sure you have the thumbv7m-none-eabi target installed. We'll use the rustup tool to install it:

$ rustup target add thumbv7m-none-eabi

Connect the STM32

In order to program the STM32 board, you need to connect it to your PC using the ST-Link USB adapter. You should wire it like this:

STM32ST-Link v2
V3 = Red3.3v (Pin 8)
IO = OrangeSWDIO (Pin 4)
CLK = BrownSWDCLK (Pin 2)
GND = BlackGND (Pin 6)
Schematic:

Connect SMT32


Once connected, you can use this command to establish the link and check that everything is working so far:

$ probe-rs list

The following debug probes were found:
[0]: STLink V2 (VID: 0483, PID: 3748, Serial: 49C3BF6D067578485335241067, StLink)
$ probe-rs info

Probing target via JTAG

ARM Chip:
Debug Port: Version 1, DP Designer: ARM Ltd
└── 0 MemoryAP
    └── ROM Table (Class 1)
        ├── Cortex-M3 SCS   (Generic IP component)
        │   └── CPUID
        │       ├── IMPLEMENTER: ARM Ltd
        │       ├── VARIANT: 1
        │       ├── PARTNO: 3107
        │       └── REVISION: 1
        ├── Cortex-M3 DWT   (Generic IP component)
        ├── Cortex-M3 FBP   (Generic IP component)
        ├── Cortex-M3 ITM   (Generic IP component)
        └── Cortex-M3 TPIU  (Coresight Component)

Unable to debug RISC-V targets using the current probe. RISC-V specific information cannot be printed.
Unable to debug Xtensa targets using the current probe. Xtensa specific information cannot be printed.

Probing target via SWD

ARM Chip:
Debug Port: Version 1, DP Designer: ARM Ltd
└── 0 MemoryAP
    └── ROM Table (Class 1)
        ├── Cortex-M3 SCS   (Generic IP component)
        │   └── CPUID
        │       ├── IMPLEMENTER: ARM Ltd
        │       ├── VARIANT: 1
        │       ├── PARTNO: 3107
        │       └── REVISION: 1
        ├── Cortex-M3 DWT   (Generic IP component)
        ├── Cortex-M3 FBP   (Generic IP component)
        ├── Cortex-M3 ITM   (Generic IP component)
        └── Cortex-M3 TPIU  (Coresight Component)

Debugging RISC-V targets over SWD is not supported. For these targets, JTAG is the only supported protocol. RISC-V specific information cannot be printed.
Debugging Xtensa targets over SWD is not supported. For these targets, JTAG is the only supported protocol. Xtensa specific information cannot be printed.

Quick start

We'll be using the excellent embassy crate to program the STM32. The embassy crate is a framework for building concurrent firmware for embedded systems. It is based on the async and await syntax, and is designed to be used with the no_stdenvironment.

Create a new hello_stm32_world project with the following files; take care to put them in the correct directories:

cargo new hello_stm32_world
cd hello_stm32_world

In the project root:

Cargo.toml

[package]
name = "hello_stm32_world"
version = "0.1.0"
edition = "2021"

[dependencies]
embassy-stm32 = { version = "0.1.0", features = ["defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any"] }
embassy-sync = { version = "0.5.0", features = ["defmt"] }
embassy-executor = { version = "0.5.0", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] }
embassy-time = { version = "0.3.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }
embassy-usb = { version = "0.1.0", features = ["defmt"] }
embassy-futures = { version = "0.1.0" }

defmt = "0.3"
defmt-rtt = "0.4"

cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
embedded-hal = "0.2.6"
panic-probe = { version = "0.3", features = ["print-defmt"] }
futures = { version = "0.3.17", default-features = false, features = ["async-await"] }
heapless = { version = "0.8", default-features = false }
nb = "1.0.0"

[profile.dev]
opt-level = "s"

[profile.release]
debug = 2

Next to the Cargo.toml, create a build script:

build.rs

fn main() {
    println!("cargo:rustc-link-arg-bins=--nmagic");
    println!("cargo:rustc-link-arg-bins=-Tlink.x");
    println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}

Then add a project-specific configuration file:

.cargo/config.toml

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# replace STM32F103C8 with your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip STM32F103C8"

[build]
target = "thumbv7m-none-eabi"

[env]
DEFMT_LOG = "trace"

And finally edit the src/main.rs file to look like:

src/main.rs

#![no_std]
#![no_main]

use defmt::info;
use embassy_executor::Spawner;
use embassy_stm32::Config;
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) -> ! {
    let config = Config::default();
    let _p = embassy_stm32::init(config);

    loop {
        info!("Hello STM32 World!");
        Timer::after_secs(1).await;
    }
}

Your project structure should look like this:

.
├── Cargo.toml
├── .cargo
│   └── config.toml
├── build.rs
└── src
    └── main.rs

First run

Now that you have the project set up, you can build and flash it to the STM32 board:

$ cargo run --release

After the standard build steps complete, you should see something like this:

      Erasing ✔ [00:00:00] [##########################################################################################################################################################] 14.00 KiB/14.00 KiB @ 24.64 KiB/s (eta 0s )
  Programming ✔ [00:00:00] [##########################################################################################################################################################] 14.00 KiB/14.00 KiB @ 18.79 KiB/s (eta 0s )    Finished in 1.332s

0.000000 TRACE BDCR configured: 00008200
└─ embassy_stm32::rcc::bd::{impl#2}::init @ /Users/mibes/.cargo/registry/src/index.crates.io-6f17d22bba15001f/embassy-stm32-0.1.0/src/fmt.rs:117 
0.000000 DEBUG rcc: Clocks { sys: Hertz(8000000), pclk1: Hertz(8000000), pclk1_tim: Hertz(8000000), pclk2: Hertz(8000000), pclk2_tim: Hertz(8000000), hclk1: Hertz(8000000), adc: Some(Hertz(1000000)), rtc: Some(Hertz(40000)) }
└─ embassy_stm32::rcc::set_freqs @ /Users/mibes/.cargo/registry/src/index.crates.io-6f17d22bba15001f/embassy-stm32-0.1.0/src/fmt.rs:130 
0.000030 INFO  Hello STM32 World!
└─ blinky_stm32::____embassy_main_task::{async_fn#0} @ src/main.rs:17  
1.000183 INFO  Hello STM32 World!
└─ blinky_stm32::____embassy_main_task::{async_fn#0} @ src/main.rs:17  

If you encounter any build issues, double-check that the config.toml and build.rs files are in the correct locations.

If you see the Hello STM32 World! message, you have successfully flashed the STM32 board with your first Rust program.

This project structure will be the foundation for the next steps in building more complex embedded projects. You may want to make a copy of this project structure to use as a template for future projects.

Running without the debugger.

Once loaded, your application is automatically executed when you power up the STM32 board, without the debugger attached. You can accomplish this in various ways:

  • Disconnect the brown and orange cables from the board (leaving the red and black attached). This will just draw power from the USB port, but won't establish the connection with the debugger.
  • Connect a USB charger to the USB port on the board. Make sure to disconnect the ST-LINK when doing this
  • Connect a 3.3v battery to the V3 and GND pins (red & black)

Next steps

Next we'll be building our first Blinky project using the breadboard and a few components.