IoT CONFERENCE Blog

Programming microcontrollers with Rust

Embedded Rust in easy steps

May 10, 2021

The Rust programming language is becoming increasingly interesting, even in the field of embedded systems. This article is a short introduction to Rust and why it is suitable for programming microcontrollers. It also presents a short Rust program that can be run on a microcontroller.

Smoke detectors, household appliances, machine tools, cars: microcontrollers are everywhere. Tiny computers are used wherever the use of a “real” computer is not possible or practical due to available space, budget, or energy consumption requirements. Often, microcontrollers run without an operating system and must respond in real-time to changing environmental conditions. Considering the limited resources – only a few kB of RAM are common – it quickly becomes clear that not every language is suitable for programming these devices. The top dog in this area is C, but C++, Assembly, and some other programming languages are also used. In recent years, Rust has become another ideal candidate for programming microcontrollers. Rust is a fairly young programming language, with version 1.0 released in 2015. Initially developed by Mozilla Research for use in the experimental browser Servo, Rust is now supported by a worldwide developer community and used productively in numerous companies (including Google, Microsoft, and Facebook).

TO NEWSLETTER

Stay tuned on IoT’s latest News

Why Rust?

Rust advertises productivity, reliability, and performance on its website [1]. Rust’s productivity is due to its modern tooling and well-thought-out language design. Rust benefits from learning from the successes and mistakes of its predecessors. However, the compiler – in the service of reliability – often makes life difficult for the developer by pointing out potential problems with numerous error messages. Since this often represents a high hurdle for beginners when learning Rust, the detailed reading of the excellent (English-language) documentation is recommended to all interested [2]. While many modern languages with just-in-time compilers and optimized garbage collectors achieve impressive performance in the form of high throughputs, Rust manages without a complex runtime environment thanks to its intelligent compiler. This results in lower memory consumption and consistent response times. For use with microcontrollers, this is of the highest importance. Overall, Rust is, despite modern features, high abstraction level, and good reliability, comparable in its runtime behavior with C and C++. This makes it a real alternative to these languages, especially with microcontrollers.

A GLIMPSE INTO IoT

SMART DATA SOLUTIONS

Hardware

Microcontrollers are available in a large number of different variations, which differ among in their capabilities and the processor architecture used. ARM Cortex-M-type microcontrollers are widely used.

ARM Cortex-M microcontrollers are offered by many different manufacturers, but they are all based on the same cores licensed by the ARM company. Even though none of the manufacturers of Cortex-M microcontrollers officially support Rust at the moment, the language can be used on many models thanks to the efforts of the open source community. In the following, we take a closer look at the use of Rust on an LPC845 from NXP [3]. A low-cost development board is available for the LPC845 in the form of the LPC845-BRK [4]. In addition, there are already Rust libraries that support many functions of the LPC845.

Step 1: Installation

Before we can write a Rust program and run it on the LPC845-BRK, we must first install the software needed for it. First, we need two standard programs: a text editor (or IDE) to edit the files and a terminal to execute various commands. The choice here is left to the reader, but on Linux, macOS, and Windows, Visual Studio Code with the rust-analyzer extension as IDE is certainly not a bad choice. Next, Rustup is needed, which can be downloaded from the official Rust website [5]. Rustup allows the installation and management of Rust toolchains. We only need one of these (the latest version from the “stable” channel), but will extend it with additional components using Rustup. After successfully installing Rustup, the Rust compiler and Cargo, Rust’s build tool, should be installed. This can be verified by running rustc -version and cargo -version. If these programs are missing, it should be possible to fix this with rustup toolchain install stable. Now we have everything we need to create “normal” Rust programs. To be able to compile programs for our microcontroller, we still need the core library of Rust (a stripped-down version of the normal standard library) for the appropriate target platform.

The LPC845 uses ARM Cortex-M0+ as the core, which in turn is built on the ARMv6-M architecture. With rustup target add thumbv6m-none-eabi we install the core library in the correct variant. One last piece of the puzzle is missing before we’re ready to jump in fully: We need to somehow be able to load and run programs that we compile for the microcontroller. There are numerous options for this, but for a Rust project, cargo-flash, an extension of Cargo, is the most accessible [6]. We can install that with cargo install cargo-flash. If everything worked, Rust programs for our microcontroller should now be compilable and executable. If there is an LPC845-BRK board at hand, we can quickly verify this. We need to clone the Git repository of the lpc8xx-hal library and run a sample program:

