The toolchain demo for Nucleo-144


  • You have installed the Rust and Drone-OS toolchain as discussed in this previous post: Drone-OS toolchain on Ubuntu 18.04.
  • You have one of these STM32 Nucleo-144 developer boards: NUCLEO-L496ZG, or the newer NUCLEO-L4R5ZI.
  • You have a Black Magic Probe adapter. It is possible to flash the binary with the onboard ST-LINK, but the scripts in this demo project are made to work with the Black Magic Probe (BMP).

What is this demo good for?

We are going to write and debug a trivial LED-blink firmware targeted for the STM32L4 Cortex-M MCU. No GUI-based IDE, no cloud tools, just the beautifully transparent command-line environment of Drone-OS’ toolchain! As you will see in later posts: Drone-OS gives you access to the full power of the Cortex-M core and peripherals in a safe manner. Of course, it has its price: this developer environment does not hide massively the inherent complexity of the target hardware by means of high abstraction. In that respect, Drone-OS’ developer environment is much more bare-bones. You are challenged to read the hardware user guides and the hardware reference manuals of your target MCU!

Watch the smooth fade-in, fade-out of the LED lights. It’s coded in Rust!

Create the project

Create a workspace directory. Change into it and use cargo to create the standard project directory structure for a new project drone-os-toolchain-demo:

cargo new drone-os-toolchain-demo
cd drone-os-toolchain-demo
cargo run

Those commands create, compile and link a ‘Hello, world!’ application that runs on the local system. That’s nice but didn’t we want to write a firmware for the NUCLEO-144 and run it on our developer board? Let’s get the files of a complete demo firmware from Github instead.

Get the LED-blink Demo from Github

Go to your workspace directory and clone th project from public Github. Before you execute ‘git clone’, remove or rename any existing directory with the name drone-os-toolchain-demo.

git clone

Let’s inspect the the project’s file structure.

HINT: All file file paths names in this post are relative to the project root.

A word about src/

You will probably start at src/ if you wonder how the source code of this project looks like, You will see those extern “C” parts in the code. Why “C”, not “Rust”? Well, this main module allocates some application binary interface (ABI) data structures and declares the functions for the board reset and startup. The “C” part defines how to call the function at the assembly level. The “C” ABI is supported by Cortex-M natively, so it is the appropriate selection in our coding for interfacing with the hardware core. Quoting:
“While Rust can call C functions without overhead, the Rust compiler still needs to know about the existence of those functions. To tell the compiler you have to declare those functions in a extern “C” { .. } block.”

The purpose of this short post is not to discuss the coding of the application targeted to the Nucleo-144 developer board. First, let’s build, flash and debug a completed project.

The project’s file structure

It is beyond the scope of this article to go into all the details of the project’s package structure. The conventions for packaging a Rust application are defined in the Cargo documentation, like here.

HINT: The source files that are typically present in a Rust project do not reflect the application’s object model (e.g. classes) as it is the case in Java or C++. They rather represent bundles of related types, data, and logic that the developer thoughtfully groups into modules. Rust source files will usually be short and easy to navigate.

The module tree is a handy structural concept for visualizing a Cargo-based Rust project. The module tree of the small firmware crate that is the subject of this post looks like this:

└──               // ABI
    ├── reset
    └── start_trunk
