Make your microcontroller apps safe and secure with Rust

Rust on a Device

© Photo by Lucas van Oort on Unsplash

© Photo by Lucas van Oort on Unsplash

Article from Issue 289/2024
Author(s):

Rust, a potential successor to C/C++, claims to solve some memory safety issues while maintaining high performance. We look at Rust on embedded systems, where memory safety, concurrency, and security are equally important.

For decades, C and increasingly C++ have been the languages of choice for microcontroller development, with assembler reserved for optimizations and start-up code. Most vendors provide free development IDEs, and most of these use GNU compilers under the hood. Some of the IDEs come with initialization code and generate device configuration code (choosing I/O functionality, etc.), so a device can be configured in a point-and-shoot manner letting you concentrate on writing application code. Similarly, manufacturers of I/O devices such as sensors and wireless modules supply drivers and example code.

C/C++ is an easy-to-learn language, but it's also easy to abuse and to write buggy code. Embedded code is expected to run unattended, often in hostile environments where code updates are difficult if not impossible. Increasingly, these devices are Internet-connected. While this means easier code updates, it requires a significant amount of extra effort to do well, and ironically this very connectivity lays the device open to nefarious access attempts. I know that there are tools available that help to minimize the risk of bad code getting into a product, and it is certainly not easy to consider rewriting large legacy codebases in another language, but I think that Rust [1] has come far enough and has sufficient advantages over C/C++ to be considered for new code and for re-writing critical sections of existing code as a stepping stone to a full Rust implementation.

With that being said, this article is intended as a "getting started" guide: Adoption of Rust is a big step in a commercial environment, and one of the best ways to evaluate a new language is to try it! In today's world of low-cost development boards and free software tools, all it takes is a little time and determination. Then you can base your decisions on some real-life experience.

There will be a lot of code in this article – many of the following listings have a filename – and you can get all those files from our download area [2].

Setting Up a Development Environment

I'm going to assume you've installed Rust and its associated tools and got beyond a simple "hello world" program. If not, you can follow the instructions on the Rust website [3] to get started. I'd encourage you to become familiar with the rudiments of Rust by writing code for your Linux platform before moving on to an embedded environment. I will use an STM32 development board, specifically the NUCLEO-L476RG [4] (Figure 1), but you could use almost any development board, though it's useful if the board has at least one LED and a push button. A little later, I'll show how to check if Rust supports a particular processor and/or architecture. STM32 devices are ARM Cortex based, and Rust has excellent support for that architecture. This particular NUCLEO development board, in common with many such boards, has an on-board programmer (typically another microcontroller), so a USB cable hooked up to you Linux box is all you need to download, run, and debug code on the board.

Figure 1: The NUCLEO-L476RG is an STM32 development board. © st.com

Development in Rust requires only the Rust toolchain and an editor. I'm going to be using VS Code [5] with the rust-analyzer plugin [6], as this has excellent support for Rust with code completion, syntax highlighting, and so on, but there are other solutions out there.

Cross Compiling

You'll need the datasheet [7] and reference manual [8] for the chosen microcontroller. From the start, you can see that the microcontroller has an ARM 32-bit Cortex-M4 32-bit processor with hard floating-point support. You can use this information to tell Rust what to cross compile to. Visit ARM's developer website [9] for more details: specifically the ARMv7E-M architecture and Thumb instruction set. Armed (no pun intended) with this information, you can visit Rust's platform support page and find the thumbv7 target support [10].

By default, your Rust installation only supports your host architecture, Linux. To add support for your target, simply type:

rustup target add thumbv7em-none-eabihf

Typing rustup show will confirm the supported targets. To start a project, type:

cargo new nucleo-l476rg-primer

Change your directory to the directory created in that command and start VS Code by typing code . (with a dot for the current directory at the end). You should see a source subdirectory and a Cargo.toml file; you can ignore the other files and directories for now. Open the main.rs file in the source directory. You need to tell the compiler not to include the standard library and that your program does not have the usual entry point. We use the #![no_std] and #![no_main] directives for this. Later you will add another directive to tell the compiler where the entry point is, and that can be a function with any name of your choice (including, oddly enough, main).

At this point you need support from an external library. There is a registry of available libraries (known as crates in Rust) [11]. The cortex_m_rt crate provides support for the Cortex core and includes linker instructions to ensure our code is properly located in the target memory [12]. You can add this crate as a dependency with

cargo add cortex-m-rt

this should add a dependency line to Cargo.toml.

Now you can add the entry-point directive and a function that never returns. Your code should look like this (main-01.rs):

#![no_std]
#![no_main]
extern crate cortex_m;
use cortex_m_rt::entry;
#[entry]
fn entry_point() -> !{
    loop {
    }
}

In order for the linker to know where to locate your code, you need to provide the cortex-m-rt crate with a memory layout file that it adds to the linker script. Looking at the memory map in the STM32L476 datasheet (page 103), you see that there is 128KB of internal RAM located at 0x20000000. Looking more closely at the datasheet (page 20), the SRAM is divided into 96KB mapped at address 0x20000000 (SRAM1) and 32KB located at address 0x10000000 with hardware parity check (SRAM2), so SRAM1 is the one you need. I will return to SRAM2 later.

