Rust Embedded Programming Part 1: Introduction
- 1295 words
- 7 min
This is my first ever blog post. I hope you enjoy it. I'm writing about Rust and embedded programming. Not only that, but I'm currently learning Rust and starting to really like it. So I'm definitely jumped on that hype train.
I started with embedded programming back in the day when I received a Lego Mindstorms set as a Christmas gift instead of a cool PlayStation. However, this experience sparked a passion for programming and embedded development in me. So why not learn Rust and discovering my passion further.
This will be a series of blog posts about Rust and embedded programming. I will contain the following parts:
- Today we will have a look at the basics of Rust in this post and call it Part 1.
- In Part 2 we will go further and checkout if and how Rust can be used on microcontrollers. For this we will use a STM32F4 packed on a Blackpill board.
- And finally, in Part 3 we will have a look at how I used Rust to build my own hydroponic system to grow my own greens.
So better stay tuned for the following parts. But now enough of the introduction. Let's start with the basics of Rust.
Why Rust?
That is 100% the first question you should ask yourself when dealing with a new programming language. Why should we use Rust? And why for embedded programming? If we have a look at the shiny Rust landing page, there are several arguments for it:
-
Performance: Rust is blazingly fast. Rust comes without a GC or a runtime, which makes it a perfect fit as a system programming language.
-
Reliability: Rust comes with an ownership model and a strong type system. This will guarantee memory safety and expose bugs at compile time.
-
Static Analysis: Rust enforces resource configurations like embedded peripherals at compile time. There can’t be an unintended side effect to your hardware.
The Basics
Let's first cover the real basics. Of course, there are excellent docs out there how you can start learning Rust. You should definitely check out the official Rust book to start with. But let me show you my favorites of Rust and get our hands dirty on the basics.
Variables and Types
// variables are immutable by default
let foo: String = String::from("Hello, "); // immutable
let mut bar: String = String::from("Hello, "); // mutable
// foo.push_str("Building IoT!"); //❌
// bar.push_str("Building IoT!"); //✔
// variable types can be inferred
let inferred_var = String::new();
fn cube(x: i32) -> i32 {
x * x * x
}
Structures and Enums
struct User {
active: bool,
username: String,
sign_in_count: u64,
}
// Instantiating
let user = User {
active: true,
username: String::from("Alice"),
sign_in_count: 1,
};
impl User {
// Function definition (no reference to Self)
fn new(active: bool, username: String) -> Self {
User {
active,
username,
sign_in_count: 0,
}
}
// Method definition (mutable or immutable reference to Self)
fn set_active(&mut self, active: bool) {
self.active = active;
}
}
enum Colors {
Red,
Green,
Blue,
}
impl Colors {
fn is_part_of_yellow(&self) -> bool {
match self {
Colors::Green => true,
Colors::Blue => true,
_ => false,
}
}
}
let color = Colors::Blue;
let color_str = match color {
Colors::Red => "Red",
Colors::Green => "Green",
Colors::Blue => "Blue",
};
if let Colors::Red = color {
println!("This is red");
}
Options and Errors
There can be only one
Rust brings it own memory management. It is called the ownership-model. It is the reason why Rust is memory safe. At compile time. Since it is a unique concept of Rust, this is definitely the part where I struggled most at the beginning. I still get overwhelmed by it sometimes. But there were some key moments where I understood it a little bit better. So let me share that moment with you.
For a rule of thumb, keep those rules in mind:
- Each value in Rust has a variable that claims ownership of it. And there can only be one owner at a time.
- There can be multiple references to a value.
- There can be only a single mutable reference to a value and no immutable references.
- Part of the lifetimes, which we won't cover in this post. But a reference can't outlive the value it refers to.
Have a look at the following code snippet:
fn main() {
// ferris becomes owner of the string
let ferris = String::from("Hello rusty ferris");
// rusty becomes owner of the string
let rusty = ferris;
// ferris can't be used anymore. This won't compile
println!("{}", ferris);
println!("{}", rusty);
}
In Rust, there can be only one owner of a value. So ferris
becomes the owner of the string. If we assign ferris
to rusty
, rusty
becomes the owner. In the Rust language, we call that "move". So ferris
is moved to rusty
. After that, ferris
can't be used anymore.
So how can we fix this. We can use reference to ferris
instead of moving it. This is called "borrowing" in Rust. We can do that by using the &
operator.
fn main() {
// ferris becomes owner of the string
let ferris = String::from("Hello rusty ferris");
// rusty borrows a reference to ferris
let rusty = &ferris;
// ferris and rusty can be used now
println!("{}", ferris);
println!("{}", rusty);
}
Now let's try to mutate rusty
. Since Rust is a pretty explicit language, we have to tell the compiler that we want to mutate the value. We can do that by using the mut
on ferris
. And we also need to use &mut
on rusty
to mutate it.
fn main() {
// ferris becomes owner of the string
let mut ferris = String::from("Hello rusty ferris");
// rusty borrows a mutuable reference to ferris
let rusty = &mut ferris;
rusty.push_str("!");
println!("{}", rusty);
}
Ok, that works. We are now able to mutate rusty
. But what happens if we try to use ferris
again? This won't compile. We can't have a mutable and immutable reference at the same time.
// ferris becomes owner of the string
let mut ferris = String::from("Hello rusty ferris");
// rusty borrows a mutuable reference to ferris
let rusty = &mut ferris;
rusty.push_str("!");
// this won't compile. We can't have a mutable and immutable reference at the same time
println!("{}", ferris);
println!("{}", rusty);
And that is actually pretty clever by the compiler. Because this might cause a data race. If we use ferris
on a different part of the code, we won't expect that it is mutated somewhere else. So the compiler prevents us from doing that.
So, to tame the borrow-checker, keep those three rules in mind:
- Each value in Rust has a variable that claims ownership of it. And there can only be one owner at a time.
- There can be multiple references to a value.
- There can be only a single mutable reference to a value and no immutable references.
Conclusion
This was a really shallow dive into the fundamentals of Rust. We had a look at the variables and data types. We also touched Structs, Enums and their special implementation with Options and Results. And finally, we had a look at the ownership model of Rust. With the h three rules in mind, you can start trying some Rust coding. Just use references and pass ownership, and you will start learning how it works. Try to read what the compiler tries to tell you. The compiler is your best teacher. And before you start to get frustrated, just follow the compilers advice and use clone()
. Just like Uncle Bob said:
Make it work, make it right, make it fast.
Hopefully you are not disappointed at this point that we haven't covered embedded programming yet. But it is important to get a grasp about the fundamentals of Rust. They play a big role in embedded programming. So stay tuned for the next part of this series. Where we will figure out the Rust embedded concepts and how we can use them on a microcontroller.