git clone https://github.com/lpc-rs/lpc8xx-hal.git
cd lpc8xx-hal
cargo flash --chip=LPC845M301JBD48 --features=845-rt --example=gpio_timer

For this, the LPC845-BRK must be connected to the PC via USB. If everything has worked, one of the LEDs on the board should blink regularly.

Step 2: A minimal program

We are now ready for the next step: write a minimal program for the microcontroller and run it there. For this, create a project with the help of Cargo and adapt it for our purposes. As mentioned before, Cargo is the official build tool for Rust. In addition, it helps us download and manage dependencies. With the command cargo new lpc845 we create a minimal project (lpc845 is the name of the project and arbitrarily interchangeable). If we run cargo run in the created directory, cargo will compile and start our program (Fig. 1).

Fig. 1: The output of cargo run

 

However, our program is a regular Rust program for the operating system installed on our computer. Before we can run it on the LPC845, we need to make adjustments. Let’s start with the dependencies: In the Cargo.toml file, we need to add two dependencies under \[dependencies\]. This part of the file should look like this:

toml
[dependencies]
lpc8xx-hal = { version = "0.7", features = ["845-rt"] }
panic-halt = "0.2"

lpc8xx-hal gives us access to many features of the LPC845 (and other models of the LPC800 series). The library is open source and available on GitHub [7]. Additionally, we still need a panic handler, for this, we can use panic-halt. This will be explained in more detail later. Now, we need to do some tweaking of Cargo’s configuration so that the correct instructions are sent to the Rust compiler. Create in the project directory (the one where Cargo.toml is located) the subdirectory .cargo and in it the file config (so the path of the created file is .cargo/config).

First, we need to instruct Cargo to compile the project for the ARMv6-M architecture used by the LPC845:

toml
[build]
target = "thumbv6m-none-eabi"

Then, we need to give Cargo some configuration for the target architecture:

toml
[target.thumbv6m-none-eabi]
rustflags = [
"-C", "link-arg=-Tlink.x",
]

So we initiate that the linker gets a so-called linker script and make sure that the compiled program also has the correct format to be executed on the LPC845 (and other microcontrollers of the type ARM Cortex-M0/M0+). We don’t need to worry about the details here, since the linker script is provided via lpc8xx-hal.

Now we have everything we need to create a program for the LPC845. However, this will not work with our Cargo generated program. Normal Rust code assumes that the operating system will take care of the numerous details necessary to load and run a program. In our case, lpc8xx-hal and its dependencies will take care of these details. However, we need to initiate this in our program. To do this, we replace the contents of src/main.rs with the following code. First, we need to start our program with these instructions to the Rust compiler:

rust
#![no_main]
#![no_std]

!\[no_main\] tells the compiler not to generate a main function for our program. This would normally be executed by the operating system when the program is started, which does not apply to our situation. !\[no_std\] tells the compiler that our program does not use the Rust standard library. This contains modules that assume the existence of a file system, network sockets, environment variables, and numerous other things that don’t exist without an operating system. But don’t worry, we still have the core library (a subset of the standard library) at our disposal. Moving on to the Panic Handler:

rust
extern crate panic_halt;

Panic in the context of Rust refers to an exceptional error. The standard library contains a panic handler that aborts the program and displays an error message. Since this cannot be implemented without an operating system, we have to bring our own panic handler. Which is the right one depends on the situation. Here we use a very simple panic handler that does nothing but terminate the execution of our program.

The last thing we need is a main function. Previously, we indicated that the compiler should not take care of this. Nevertheless, we need a function to start our program (Listing 1).

 

Listing 1

rust use lpc8xx_hal::cortex_m_rt::entry; #[entry] fn main() -> ! { loop { lpc8xx_hal::cortex_m::asm::nop(); } }

 

#\[entry\] specifies that this function should be executed after the microcontroller is started. Under the hood, one of the dependencies of lpc8xx-hal makes sure that this happens. ! is the return value of the function. This is a special type (called never type) that expresses that the function never ends. Since there is no operating system to take over again after the program ends, our program should just keep running as long as the hardware is active and no panic occurs. Finally, we have an infinite loop that does nothing but execute the assembly instruction nop (No Operation) over and over again. With this, we have a minimal but complete microcontroller program. Although it effectively does nothing, we can load it onto the microcontroller and run it with the following command: cargo flash –chip=LPC845M301JBD48 (Fig. 2).

Fig. 2: The output of the cargo-flash command

 

