Introduction to Embedded Systems with Rust: A Beginner's Guide Using ESP32
by Daniel Zelei
Sep 1
9 min read
101 views
Are you curious about the embedded world but have no idea where to start with electronics? How do you read the temperature? How do you display information on a screen? How can you write your own library?
Let me guide you into this world with the help of Rust and the ESP32 microcontroller.
In this article, I'll introduce the basics of embedded programming and show you how to get started with both the hardware and software side.
Hardware Basics
A great way to start learning about embedded systems is by using a microcontroller. These small, powerful devices are often used in educational projects and have a lot of tutorials and libraries available.
The most popular options are:
- Arduino
- ESP32
- Raspberry Pi
In this tutorial, we will focus on the ESP32, a versatile and powerful microcontroller that is perfect for beginners and experts alike.
You can buy the microcontroller on its own or as part of a starter kit that includes basic sensors like temperature or light sensors.
Software Basics
Now that you have the hardware, let’s talk about the software. For the ESP32, there is a well-documented library available that will help you get started with Rust: ESP-RS Documentation.
I will walk you through the steps of setting up the ESP32 with Rust, from installing the necessary tools to writing your first program. Along the way, I’ll also share the challenges I faced that weren’t covered in the official documentation, so you won’t have to struggle with the same issues.
Step 1: std
vs. no_std
In Rust, there are two different approaches to programming: using the full standard library (std) or opting for no_std. Understanding the difference is crucial when working on embedded systems, where resources are often limited.
std
-
The std library provides many features out of the box, such as collections (e.g., Vec), file handling, networking, and threading. This makes development faster and easier.
-
Many Rust libraries and tools depend on
std
, so using it allows you to take advantage of a wide range of existing resources. -
If you're coming from desktop or server Rust development, you'll find the
std
library familiar and comfortable.
no_std
-
no_std
is designed for systems with very limited resources, making it ideal for embedded devices. -
You can run
no_std
programs directly on the hardware without an OS, which is perfect for microcontrollers. -
Programs tend to be smaller, which helps when working with devices that have limited storage
In embedded Rust development, where microcontrollers often run with very limited memory and no operating system, no_std
is the better choice. It strips away features that require more system resources, focusing on essential functionality needed for low-level programming.
For this tutorial, we will be using no_std
to work directly on the ESP32 hardware.
Step 2: Setup
Before we start coding, we need to set up the development environment based on your device’s architecture. Espressif's SoCs are built on different architectures, such as RISC-V and Xtensa, and the setup process depends on which one you're using.
Identifying Your Architecture
- Xtensa: For example, the ESP32-WROOM is based on the Xtensa architecture.
- RISC-V: Devices like the ESP32-C3 are based on the RISC-V architecture.
If you're unsure about your device’s architecture, check your device's specifications, which are usually available in the product's documentation or on the manufacturer's website.
Installing the Toolchain
To develop applications in Rust for Espressif SoCs, we need to install the appropriate toolchains. This is where espup
comes in—it helps install and maintain the required toolchains for your architecture.
To install espup
, run the following command in your terminal:
cargo install espup
Then, install the necessary toolchains:
espup install
After installation, you'll see a message like this, suggesting you set some environment variables:
[info]: Installation successfully completed!
To get started, you need to set up some environment variables by running: '. /home/danielzelei/export-esp.sh'
This step must be done every time you open a new terminal.
See other methods for setting the environment in <https://esp-rs.github.io/book/installation/riscv-and-xtensa.html#3-set-up-the-environment-variables>
If you encounter the following error:
Caused by:
process didn't exit successfully: `/tmp/cargo-installlfnEMgk/release/build/libudev-sys-678fd6d3ec7ef9ed/build-script-build` (exit status: 101)
...
--- stderr
thread 'main' panicked at '/home/danielez/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libudev-sys-0.1.4/build.rs:38:41:
called `Result::unwrap()` on an `Err` value: "pkg-config exited with status code 1
PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags libudev
The system library `libudev` required by crate `libudev-sys` was not found.
The pkg-config utility tells us that `libudev.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.
NOTE: if you have installed the library, you may need to add PKG_CONFIG_PATH to the directory containing `libudev.pc`.
"
You can fix it easily by running: (https://github.com/esp-rs/espflash/issues/108)
sudo apt-get install libudev-dev
Software - The template
The next step is to create a project using a template.
Install cargo-generate
, which allows you to create a new project based on an existing template:
cargo install cargo-generate
Then, generate the project using the esp-template
:
cargo generate esp-rs/esp-template
Once the project is created, open it (for example, using your favorite IDE or by navigating into the project directory in the terminal) and check the code inside the src/main.rs file. Here’s what the code looks like:
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{
clock::ClockControl, delay::Delay, peripherals::Peripherals, prelude::*, system::SystemControl,
};
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take();
let system = SystemControl::new(peripherals.SYSTEM);
let clocks = ClockControl::max(system.clock_control).freeze();
let delay = Delay::new(&clocks);
esp_println::logger::init_logger_from_env();
loop {
log::info!("Hello world!");
delay.delay(500.millis());
}
}
Let’s break down what’s happening here:
#![no_std]
This tells Rust not to use the standard library, which is common in embedded programming.
#![no_main]
This indicates that the usual main entry point is not used. Instead, a custom entry point is provided, typical for embedded systems.
#[entry]
fn main() -> ! {
This defines the entry point of the program with the main function, which never returns (-> ! indicates a diverging function).
You'll notice that the imports include hal, which stands for Hardware Abstraction Layer (HAL) for embedded systems.
let peripherals = Peripherals::take();
This line acquires access to the microcontroller’s hardware peripherals.
let system = SystemControl::new(peripherals.SYSTEM);
This wakes up and activates the system controller using the microcontroller's system tools.
let clocks = ClockControl::max(system.clock_control).freeze();
This sets the system clocks to their fastest speed and locks that setting in place.
Setting the system clocks to their fastest speed ensures the microcontroller runs at its highest performance, making everything work as quickly as possible. Locking that setting makes sure the speed stays consistent during operation, so there are no unexpected slowdowns or changes.
Software - Execution - Hello World
Connect your ESP32 via USB and start with:
cargo run
The first time, you might see the following error:
WARNING: use --release
We *strongly* recommend using release profile when building esp-hal.
The dev profile can potentially be one or more orders of magnitude
slower than release, and may cause issues with timing-senstive
peripherals and/or devices.
Finished `dev` profile [optimized + debuginfo] target(s) in 0.09s
Running `espflash flash --monitor target/xtensa-esp32-none-elf/debug/rust-test-esp32`
[2024-08-01T21:27:30Z INFO ] Serial port: '/dev/ttyUSB0'
[2024-08-01T21:27:30Z INFO ] Connecting...
Error: espflash::serial_error
× Failed to open serial port /dev/ttyUSB0
├─▶ Error while connecting to device
├─▶ IO error while using serial port: Permission denied
╰─▶ Permission denied
You can fix this by running:
sudo usermod -a -G dialout $USER
After adding your user to the dialout group, you need to log out and log back in for the changes to take effect. Alternatively, you can reboot your system.
Then, verify your group membership with the following command:
groups
You should see dialout listed in the output.
Now, try running the command again:
cargo run
You should see the following output:
I (31) boot: ESP-IDF v5.1-beta1-378-gea5e0ff298-dirt 2nd stage bootloader
I (31) boot: compile time Jun 7 2023 07:48:23
I (33) boot: Multicore bootloader
I (37) boot: chip revision: v3.1
I (41) boot.esp32: SPI Speed : 40MHz
I (46) boot.esp32: SPI Mode : DIO
I (50) boot.esp32: SPI Flash Size : 4MB
I (55) boot: Enabling RNG early entropy source...
I (60) boot: Partition Table:
I (64) boot: ## Label Usage Type ST Offset Length
I (71) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (79) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (86) boot: 2 factory factory app 00 00 00010000 003f0000
I (94) boot: End of partition table
I (98) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=04168h ( 16744) map
I (112) esp_image: segment 1: paddr=00014190 vaddr=3ffb0000 size=0000ch ( 12) load
I (115) esp_image: segment 2: paddr=000141a4 vaddr=40080000 size=01028h ( 4136) load
I (125) esp_image: segment 3: paddr=000151d4 vaddr=00000000 size=0ae44h ( 44612)
I (148) esp_image: segment 4: paddr=00020020 vaddr=400d0020 size=05d24h ( 23844) map
I (157) boot: Loaded app from partition at offset 0x10000
I (157) boot: Disabling RNG early entropy source...
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
Congratulations! You've successfully printed "Hello world!" using your ESP32.
The logging (log::info!("Hello world!");
) in this code is typically sent to a serial terminal over the UART interface of the ESP32. It is not output on the ESP32 chip itself but instead sent to a connected device (like a computer) where you can view the logs using appropriate software.
Software - Flashing a Light
Most ESP32 microcontrollers come with a built-in LED. Let’s try to flash that light! In the example provided by ESP-RS, you can find the following code:
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{
clock::ClockControl,
delay::Delay,
gpio::{Io, Level, Output},
peripherals::Peripherals,
prelude::*,
system::SystemControl,
};
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take();
let system = SystemControl::new(peripherals.SYSTEM);
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
// Set GPIO7 as an output, and set its state high initially.
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
let mut led = Output::new(io.pins.gpio7, Level::Low);
led.set_high();
// Initialize the Delay peripheral, and use it to toggle the LED state in a
// loop.
let delay = Delay::new(&clocks);
loop {
led.toggle();
delay.delay_millis(500u32);
}
}
There are two new lines that might be unclear at first:
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
This line initializes the GPIO (General Purpose Input/Output) peripheral, which controls the input and output pins on the microcontroller. The IO MUX (Input/Output multiplexer) allows the microcontroller to route different internal signals to the physical pins on the chip.
let mut led = Output::new(io.pins.gpio7, Level::Low);
You need to find out what GPIO7 corresponds to on your ESP32. In my case, the LED is connected to GPIO2, as specified in the documentation:
So, I will use io.pins.gpio2
.
Finally, run the code:
cargo run
And voila! The LED should be flashing.
That’s all for now. In the next part, we’ll dive into a simple setup with LEDs and buttons, and we’ll also write our own temperature sensor library from scratch. Stay tuned as we continue to explore embedded Rust on the ESP32!