Gone are the days where C/C++ were the only option for embedded systems firmware/software development. Rust is another C like programming language. But wait, its not just another programming language. It has some ingenious ideas which makes us forget about memory management but the same time, without the overhead of a garbage collection. You heard it right, automatic memory management at compile time! During the run time, we can just sit back and enjoy the perks of memory safety and fearless concurrency!

Rust has matured over time and has been long used in production systems for generic software. All those days, the embedded ecosystem has been maturing all along, and we're almost there at production ready quality. Timing couldn't be better to check it out.

Installation

The official book has all the instruction you need. I'm mentioning it here for the sake of clarity. I use Linux as my daily driver. This should work on pretty much any distros. For MacOS the steps are similar too. Official method is the most preferred style for installation which is basically firing up a terminal and running a script. That will ask you some questions, just choose the default and you should be good to go:

$ curl https://sh.rustup.rs -sSf | sh

By default, Rust installs the compiler and standard library for your host architecture, which would most likely be x86_64 . So we need to install the necessary components for our ARM v6 and v7 architecture. Keep in mind, although you can do a lot of stuff in the default stable toolchain, living on the nightlies will make your life a little easier. Once you get advanced enough, you can consider the pros and cons of moving to the stable. For now, lets stick to the nightly channel:

$ rustup default nightly
$ rustup target add thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf

Lets also install some tools that would come handy later

$ cargo install cargo-binutils cargo-generate
$ rustup component add llvm-tools-preview

It would be nice to have two more tools handy. Best option is to use your distro's package manager to install them. I'm on fedora. So it goes like:

sudo dnf install qemu openocd

Getting started

Now that you have all the tools ready, lets get our hands dirty. Start by creating a new project with cargo:

cargo new embedded

That command will generate a folder `embedded` with some minimal files, which is basically a template for programs running on your host OS. To tell Rust that you need the stuff compiled for another architecture, you can tell it so by creating a configuration file in a hidden directory within this newly created folder:

cd embedded
mkdir .cargo

Now create a file `.cargo/config` with the below content:

[build]
target = "thumbv7m-none-eabi"

That says, you want to target a microcontroller of the ARMv7m architecture, which are Cortex M3, Cortex M4, etc. Cortex M0/M0+ are ARMv6m. Rust actually supports quite  a lot of architectures `rustc --print target-list` gives 99 of them. Although not all of them are embedded use case, and not all of them have a mature ecosystem as the ARM. Now we need to tell cargo, the rust package manager to go and fetch some libraries for us. We can certainly do without them. But didn't I say we have a good ecosystem?

[package]
name = "embedded"
version = "0.1.0"
authors = ["Aurabindo Jayamohanan <mxxx@auxxxxxdo.in>"]
edition = "2018"

[dependencies]
cortex-m = "0.5"
cortex-m-rt = "0.6"
cortex-m-semihosting = "0.3"
panic-semihosting = "0.5"

Package

Here you have some metadata about your program. Feel free to change the various parameters in there.

Dependencies

This lists the libraries and their version you wish to use. Upon reading this, cargo will fetch them from crates.io when you compile your code.

Now comes the main program source code. Put it in `src/main.rs`:

#![no_std]
#![no_main]
#![deny(unsafe_code)]

use cortex_m;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::{debug,hprintln};
use panic_semihosting;

#[entry]
fn main() -> ! {
    hprintln!("Rusty says hello!").unwrap();
    loop {
        debug::exit(debug::EXIT_SUCCESS);
    }
}

#[exception]
fn DefaultHandler(irqn: i16) {
    panic!("Unhandled exception(IRQn = {})", irqn);
}

#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
    panic!("{:#?}", ef);
}

I'll explain them line by line.

#![no_std]
#![no_main]
#![deny(unsafe_code)]

The first line above tells the compiler not to pull in the standard library. In embedded world, we dont need those. The next line says this program does not have a `main()` like all conventional ones do. And finally the last line tells the compiler to reject any unsafe code. Unsafe code is used when we know we're smarter than the compiler. Its the way we yell at the compiler for it to shut up and take whatever we give. When dealing with systems programming tasks like writing an OS kernel, etc., such unsafe blocks will be necessary to some extend. Here, we're telling that we will not resort to such horrors. However, the libraries we use can (and they do) use unsafe code. Basically, its in everybody's best interest to keep unsafe code to minimum.