└──                // Declaring library modules, coded in Drone-OS libs
    ├── periph                 // MCU peripherals configuration
    │   └── src/periph/  // Declares peripheral modules used in this project
    │       └── flash
    │       │   └── src/periph/  // MCU flash configuration
    │       └── lse
    │       │   └── src/periph/    // Low speed ext. resonator configuration
    │       └── msi
    │       │   └── src/periph/    // Multispeed internal oscillator config.
    │       └── pll
    │           └── src/periph/    // Phase-locked loop clock configuration
    ├── drv                    // MCU peripherals setup
    │   └── src/periph/  // Declares peripheral modules used in this project
    │       └── flash
    │       │   └── src/periph/  // MCU flash setup
    │       │       └── new
    │       │       └── free
    │       │       └── init
    │       └── lse
    │       │   └── src/periph/    // Low speed ext. resonator setup
    │       │       └── new
    │       │       └── free
    │       │       └── init
    │       └── msi
    │       │   └── src/periph/    // Multispeed internal oscillator setup
    │       │       └── new
    │       │       └── free
    │       │       └── init
    │       └── pll
    │           └── src/periph/    // Phase-locked loop clock setup
    │               └── new
    │               └── free
    │               └── init
    ├── consts            // Constants     
    │   └── src/    // Declaration of constants used in this project
    ├── heap              // The heap allocator
    │   └── src/  
    │       └── pools        // Memory allocation for application's heap
    ├── reg               // Memory-mapped registers
    │   └── src/       // Registration of MCU-type-specific token structure
    ├── sv                // Supervisor
    │   └── src/        // Supervisor services setup
    └── thr               // Interrupt driven threads
    │   └── src/thr/  // Declares the modules used in this project
    │       └── button
    │           └── src/thr/  // Nucleo-144 board PUSH-BUTTON actions
    │       └── led
    │           └── src/thr/     // Nucleo-144 LED button actions
    │       └── trunk
    │           └── src/thr/   // Application configuration and setup thread.

In addition to the manually compiled module tree, there is the cargo doc generator. I will discuss the art of documenting Rust crates in a later post.

The build process

These installation and target configuration steps have to be executed only once before any new project can be built and flashed to the target. Assuming you are in the root directory of the project, run these shell commands:

rustup component add rust-src clippy rustfmt llvm-tools-preview && \
rustup target add i686-unknown-linux-musl thumbv7m-none-eabi thumbv7em-none-eabihf

The build process can now be started. In the project’s root directory, execute this shell command:

just build

As it is the first-time build, cargo will;

  • Read the file rust-toolchain. If the indicated version of the toolchain is not already installed, it will automatically go and fetch it from the Rust repository.
  • Read the file Cargo.toml and fetch and compile all external crates that are listed as dependencies.
  • Cross-compile our own project and generate the binary file for the download to the target MCU.

The full compilation can take several minutes. Finally, you should see these lines:

The binary file can be found here (your workspace might have a different name):

There is this command if you want to look at the contents of the binary file, including the memory allocations. The command will only work after the cargo-binutils have been installed:

cargo install cargo-binutils
just inspect

Connect the Debug Probe to the Nucleo board

I recommend using the official Black Magic Debug Probe hardware or a decent clone of it, and the most recent firmware version. Then you either disable the onboard ST-LINK as described in the user manual of the Nucleo board, or you cut off the ST-LINK portion (see section Cuttable PCB of the user manual). Carefully read the section about External power supply inputs. I wired the BMP and the Nucleo on a cut-out Nucleo for SWD debugging mode as follows:

BMP Pins          Nucleo Headers      Trace to MCU Pin  
RST ............. CN11 pin 14         RESET
TDI ............. not connected
TDO ............. not connected
TCK ............. CN11 pin 15         PA14*
TMS ............. CN11 pin 13         PA13*
UART RXD ........ CN7  pin 15         PB3
TPWR ............ CN11 pin 16         VDD MCU**

(*) PA13 and PA14 are shared with SWD signals connected to ST-LINK/V2-1. If ST-LINK part is not cut, it is not recommended to use them as I/O pins.
(**) 3.3 V on Nucleo-144

Flash the firmware

The shell command for flashing and starting the application is:

just flash

The LED on the Nucleo should now blink and the output on the console should look like this;

Inspect the ITM logging output

The source code of this demo project includes a println statement which regularly prints a counter value to the ITM logging channel. The output is sent to the debug adapter (the BMP) which forwards it to the host PC over USB. You can see the output with this shell command:

just swo

The listener itmsink is started which parses the input from the virtual debugging COM-port. The data from the logging channel is displayed in the terminal console. Other ITM channel data, like the heap allocation and deallocation events, will be stored to a binary file for subsequent formatting with Drone-OS utility processors.

That is the result. So you got an impression how the Drone-OS and the Rust toolchain are applied for building, flashing and tracing your own firmware for a ARM Cortex-M. To conclude this post, I add this contemplative image of a much simpler toolchain 🙂

Leave a Comment