There is also 1MB internal FLASH at 0x08000000. With some digging in the reference manual (at page 393), you can find that at reset the processor will jump to an entry in the interrupt vector table located at the beginning of FLASH. This is taken care of in the start-up code, so all you have to tell the linker is where to find the memory. Add memory.x with the following lines in the root of your project directory:

MEMORY
{
  RAM   : ORIGIN = 0x20000000, LENGTH = 96K
  FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
}

The linker also needs to know the target microcontroller so you need to add another file, config.toml, in a new .cargo directory:

[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]

This saves you from having to pass these arguments on the command line. The file link.x that rustflags refers to is generated by Cargo, and memory.x is included in it. If you attempt to control memory layout by adding statements to memory.x, they may clash with those in link.x and lead to some puzzling error messages. If you want finer control over memory layout, use the Rust flags to point to your own linker script.

To help VS Code's Rust analyzer, you need to add a further configuration file, which will ensure that it applies checks to the target architecture instead of the host. Otherwise you will get some strange messages. In a directory called .vscode add the settings.json file from the source code file collection.

If you try to compile the code now with cargo build, you'll get an error: The compiler will complain about a missing panic handler. As you may know, Rust may panic when something bad happens, such as an out-of-bounds array reference. In a desktop application, this would cause the program to terminate and print an informative error message. On an embedded system, that's impossible, so for the moment, we'll add a default handler which just stops the processor in an infinite loop. To add the handler, run

cargo add panic-halt

and add the line

use panic_halt as _;

to main.rs immediately below the existing use statement. The code should now build without errors, and the binaries for your target should be in the target subdirectory. You can install some further tools to check their validity. Run the following commands:

rustup component add llvm-tools
cargo install cargo-binutils

This installs the standard LLVM tools and a wrapper that Cargo uses to call them. If you type

cargo size -- -Ax

you will get some information about how the code is laid out in memory (see Listing 1). It shows that the vector table is located at the start of the flash memory, the program code (.text) 0x400 bytes later, and it's all of 0x8c bytes in size. Any variables (.data) would be located in RAM, though in this case, with the program being so simple, the .data segment has zero size.

Listing 1

Output of "Cargo Size"

nucleo-l476rg-primer  :
section               size         addr
.vector_table        0x400    0x8000000
.text                 0x8c    0x8000400
.rodata                  0    0x800048c
.data                    0   0x20000000
.gnu.sgstubs             0    0x80004a0
.bss                     0   0x20000000
.uninit                  0   0x20000000
.debug_abbrev       0x11ab          0x0
.debug_info        0x223c7          0x0
.debug_aranges      0x1348          0x0
.debug_ranges      0x195f0          0x0
.debug_str         0x3b4da          0x0
.comment              0x40          0x0
.ARM.attributes       0x3a          0x0
.debug_frame        0x4154          0x0
.debug_line        0x1f26e          0x0
.debug_loc            0x29          0x0
Total              0x9cc75

Programming the Target

To program your microcontroller with this code, you need one further tool, cargo embed. This tool talks over USB to the companion processor on the target board, which in turn manages programming the microcontroller. You can no longer install cargo embed directly as it is now part of a project called probe-rs. Follow the instructions at the project website [13], which just amounts to running a curl command. Let's first check that probe-rs supports our target:

$ probe-rs chip list | grep STM32L476
...
STM32L476RCTx
STM32L476RETx
STM32L476RGTx
...

The output shows it supports many variants of the STM32L476, including our target. Now connect the target board to a USB socket on your host. The power LED should light. Type probe-rs list and you should see that it has detected the target board. Your output may vary, depending on the exact configuration:

The following debug probes were found:
[0]: STLink V3 -- 0483:3753:000600273756501320303658 (ST-LINK)

Let's try programming the target:

cargo embed --chip STM32L476RGT

You should see an output similar to that in Figure 2 – success: Your code runs! Note that the proper launch command was cargo embed and not cargo run, which would attempt to run your program on the host.

Figure 2: Cargo can copy the binary to the target machine.

To complete the project configuration, you can add the chip information to an Embed.toml file in the project's root folder, so you don't have to provide all the details every time:

[default.general]
chip = "STM32L476RGTx"
[default.rtt]
enabled = false
[default.reset]
halt_afterwards = false

Then typing cargo embed will be enough to program and run your code on the target.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Rust Language

    We look at a few features of Rust, Mozilla's systems programming language, and its similarity to other languages.

  • Kernel News

    In kernel news: Rust in Linux; and Compiler and Kernel Frenemies.

  • Rust

    Largely unnoticed by the public, the Mozilla Foundation is tinkering with its own programming language, Rust, which is intended to make writing reliable, fast, and concurrently running applications easier. For this purpose, the developers are borrowing generously from other languages.

  • Two Types of Round Trip

    Due to the COVID-19 lockdown, Charly has time to devote to gadgets like graphical ping tools, flashing space stations, and space walks.

  • Topgrade

    Topgrade detects all the package managers installed on a system and executes them one by one at the command line.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News