Importing libraries

use cortex_m;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::{debug,hprintln};
use panic_semihosting;

This is where we bring in the external libraries. They contain the necessary glue to create a program thats runnable on a cortex m series microcontroller. First one provides low level access to Cortex M3/4 peripherals. Second one give you a minimal runtime for the rust program. Third one enables _semi hosting_, a technique which lets you use your computer's I/O system within the microcontroller which is only used during development and debugging. The last one, is a Rust language specific feature. It simply diverts panics via semihosting into the host machines's I/O.  Panic happens when a rust program encounters an unrecoverable error. Similar to the infamous Segfaults.

Main entry point

#[entry]
fn main() -> ! {
    hprintln!("Rusty says hello!").unwrap();
    loop {
        debug::exit(debug::EXIT_SUCCESS);
    }
}

Next we have an attribute entry which tells the compiler that this the where our program starts. We need not call it "main". This is the first function which will be executed by the system. First line withing the main is a macro, which prints a message via the semihosting mechanism mentioned earlier. If we have a debugging system connected, then we'll be able to see the message on our host computer screen.

Next thing to notice is the return type of our main function. `!` is the _never_ type. It means, this function does not return. That is the typical standard in an embedded system. You just do one task and keep doing them. Never stop. So, our program goes into a loop and stays only. Only while we're debugging, we know we dont need to keep looping there, so we inform the debugger that we're done here.

Interrupts

#[exception]
fn DefaultHandler(irqn: i16) {
    panic!("Unhandled exception(IRQn = {})", irqn);
}

#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
    panic!("{:#?}", ef);
}

Interrupts are a very important feature without which no embedded systems would be practical. Rust has a nice way to tell which functions act as a handler for an interrupt via the `exception` attribute. **HardFault** is a standard exception you hit when the system encounters situations like null pointer dereference, divide by zero,etc. _DefaultHandler_ on the other hand is a catch-all handler which would run for any interrupt in the system. Other interrupts, say for UART might be named "UART0".

Thats for the simplest Rust embedded hello world program. Now lets compile it.


cargo build

It couldn't be any more simpler. If you were using GCC, you would likely have had to compile it manually and link it by hand. But Cargo has you covered here!

Now we have only built the firmware. We need to run it somewhere to check this all works right? Lets try to emulate this firmware, shall we?

Emulating firmware

If you've noticed, we have not yet targeted a specific chip yet. The firmware we have at the moment is unrunnable. We need to target a specific chip. By targetting, I mean, getting the flash and RAM addresses correct so that this firmware when flashed goes to the right location in the controller's memory. Once that is done, we will be in a position to either flash to a real microcontroller, or to emulate it. For that, you need two more files:

memory.x:

MEMORY
{
  /* NOTE 1 K = 1 KiBi = 1024 bytes */
  /* TODO Adjust these memory regions to match your device memory layout */
  /* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

and `build.rs` in the top level directory:

use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

fn main() {
    // Put the linker script somewhere the linker can find it
    let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
    File::create(out.join("memory.x"))
        .unwrap()
        .write_all(include_bytes!("memory.x"))
        .unwrap();
    println!("cargo:rustc-link-search={}", out.display());

    // Only re-run the build script when memory.x is changed,
    // instead of when any part of the source code changes.
    println!("cargo:rerun-if-changed=memory.x");
}

Its a custom build script that rust will run first, prior to compiling the actual code. This will be run in your local development machine. It is a way to tell the linker what address the FLASH and RAM sections should be located.

There is one last thing we need to do. That is, to edit our hidden config file at `.cargo/config` to put the command that will launch our emulator. We can certainly do it manually, but its much easier to do cargo run. I assume you already have qemu installed:

[target.thumbv7m-none-eabi]
# make `cargo run` execute programs on QEMU
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"

And finally, do cargo run. Then you'll see:

Woohoo! Now we're ready to play with some real hardware!