You are on page 1of 5

Embedded rust programming

Programming embedded systems in rust can be categorised by the corresponding level of


abstraction at which the programming is carried out, This results in four main levels:
1. Raw Memory Address Access
2. Peripheral access crate (PAC) level
3. Hardware abstraction layer (HAL) level
4. Embedded hardware abstraction (Embedded HAL) layer level

Raw Memory Address Access


This is the lowest level of abstraction and is reminiscent of embedded C programming. At this level,
we need detailed knowledge of the hardware typically provided by the reference manual or
datasheet. Our code then uses pointers to the various hardware devices and and their registers as
global variables. Rust declares global variables using the static keyword.
This requires that access functions to such variables be declared as unsafe, otherwise the compiler
will reject the functions. Generally a utility called svd2rust provides device description in rust if the
manufacturer provides an svd file of the device which is common of manufacturers. Our program
then uses this description in programming. We will first discuss the concepts of pointers,references
and pointers, volatile access and unsafe programming which are closely linked to this level of
programming before providing an example program.
Pointers
A pointer is a general programming concept for a variable that contains an address in memory. In
Rust, we have smart pointers in addition to normal pointers. Smart pointers are data structures
that not only act as a pointer but also have additional metadata and capabilities. Box pointers are
one of the smart pointers in Rust.

Box Pointers
Boxes allow us to store data on the heap instead of the stack. What remains on the stack is the
pointer to the heap data. A box can be defined in Rust as -
let ptr = Box::new(25);
A box does not have any capabilities other than storing the data on a heap other than the stack. We
use Box in the following cases -
• When we have a type whose size can’t be known at compile time and we want to use a value
of that type in a context that requires an exact size
• If we have a large amount of data and we want to transfer ownership but ensure the data
won’t be copied when we do so
• When we want to own a value and we care only that it’s a type that implements a particular
trait rather than being of a specific type
In the example below, there is a variable ptr that stores a Box pointer that points to a string “hello”.
The string hello is on the heap and the box pointer ptr is on the stack. The program will print the
following when executed -
The pointer points to data : hello
Example:
fn main() {
let ptr = Box::new("hello");
println!("The pointer points to data : {}", ptr);
}
Ownership:
The ptr variable owns the box pointer and thus the rules of ownership apply to it as well. As the
main method ends, the memory pointed by the ptr box pointer will be freed and the box pointer on
the stack will also be freed.
References vs Pointers
In Rust, pointers (called raw pointers) exist but are only used in specific circumstances, as
dereferencing them is always considered unsafe - Rust cannot provide its usual guarantees about
what might be behind the pointer.
References behave similarly to pointers, in that they can be dereferenced to access the underlying
values, but they are a key part of Rust's ownership system: Rust will strictly enforce that we may
only have one mutable reference or multiple non-mutable references to the same value at any given
time.

Volatile Access
Unlike C which declares variables as volatile, in Rust, we use specific methods to perform volatile
access: core::ptr::read_volatile and core::ptr::write_volatile. These methods take a *const T or a *mut T (raw
pointers) and perform a volatile read or write. In the example below we contrast C code and Rust
code on volatile.
C code:
volatile bool signalled = false;

void ISR() {
// Signal that the interrupt has occurred
signalled = true;
}

void driver() {
while(true) {
// Sleep until signalled
while(!signalled) { WFI(); }
// Reset signalled indicator
signalled = false;
// Perform some task that was waiting for the interrupt
run_task();
}
}

The corresponding Rust code:


static mut SIGNALLED: bool = false; //declaring SIGNALLED static makes it global

#[interrupt]
fn ISR() {
// Signal that the interrupt has occurred
// (In real code, we should consider a higher level primitive, such as an atomic type).
unsafe { core::ptr::write_volatile(&mut SIGNALLED, true) };
}

fn driver() {
loop {
// Sleep until signalled
while unsafe { !core::ptr::read_volatile(&SIGNALLED) } {}
// Reset signalled indicator
unsafe { core::ptr::write_volatile(&mut SIGNALLED, false) };
// Perform some task that was waiting for the interrupt
run_task();
}
}
In the code above we note that:
• We can pass &mut SIGNALLED into the function requiring *mut T, since &mut T
automatically converts to a *mut T (and the same for *const T)
• We need unsafe blocks for the read_volatile/write_volatile methods, since they are unsafe
functions.
Unsafe rust
In embedded systems, we often need access to the underlying computer hardware which is
inherently unsafe. This is usually done using pointers to the hardware referred to as raw pointers.
Rust provides unsafe rust for such access. The unsafe rust code is enclosed in a block. Unsafe rust
provides five main capabilities:
• Dereference a raw pointer
• Call an unsafe function or method
• Access or modify a mutable static variable
• Implement an unsafe trait
• Access fields of unions
The unsafe keyword only gives access to these five features that are then not checked by the
compiler for memory safety. All other code inside of an unsafe block is still checked for safety.
Generally it is wise to keep unsafe blocks small in case of debugging.

Dereferencing a Raw Pointer


Unsafe Rust has two new types called raw pointers that are similar to references. As with
references, raw pointers can be immutable or mutable and are written as *const T and *mut T,
respectively. The asterisk isn’t the dereference operator; it’s part of the type name. In the context of
raw pointers, immutable means that the pointer can’t be directly assigned to after being
dereferenced. Unlike references and smart pointers, Raw pointers:
• Are allowed to ignore the borrowing rules by having both immutable and mutable pointers
or multiple mutable pointers to the same location
• Aren’t guaranteed to point to valid memory
• Are allowed to be null
• Don’t implement any automatic cleanup
We can create an immutable and a mutable raw pointer from references as shown below:
let mut num = 5;

