Embedded Rust - STM32F401
In this chapter, we'll be looking at programming an STM32F401 (STM32F401CCU6), or Black Pill board. This board is relatively cheap, comes with a USB-C connector and offers great development possibilities. The STM32 development board is equipped with an ARM Cortex-M4 processor. The full specifications can be found here: https://stm.com.
If you are looking for details on the STM32F103, or Blue Pill board, check this chapter: Embedded Rust - STM32F103.
Getting prepared
Hardware
In order to build your first embedded project, you need the following:
- One or more STM32F401CCU6 development boards; these are also known as Black 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 thumbv7em-none-eabi
target installed. We'll use the rustup
tool to install it:
$ rustup target add thumbv7em-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:
Pinout:
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-M4 SCS (Generic IP component)
│ └── CPUID
│ ├── IMPLEMENTER: ARM Ltd
│ ├── VARIANT: 0
│ ├── PARTNO: 3108
│ └── REVISION: 1
├── Cortex-M3 DWT (Generic IP component)
├── Cortex-M3 FBP (Generic IP component)
├── Cortex-M3 ITM (Generic IP component)
├── Cortex-M4 TPIU (Coresight Component)
└── Cortex-M4 ETM (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-M4 SCS (Generic IP component)
│ └── CPUID
│ ├── IMPLEMENTER: ARM Ltd
│ ├── VARIANT: 0
│ ├── PARTNO: 3108
│ └── REVISION: 1
├── Cortex-M3 DWT (Generic IP component)
├── Cortex-M3 FBP (Generic IP component)
├── Cortex-M3 ITM (Generic IP component)
├── Cortex-M4 TPIU (Coresight Component)
└── Cortex-M4 ETM (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]
edition = "2021"
name = "hello_stm32_world"
version = "0.1.0"
license = "MIT OR Apache-2.0"
[dependencies]
embassy-stm32 = { version = "0.1.0", features = ["defmt", "stm32f401cc", "unstable-pac", "memory-x", "time-driver-any", "exti", "chrono"] }
embassy-sync = { version = "0.5.0", features = ["defmt"] }
embassy-executor = { version = "0.5.0", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "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-net = { version = "0.4.0", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", ] }
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"
embedded-io = { version = "0.6.0" }
embedded-io-async = { version = "0.6.1" }
panic-probe = { version = "0.3", features = ["print-defmt"] }
[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 STM32F401CCUx with your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip STM32F401CCUx"
[build]
target = "thumbv7em-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::*; use embassy_executor::Spawner; use embassy_stm32::gpio::{Level, Output, Speed}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; #[embassy_executor::main] async fn main(_spawner: Spawner) { let p = embassy_stm32::init(Default::default()); info!("Hello STM32 World!"); let mut led = Output::new(p.PC13, Level::High, Speed::Low); loop { info!("high"); led.set_high(); Timer::after_millis(300).await; info!("low"); led.set_low(); Timer::after_millis(300).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] [############################################################################################################################################################################] 32.00 KiB/32.00 KiB @ 43.20 KiB/s (eta 0s )
Programming ✔ [00:00:00] [############################################################################################################################################################################] 21.00 KiB/21.00 KiB @ 32.46 KiB/s (eta 0s ) Finished in 1.41s
0.000000 TRACE BDCR ok: 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 flash: latency=0
└─ embassy_stm32::rcc::_version::init @ /Users/mibes/.cargo/registry/src/index.crates.io-6f17d22bba15001f/embassy-stm32-0.1.0/src/fmt.rs:130
0.000000 DEBUG rcc: Clocks { sys: Hertz(16000000), pclk1: Hertz(16000000), pclk1_tim: Hertz(16000000), pclk2: Hertz(16000000), pclk2_tim: Hertz(16000000), hclk1: Hertz(16000000), hclk2: Hertz(16000000), hclk3: Hertz(16000000), plli2s1_q: None, plli2s1_r: None, pll1_q: None, rtc: Some(Hertz(32000)) }
└─ embassy_stm32::rcc::set_freqs @ /Users/mibes/.cargo/registry/src/index.crates.io-6f17d22bba15001f/embassy-stm32-0.1.0/src/fmt.rs:130
0.000000 INFO Hello STM32 World!
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)