In Part 1 of this blog series, we explored building bare-metal applications on Arm microcontrollers using Rust. In Part 2, we shift our focus to integrating Rust with Real-Time Operating Systems (RTOS) on microcontrollers and medium-sized microprocessors.
Most existing RTOS’s are written in C, and so any Rust program running on top will need to interact with an existing C API. Examples of an RTOS include, but are certainly not limited to, Eclipse ThreadX, FreeRTOS or Zephyr. On Arm, these systems are typically executing A32 instructions in AArch32 mode, on processors such as the Cortex-R52; although the concepts here would apply equally on a Cortex-M4, or Cortex-M55 or similar.
Figure 1: Approaches to writing Rust applications
Rust supports importing and exporting C-compatible functions, raw pointers, volatile memory accesses, and inline assembly for low-level hardware interactions. A full demonstration is beyond what can be pasted into a blog post, and so Ferrous Systems have published an open-source example application, which uses the Eclipse ThreadX RTOS and targets an Arm Cortex-R5 on an Arm Versatile Application Board (alongside an Arm PL011 UART, Arm PL190 Vector Interrupt Controller and Arm SP804 Dual Timer). The example compiles ThreadX as a static C library and then links it into a binary that is written in a mixture of Rust and Arm Assembly. This example can be compiled with either Ferrocene or with the standard Rust toolchain.
As with the bare-metal microcontrollers from the first section, on these real-time systems, the full Rust Standard Library is typically unavailable. Instead, the user is limited to a more basic subset that is known as libcore. It is not that it cannot be done: There are Rust Standard Library ports available for FreeRTOS and NuttX and others. However, these systems usually care a great deal about resource allocation and performance and so it makes sense to create some high-performance bindings to the parts of the RTOS that you require, instead of trying to abstract the RTOS behind an API that is more suited to the Application Processors that the next section will focus on. This approach is also beneficial for Functional Safety systems, where certifying a small custom RTOS shim in Rust is more practical than certifying the full Rust Standard Library.
In the ThreadX example, after the assembly language startup code has set up the stack pointer and enabled the Floating-Point Unit (FPU), execution is handed over to a main function written in Rust. The Rust code initializes drivers’ peripherals and then hands over execution to the ThreadX scheduler. Part of the ThreadX set-up involves calling back into the Rust firmare through a function called tx_application_define which has been written in Rust but declared as having a “C compatible” interface. This function is used to create a byte pool for the task stacks and various spawn tasks. A snippet from that function is shown in Figure 2, showing how easily Rust can call C APIs.
let byte_pool = { static BYTE_POOL: StaticCell<threadx_sys::TX_BYTE_POOL> = StaticCell::new(); static BYTE_POOL_STORAGE: StaticCell<[u8; DEMO_POOL_SIZE]> = StaticCell::new(); let byte_pool = BYTE_POOL.uninit(); let byte_pool_storage = BYTE_POOL_STORAGE.uninit(); unsafe { threadx_sys::_tx_byte_pool_create( byte_pool.as_mut_ptr(), c"byte-pool0".as_ptr().cast(), byte_pool_storage.as_mut_ptr().cast(), DEMO_POOL_SIZE as u32, ); byte_pool.assume_init_mut() } };Figure 2: An example of creating a ThreadX byte pool, using Rust. The threadx_sys crate contains auto-generated bindings that are based on the RTOS’s C header files.
let byte_pool = { static BYTE_POOL: StaticCell<threadx_sys::TX_BYTE_POOL> = StaticCell::new(); static BYTE_POOL_STORAGE: StaticCell<[u8; DEMO_POOL_SIZE]> = StaticCell::new(); let byte_pool = BYTE_POOL.uninit(); let byte_pool_storage = BYTE_POOL_STORAGE.uninit(); unsafe { threadx_sys::_tx_byte_pool_create( byte_pool.as_mut_ptr(), c"byte-pool0".as_ptr().cast(), byte_pool_storage.as_mut_ptr().cast(), DEMO_POOL_SIZE as u32, ); byte_pool.assume_init_mut() } };
Instead of converting the various ThreadX header files into Rust manually, the example uses the bindgen tool to automatically generate Rust bindings for ThreadX. This tool, originally developed by Mozilla and currently being supported by Ferrous Systems, can be applied to almost any library with a standard C header file, for example those provided by ThreadX. The example uses the automatically generated bindings from bindgen, allowing the Rust code to call into any ThreadX function, and the RTOS can call back into any Rust function marked as having extern "C" linkage.
ThreadX source code must be compiled with a standard C compiler, which the examples handle automatically. Rust is then told to link the resulting libthreadx.a to the compiled Rust code, producing the final binary.
In our example, the start-up code was written in Rust, but you might prefer the RTOS to handle the start-up and driver initialization from C, with just the tasks written in Rust. Alternatively, you could use an RTOS that was written entirely in Rust, like OxidOS. The general steps remain the same: Compile the library code you need into static libraries, then compile and link a binary using those static libraries. Whether the RTOS is a library, or the binary does not really change much, except the order in which things must be compiled.
See Part 3 where we explore using Rust with full-blown operating systems like Linux, Windows, and macOS on Arm processors.Figure 3: Real-Time OSes are often used in industrial and automotive applications.
Stay tuned for Part 3.