let r1 = &num as *const i32;


let r2 = &mut num as *mut i32;
We’ve created raw pointers by using as to cast an immutable and a mutable reference into their
corresponding raw pointer types. Because we created them directly from references guaranteed to
be valid, we know these particular raw pointers are valid, but we can’t make that assumption about
just any raw pointer. If we create a raw pointer to an arbitrary location in memory, there is no
guarantee that it will be valid. An example is shown below:
let address = 0x012345usize; //usize is pointer-sized unsigned integer type in this case pointer size is 32 bits.
let r = address as *const i32;
While we can create raw pointers in safe code, we can’t dereference raw pointers and read the data
being pointed to. Thus we need to use the code below to do so:
let mut num = 5;

let r1 = &num as *const i32;


let r2 = &mut num as *mut i32;

unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
As the example above shows, with raw pointers, and unsafe rust, we have created a mutable pointer
r2 and an immutable pointer r1 to the same location and can change data through the mutable
pointer, potentially creating a data race. This would not be allowed by rust.

Calling an Unsafe Function or Method


Unsafe functions and methods look exactly like regular functions and methods, but they have an
extra unsafe before the rest of the definition. To illustrate the syntax, we have an empty unsafe
function:
unsafe fn dangerous() {}

unsafe {
dangerous();
}
We must call the dangerous function within a separate unsafe block. If we try to call dangerous
without the unsafe block, we’ll get an error.
Bodies of unsafe functions are effectively unsafe blocks, so to perform other unsafe operations
within an unsafe function, we don’t need to add another unsafe block.
Creating a Safe Abstraction over Unsafe Code
One common abstraction is to wrap unsafe code in a safe function. As an example, a function
split_at_mut, that requires some unsafe code is shown below:

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);


assert_eq!(b, &mut [4, 5, 6]);

The function is implemented as shown below:


fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();

assert!(mid <= len);

(&mut slice[..mid], &mut slice[mid..])


}
Rust will not compile this function because it notices two mutable borrows on the same function
and thus decides it is not safe. We know it is safe because the borrow occurs on different parts of the
vector. We can use unsafe to get round this problem as we know the borrow is safe as shown below:
use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();

assert!(mid <= len);

unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Now we generate a raw pointer for slice
let ptr = slice.as_mut_ptr();
A slice is a pointer to some data and the length of the slice. We use the len method to get the length
of a slice and the as_mut_ptr method to access the raw pointer of a slice. In this case, because we
have a mutable slice to i32 values, as_mut_ptr returns a raw pointer with the type *mut i32, which
we’ve stored in the variable ptr. Then we access the slice using the raw pointers using an access
function as shown below:
slice::from_raw_parts_mut(ptr, mid) //first half of the slice
slice::from_raw_parts_mut(ptr.add(mid), len – mid) //second half of the slice
The slice::from_raw_parts_mut function takes a raw pointer and a length, and it creates a slice. We use
this function to create a slice that starts from ptr and is mid items long. Then we call the add method
on ptr with mid as an argument to get a raw pointer that starts at mid, and we create a slice using that
pointer and the remaining number of items after mid as the length.
The function slice::from_raw_parts_mut is unsafe because it takes a raw pointer and must trust that this
pointer is valid. The add method on raw pointers is also unsafe, because it must trust that the offset
location is also a valid pointer.
Although the function split_at_mut has unsafe raw pointer access function to slice, it is a safe function
and therefore is not declared unsafe. Thus it abstracts unsafe function and presents a safe function.
Functions declared within extern blocks are always unsafe to call from Rust code. The reason is that
they are from other languages and don’t enforce Rust’s rules and guarantees, and Rust can’t check
them, so responsibility falls on the programmer to ensure safety.
Accessing or Modifying a Mutable Static Variable
In Rust, global variables are called static variables. The names of static variables are in
SCREAMING_SNAKE_CASE by convention. Static variables can only store references with the
'static lifetime, which means the Rust compiler can figure out the lifetime and we aren’t required to
annotate it explicitly. Accessing an immutable static variable is safe. Example:
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
println!("name is: {}", HELLO_WORLD);
}
Unlike constants, values in a static variable have a fixed address in memory. Using the value will
always access the same data. Constants, on the other hand, are allowed to duplicate their data
whenever they’re used.
Accessing and modifying mutable static variables is unsafe. The example below shows how to
declare, access, and modify a mutable static variable.

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}

Any code that reads or writes from COUNTER must be within an unsafe block. With mutable data
that is globally accessible, it’s difficult to ensure there are no data races, which is why Rust
considers mutable static variables to be unsafe.

Implementing an Unsafe Trait


A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify.
We can declare that a trait is unsafe by adding the unsafe keyword before trait and marking the
implementation of the trait as unsafe too, as shown below:
unsafe trait Foo {
// methods go here
}

unsafe impl Foo for i32 {


// method implementations go here
}

Accessing Fields of a Union


A union is similar to a struct, but only one declared field is used in a particular instance at one time.
Unions are primarily used to interface with unions in C code. Accessing union fields is unsafe
because Rust can’t guarantee the type of the data currently being stored in the union instance.

You might also like