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:
STM32 | ST-Link v2 |
---|---|
V3 = Red | 3.3v (Pin 8) |
IO = Orange | SWDIO (Pin 4) |
CLK = Brown | SWDCLK (Pin 2) |
GND = Black | GND (Pin 6) |
Schematic:
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_std
environment.
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
andGND
pins (red & black)
Next steps
Next we'll be building our first Blinky project using the breadboard and a few components.