Step 3: Button and LED

Our minimal program is certainly a nice intermediate success, but achieving a visible effect would of course be even better. The buttons and LEDs available on the LPC845-BRK are the easiest way to achieve this. For this, we replace the code in the main function with the following new code.

rust
let p = lpc8xx_hal::Peripherals::take().unwrap();

Peripherals is a struct from the lpc8xx-hal library that gives us access to many of the features of the LPC845. The take method gets us an instance of Peripherals. The API of lpc8xx-hal is designed to guarantee error-free access to hardware features. One aspect of this is to prevent concurrent, overlapping accesses to the API. That is why take has a return value of type Option <Peripherals>, and only the first call will actually return a Peripherals instance. With unwrap, the peripherals instance is unwrapped from the option so we can use it from now on. This is trouble-free because it is the only call to Peripherals::take in this program. Further calls would cause a panic to prevent multiple access to the API.

rust
let mut syscon = p.SYSCON.split();
let gpio = p.GPIO.enable(&mut syscon.handle);

Here we enable the GPIO hardware. GPIO stands for General Purpose I/O and controls digital input and output signals. We will use it to interact with a button and LED.

rust
let button = p.pins.pio0_4.into_input_pin(gpio.tokens.pio0_4);
let mut led = p.pins.pio1_1.into_output_pin(
gpio.tokens.pio1_1,
lpc8xx_hal::gpio::Level::High,
);

Here we configure the pins to which a button and an LED are connected. The button pin (PIO0_4) is set to input mode, the LED pin (PIO1_1) to output mode. The API not only makes the required configurations, it also keeps track of which mode the pins are in at compile time. This prevents us from accidentally calling methods later that are not available in the corresponding mode. This would cause a compile error instead of leading to undesired behavior only at runtime.

 

Listing 2

rust loop { if button.is_high() { led.set_high(); } else { led.set_low(); } }

 

After the configuration is done, here is finally the core of the program: We read the signal coming from the button and pass it directly to the LED. If the button is pressed, the LED lights up. If it is released, the LED goes off again. Attention: We mean here the white button, which is located more towards the center of the board. The other two buttons (RST and ISP) are next to each other, near the USB socket. Compared to some C APIs (e.g. Arduino) Rust code is sometimes bulkier, the HAL APIs in Rust are often much more complex and harder to understand. In return, many erroneous programs cannot be compiled in Rust in the first place, whereas comparable errors in C can go unnoticed at first, only to cause problems later at runtime.

Further links

I hope this article could give you a first impression of Embedded Rust. Of course, such a short introduction can’t cover all relevant details, so here are a few references to further material.

First of all, anyone interested in learning Rust should refer to the official documentation. Numerous resources can be accessed on the Rust website under the Learn menu item [8]. For beginners, the online book “The Rust Programming Language” is probably the best place to start [9]. For those specifically interested in Embedded Rust, “The Embedded Rust Book” is recommended [10]. This also points to other resources on the subject. For those who want an overview of the Embedded Rust ecosystem, the available tools and libraries, and the supported hardware, “Awesome Embedded Rust” [11] is recommended. And finally, the program presented in this article is available on GitHub [12]. If you want to buy an LPC845-BRK to run the program or for your own experiments, you can find it at relevant resellers e.g. Farnell [13] or Mouser [14].

Links und Literature

[1] https://www.rust-lang.org

[2] https://www.rust-lang.org/learn

[3] https://www.nxp.com/products/processors-and-microcontrollers/arm-microcontrollers/general-purpose-mcus/lpc800-cortex-m0-plus-/low-cost-microcontrollers-mcus-based-on-arm-cortex-m0-plus-cores:LPC84X

[4] https://www.nxp.com/products/processors-and-microcontrollers/arm-microcontrollers/general-purpose-mcus/lpc800-cortex-m0-plus-/lpc845-breakout-board-for-lpc84x-family-mcus:LPC845-BRK

[5] https://www.rust-lang.org/tools/install

[6] https://github.com/probe-rs/cargo-flash

[7] https://github.com/lpc-rs/lpc8xx-hal

[8] https://www.rust-lang.org/learn

[9] https://doc.rust-lang.org/book/

[10] https://doc.rust-lang.org/stable/embedded-book/

[11] https://github.com/rust-embedded/awesome-embedded-rust

[12] https://github.com/braun-embedded/lpc845-example

[13] https://de.farnell.com

[14] https://www.mouser.de