Make your microcontroller apps safe and secure with Rust
Rust on a Device
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.
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.
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
(incl. VAT)
Buy Linux Magazine
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.
News
-
Gnome 48 Debuts New Audio Player
To date, the audio player found within the Gnome desktop has been meh at best, but with the upcoming release that all changes.
-
Plasma 6.3 Ready for Public Beta Testing
Plasma 6.3 will ship with KDE Gear 24.12.1 and KDE Frameworks 6.10, along with some new and exciting features.
-
Budgie 10.10 Scheduled for Q1 2025 with a Surprising Desktop Update
If Budgie is your desktop environment of choice, 2025 is going to be a great year for you.
-
Firefox 134 Offers Improvements for Linux Version
Fans of Linux and Firefox rejoice, as there's a new version available that includes some handy updates.
-
Serpent OS Arrives with a New Alpha Release
After months of silence, Ikey Doherty has released a new alpha for his Serpent OS.
-
HashiCorp Cofounder Unveils Ghostty, a Linux Terminal App
Ghostty is a new Linux terminal app that's fast, feature-rich, and offers a platform-native GUI while remaining cross-platform.
-
Fedora Asahi Remix 41 Available for Apple Silicon
If you have an Apple Silicon Mac and you're hoping to install Fedora, you're in luck because the latest release supports the M1 and M2 chips.
-
Systemd Fixes Bug While Facing New Challenger in GNU Shepherd
The systemd developers have fixed a really nasty bug amid the release of the new GNU Shepherd init system.
-
AlmaLinux 10.0 Beta Released
The AlmaLinux OS Foundation has announced the availability of AlmaLinux 10.0 Beta ("Purple Lion") for all supported devices with significant changes.
-
Gnome 47.2 Now Available
Gnome 47.2 is now available for general use but don't expect much in the way of newness, as this is all about improvements and bug fixes.