Preface

This article aims to explain the basics to get you developing on STM32 devices using Rust language. This tutorial takes place in a linux machine, precisely Ubuntu.

Why Rust?

Rust claims to be an interesting alternative for embedded developing for having, among others, characteristics such as:

  • Powerful static analysis: Enforce pin and peripheral configuration at compile time, guaranteeing that resources won’t be used by unintended parts of your application.
  • Flexible memory: Dynamic memory allocation is optional. Use a global allocator and dynamic data structures. Or leave out the heap altogether and statically allocate everything.
  • Fearless concurrency: Rust makes it impossible to accidentally share state between threads. Use any concurrency approach you like, and you’ll still get Rust’s strong guarantees.

What will you need?

  • A PC with the Rust Toolchain installed
  • A board with a STM32 device

In this case, I’ll be working with the NUCLEO-F103RB development board containing a STM32F103RB MCU. This board already comes with an onboard ST-LINK debugger and programmer, but you can use an external one if needed.

Installations

As said before, all the steps described is this tutorial are for a machine running linux (Ubuntu).

Rust

Start by making sure you have curl installed

sudo apt install curl

Then run the following to install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, you can check your Rust compiler version with

rustc -V

Rust installation already comes with Cargo, the Rust’s package manager that will be used to download Rust package’s dependencies, compile the packages, make them distributable and upload them to crates.io.

For bandwidth and disk usage concerns, the default installation only supports native compilation. To add cross compilation support for the Arm Cortex-M architectures, you’ll have to install them separately.

For Cortex-M0, M0+, and M1 (ARMv6-M architecture):

rustup target add thumbv6m-none-eabi

For Cortex-M3 (ARMv7-M architecture):

rustup target add thumbv7m-none-eabi

For Cortex-M4 and M7 without hardware floating point (ARMv7E-M architecture):

rustup target add thumbv7em-none-eabi

And finally, for Cortex-M4F and M7F with hardware floating point (ARMv7E-M architecture):

rustup target add thumbv7em-none-eabihf

Cargo binary utils

Make sure you have the build-essential package installed, which is necessary for having the C compiler used by Cargo

sudo apt install build-essential

Then you can install cargo-binutils

cargo install cargo-binutils
rustup component add llvm-tools-preview

Cargo flash

Cargo flash will be used to program the microcontroller. Before installing it, you must first install the libusb package

sudo apt install libusb-1.0-0-dev

And finally, for cargo flash

cargo install cargo-flash

Creating the project

Let’s create a simple blink project. By running the following command, a new Rust project will be created in a new folder

cargo init blink && cd blink

Configurations

Before writing any code, you must first configure the project so that it compiles for your specific target and uses the correct linker script. Since I’m working with a Cortex M3, I must configure it for the thumbv7m-none-eabi target. This can be done by creating a file .cargo/config in your project root.

# .cargo/config

[build]
# Always compile for the instruction set of the STM32F1
target = "thumbv7m-none-eabi"

# use the Tlink.x scrip from the cortex-m-rt crate
rustflags = [ "-C", "link-arg=-Tlink.x"]

The next thing we need to do is create the linker script responsible for telling the linker about the memory layout of the device. It must be created in the project root and named memory.x. The following configuration is for the STM32F103RB MCU which has 128 Kbytes of Flash memory and 20 Kbytes of SRAM. You must adapt it to your device if it’s the case.

/* memory.x - Linker script for the STM32F103RB */

MEMORY
{
    /* Flash memory begins at 0x80000000 and has a size of 128kB*/
    FLASH : ORIGIN = 0x08000000, LENGTH = 128K
    /* RAM begins at 0x20000000 and has a size of 20kB*/
    RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

Now, you need to configure the Cargo.toml file already present in your project root. This is the file where all the dependencies (crates) used in your project must be specified.

For this project in particular, copy the following content to the file

# Cargo.toml
[package]
name = "blink"
version = "0.1.0"
edition = "2018"

[profile.release]
opt-level = 'z' # turn on maximum optimizations. We only have 64kB
lto = true      # Link-time-optimizations for further size reduction

[dependencies]
cortex-m = "^0.6.3"       # Access to the generic ARM peripherals
cortex-m-rt = "^0.6.12"   # Startup code for the ARM Core
embedded-hal = "^0.2.4"   # Access to generic embedded functions (`set_high`)
panic-halt = "^0.2.0"     # Panic handler

# Access to the STM32F103 HAL.
[dependencies.stm32f1xx-hal]
# STM32F103RB contains a 128kB flash variant which is called "medium density"
features = ["stm32f103", "rt", "medium"]
version = "^0.6.1"

Main project

And finally, for the main.rs file, a simple blink program

// src/main.rs

// std and main are not available for bare metal software
#![no_std]
#![no_main]

use panic_halt as _;

use cortex_m_rt::entry;
use embedded_hal::digital::v2::OutputPin;
use stm32f1xx_hal as hal;
use hal::{pac, delay::Delay, prelude::*};

#[entry]
fn main() -> ! {
    /* Get access to device and core peripherals */
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();

    /* Get access to RCC, AFIO and GPIOA */
    let mut rcc = dp.RCC.constrain();
    let mut flash = dp.FLASH.constrain();
    let mut gpioa = dp.GPIOA.split(&mut rcc.apb2);

    /* Set up LED pin */
    let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.crl);

    /* Set up sysclk and freeze it */
    let clocks = rcc.cfgr.sysclk(8.mhz()).freeze(&mut flash.acr);

    /* Set up systick delay */
    let mut delay = Delay::new(cp.SYST, clocks);

    loop {
        /* Light show */
        led.set_high().ok();
        delay.delay_ms(1_000_u16);
        led.set_low().ok();
        delay.delay_ms(1_000_u16);
    }
}

Building and Flashing

To build the project in release mode

cargo build --release

Now, you are ready to flash the program. With the device connected and the ST-LINK recognized, flash it by specifying the device with

cargo flash --chip stm32f103rb --release

Go further

Some links used as reference that will definitely be useful on your projects:

Comments