The Rust Programming Language has gained the attention of government security agencies, and even the White House, due to its unique blend of safety, performance and productivity. Rust is designed to remove common programming burdens and handle issues like use-after-free errors at compile time. Remarkably, it achieves this without using a garbage collector, generating machine code that rivals the performance of C and C++.
In this three-part blog series, Jonathan Pallant, Senior Engineer and Trainer at Ferrous Systems, offers an overview of Rust support for the Arm architecture including an introduction to Ferrocene, the qualified Rust toolchain for mission-critical and safety-critical applications. This overview is essential for anyone considering Rust for their next Arm-based project.
This series explores three examples that are picked from across the broad Arm spectrum, looking into the specifics of using Rust on bare-metal, RTOS, and Rich OS applications. Additionally, it discusses the current state of Rust on Arm, highlighting the features and libraries that are available from the Rust Project and from third parties, with or without commercial support.
Figure 1: Approaches to writing Rust applications
The first area that we will explore is the case of a microcontroller running a bare-metal application that is written in pure Rust. This will then be built on in the second section, which will add in a pre-existing RTOS that is written in C or C++.
The term microcontroller is used here to mean a small System-on-Chip (SoC) with integrated SRAM (and probably Flash). On Arm, these devices are executing T32 instructions in AArch32 mode, but some systems may use the A32 instruction set instead. Many of the ‘bare-metal’ concerns addressed here would also apply to lower-level code on a larger application processor, such as Secure Boot firmware or a Hypervisor. However, this section will look at a Nordic nRF52840 microcontroller running on the nRF52-DK developer kit. This popular microcontroller features an Arm Cortex-M4 processor, along with 256 KiB of SRAM and 1 MiB of Flash.
Bare-metal Rust firmware for Arm Cortex-M can rely on the start-up code produced by the Rust Embedded Devices Working Group, in a crate called cortex-m-rt. This crate allows the firmware to be written in pure Rust - the small amount of inline assembly needed (e.g., to initialize the data sections before main) is bundled up inside cortex-m-rt, which just takes you as far as the Rust fn main().
When the system has started and is running Rust code, a rich ecosystem of drivers is available. For example, the nrf-hal project has drivers for every peripheral in our nRF52840. Indeed, many Arm based microcontrollers have an excellent set of open-source drivers, including many of those from Nordic Semi, ST Micro, and Raspberry Pi. Cross-platform abstractions like the embedded-hal let those drivers describe the peripherals in a standardized way, allowing users to build re-usable components and libraries that can work on any suitable implementation, even across chip manufacturers. During the recent chip shortage in 2021, many embedded systems developers using Rust found this to be a very useful property, as it was much easier to change microcontrollers according to availability.
If you have not seen bare-metal Rust before, a full ‘blinky’ example for the nRF52840 is provided in Figure 2.
#![no_std] #![no_main] use core::fmt::Write; use cortex_m_rt::entry; use nrf52840_dk_bsp::Board; #[entry] fn main() -> ! { let mut nrf52 = Board::take().unwrap(); loop { writeln!(nrf52.cdc, "On!").unwrap(); nrf52.leds.led_2.enable(); writeln!(nrf52.cdc, "Off!").unwrap(); nrf52.leds.led_2.disable(); } } #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop { cortex_m::asm::udf(); } }Figure 2: A minimal but complete Rust ‘blinky’ for the nRF52-DK, using an open-source Board Support Package which provides UART drivers, GPIO, and more.
#![no_std] #![no_main] use core::fmt::Write; use cortex_m_rt::entry; use nrf52840_dk_bsp::Board; #[entry] fn main() -> ! { let mut nrf52 = Board::take().unwrap(); loop { writeln!(nrf52.cdc, "On!").unwrap(); nrf52.leds.led_2.enable(); writeln!(nrf52.cdc, "Off!").unwrap(); nrf52.leds.led_2.disable(); } } #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop { cortex_m::asm::udf(); } }
As the example shows, Rust allows the development of rich APIs to describe the various hardware interfaces like, LEDs and UARTs. Yet, the powerful optimizer, built-in to the Rust compiler, results in machine code that is broadly similar with what a C compiler would produce. Types like the Led type which backs the nrf52.leds.led_2 value shown in Figure 1 takes up no memory at run-time. It is a so-called zero-sized type. That means the type of system can be used to introduce safety and robustness into an API, with absolutely no run-time overhead.
Of course, it is sufficient for many applications, but developers are not just limited to basic event-loops and interrupt routines on microcontrollers in Rust. Microcontrollers based on the Arm Cortex-M can run Async Rust, using a small lightweight async executors written in pure Rust, for example, embassy. This can often be a productive and cost-effective alternative to starting up a full RTOS, especially when you only have a handful of tasks you need to execute concurrently.
Sometimes, however, a full RTOS is the right solution. In Part 2, we explore how to integrate Rust with existing C APIs, including practical examples with RTOSes like FreeRTOS and Eclipse ThreadX.
Figure 3: nRF52840 DK (Credit: Nordic Semiconductor)
Read Part 2