Introduction
Content of this material
The goal of this book is to provide a getting-started guide on using the Rust programming language with Espressif SoCs and modules using no_std
(bare metal) approach. To better understand this approach, see Developing on Bare Metal (no_std) chapter of The Rust on ESP Book.
The introductory trail will introduce you to the basics of embedded development and how to make the embedded board interact with the outside world by reacting to a button press, and lighting an LED.
Note that there are several examples covering the use of specific peripherals under the examples folder of
esp-hal
. Seeesp-hal/examples
.
If you would like to learn about std
development, see Using the Standard Library (std) chapter of The Rust on ESP Book and,
Embedded Rust on Espressif training.
You can join the esp-rs community on Matrix for all technical questions and issues! The community is open to everyone.
The board
Examples shown here usually apply to ESP32-C3 using the ESP32-C3-DevKit-RUST-1 board.
You can use any SoC supported by no_std
but smaller code and configuration changes might be needed.
Rust knowledge
- Basic Rust like The Rust Book Chapters 1 - 6, Chapter 4 Ownership, does not need to be fully understood.
- The Rust on ESP Book is not required, but it is highly recommended, as it can help you understand the Rust on ESP ecosystem and many of the concepts that will be discussed during the training.
Preparations
This chapter contains information about the course material, the required hardware, and an installation guide.
Icons and Formatting We Use
We use Icons to mark different kinds of information in the book:
- ✅ Call for action.
- ⚠️ Warnings, details that require special attention.
- 🔎 Knowledge that dives deeper into a subject but which you are not required to understand, proceeding.
- 💡 Hints that might help you during the exercises
Example note: Notes like this one contain helpful information
Code Annotations
In some Rust files, you can find some anchor comments:
// ANCHOR: test
let foo = 1;
...
// ANCHOR_END: test
Anchor comments can be ignored, they are only used to introduce those parts of code in this book. See mdBook
documentation
Required Hardware
- Rust ESP Board: available on Mouser, Aliexpress. Full list of vendors.
- USB-C cable suitable to connect the board to your development computer.
- Wi-Fi access point connected to the Internet.
No additional debugger/probe hardware is required.
Simulating Projects
Certain projects can be simulated with Wokwi. Look for indications in the book to identify projects available for simulation. Simulation can be accomplished through two methods:
- Using wokwi.com: Conduct the build process and code editing directly through the browser.
- Using Wokwi VS Code extension: Leverage VS Code to edit projects and perform builds. Utilize the Wokwi VS Code extension to simulate the resulting binaries.
- This approach requires some installation
- This approach assumes that the project is built in debug mode
- This approach allows debugging the project
Companion Material
Checking the hardware
Connect the Espressif Rust Board to your computer. Verify, a tiny red control LED lights up.
The device should also expose its UART serial port over USB:
Windows: a USB Serial Device (COM port) in the Device Manager under the Ports section.
Linux: a USB device under lsusb
.
The device will have a VID (vendor ID) of 303a
and a PID (product ID) of 1001
-- the 0x
prefix will be omitted in the output of lsusb
:
$ lsusb | grep USB
Bus 006 Device 035: ID 303a:1001 Espressif USB JTAG/serial debug unit
Another way to see the device is to see which permissions and port is associated to the device is to check the /by-id
folder:
$ ls -l /dev/serial/by-id
lrwxrwxrwx 1 root root .... usb-Espressif_USB_JTAG_serial_debug_unit_60:55:F9:C0:27:18-if00 -> ../../ttyACM0
If you are using a ESP32-C3-DevKitC-02 the command is
$ ls /dev/ttyUSB*
macOS: The device will show up as part of the USB tree in system_profiler
:
$ system_profiler SPUSBDataType | grep -A 11 "USB JTAG"
USB JTAG/serial debug unit:
Product ID: 0x1001
Vendor ID: 0x303a
(...)
The device will also show up in the /dev
directory as a tty.usbmodem
device:
$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem0
Software
Follow the steps below for a default installation of the ESP32-C3 platform tooling.
🔎 Should you desire a customized installation (e.g. building parts from source, or adding support for Xtensa targets), instructions for doing so can be found in the Installation chapter of the Rust on ESP Book.
Rust toolchain
✅ If you haven't got Rust on your computer, obtain it via https://rustup.rs/
✅ Install Rust stable channel, if you haven't already, and add support for the target architecture using the following command:
rustup toolchain install stable --component rust-src --target riscv32imc-unknown-none-elf
🔎 Rust is capable of cross-compiling to any supported target (see rustup target list
). By default, only the native architecture of your system is installed.
Espressif toolchain
Several tools are required:
cargo-espflash
- upload firmware to the microcontroller and open serial monitor with cargo integrationespflash
- upload firmware to the microcontroller and open serial monitor
✅ Install them with the following command:
cargo install cargo-espflash espflash
⚠️ The espflash
and cargo-espflash
commands listed in the book assume version is >= 2
Toolchain dependencies
Debian/Ubuntu
sudo apt install llvm-dev libclang-dev clang
macOS
When using the Homebrew package manager, which we recommend:
brew install llvm
Docker
An alternative environment, is to use Docker. The repository contains a Dockerfile
with instructions to install the Rust toolchain, and all required packages. This virtualized environment is designed
to compile the binaries for the Espressif target. Flashing binaries from containers is not possible, hence there are two options:
- Execute flashing commands, e.g.,
cargo-espflash
, on the host system. If proceeding with this option, it's recommended to keep two terminals open:- In the container: compile the project
- On the host: use the
cargo-espflash
sub-command to flash the program onto the embedded hardware
- Use
web-flash
crate to flash the resulting binaries from the container. The container already includesweb-flash
. Here is how you would flash the build output ofhello-world
project:web-flash --chip esp32c3 target/riscv32imc-unknown-none-elf/release/hello-world
✅ Install Docker
for your operating system.
✅ Get the docker image: There are 2 ways of getting the Docker image:
- Build the Docker image from the
Dockerfile
:
Building the image takes a while depending on the OS & hardware (20-30 minutes).docker image build --tag rust-std-training --file .devcontainer/Dockerfile .
- Download it from Dockerhub:
docker pull espressif/rust-std-training
✅ Start the new Docker container:
docker run --mount type=bind,source="$(pwd)",target=/workspace,consistency=cached -it rust-std-training /bin/bash
This starts an interactive shell in the Docker container. It also mounts the local repository to a folder
named /workspace
inside the container. Changes to the project on the host system are reflected inside the container & vice versa.
Additional Software
VS Code
One editor with good Rust support is VS Code, which is available for most platforms. When using VS Code, we recommend the following extensions to help during the development.
Rust Analyzer
to provide code completion & navigationEven Better TOML
for editing TOML based configuration files
There are a few more useful extensions for advanced usage
VS Code & Devcontainer
One extension for VS Code that might be helpful to develop inside a Docker container is Remote Containers
.
It uses the same Dockerfile
as the Docker setup, but builds the image and connects to it from within VS Code.
Once the extension is installed, VS Code recognizes the configuration in the .devcontainer
folder. Use the Remote Containers - Reopen in Container
command to connect VS Code to the container.
Workshop repository
The entire material can be found at https://github.com/esp-rs/no_std-training.
✅ Clone and change into the workshop repository:
git clone "https://github.com/esp-rs/no_std-training.git"
cd no_std-training
❗ Windows users may have problems with long path names. Follow these steps to substitute the path:
git clone https://github.com/esp-rs/no_std-training.git
subst r: no_std-trainings
cd r:\
Repository contents
book/
: markdown sources of this bookintro/
: code examples and exercises for the introduction course
Hello World
The hello-world
example is basically a project generated with esp-generate. Templates are already covered in The Rust on ESP Book, see Generating Projects from Templates chapter for more details on how to generate a project from the esp-generate, and Understanding esp-generate for detail on what is inside the template project.
Since we already have the code for this example, let's use it to do a consistency check!
✅ Connect the USB-C port of the board to your computer and enter the hello-world
directory in the workshop repository:
cd intro/hello-world
✅ Build, flash, and monitor the project:
$ cargo run --release
(...)
Finished release [optimized] target(s) in 1.78s
(...)
Chip type: esp32c3 (revision v0.3)
Crystal frequency: 40MHz
Flash size: 4MB
Features: WiFi, BLE
MAC address: 60:55:f9:c0:39:7c
App/part. size: 210,608/4,128,768 bytes, 5.10%
[00:00:00] [========================================] 13/13 0x0
[00:00:00] [========================================] 1/1 0x8000
[00:00:01] [========================================] 67/67 0x10000
[00:00:01] [========================================] 67/67 0x10000
[2023-07-07T08:16:32Z INFO ] Flashing has completed!
Commands:
CTRL+R Reset chip
CTRL+C Exit
(...)
(...)
INFO - Hello world!
🔎 If
cargo run
has been successful, you can exit withctrl+C
.
🔎
cargo run
is configured to useespflash
as custom runner. The same output can be achieved via:
- Using
cargo-espflash
:cargo espflash flash --release --monitor
- Building your project and flashing it with
espflash
:cargo build --release && espflash target/riscv32imc-unknown-none-elf/release/hello_world
This modification is applied to all the projects in the training for convenience.
💡 By default espflash will use a baud-rate of 115200 which is quite conservative. An easy way to increase the baud-rate is setting the environment variable
ESPFLASH_BAUD
to e.g. 921600
Simulation
This project is available for simulation through two methods:
- Wokwi project
- Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
, and chooseintro/hello-world/wokwi.toml
. - Build your project.
- Press F1 again and select
Wokwi: Start Simulator
.
- Press F1, select
Intro Workshop
This workshop guides you through the basics of embedded development, at the end of the workshop we will be able to interact with the outside world. The content includes:
- A
panic
example - A blinky example
- A button example
- A button with interrupt example
Preparations
Please go through the preparations chapter to prepare for this workshop.
Panic!
When something goes terribly wrong in Rust there might occur a panic.
Setup
✅ Go to intro/panic
directory.
✅ Open the prepared project skeleton in intro/panic
.
✅ Open the docs for this project with the following command:
cargo doc --open
intro/panic/examples/panic.rs
contains the solution. You can run it with the following command:
cargo run --example panic
Exercise
✅ In main.rs
add a panic!
somewhere, e.g. after our println
✅ Run the code
cargo run
We see where the panic occurred, and we even see a backtrace!
While in this example things are obvious, this will come in handy in more complex code.
✅ Now try running the code compiled with release profile.
cargo run --release
Now things are less pretty:
!! A panic occured in 'examples\panic.rs', at line 15, column 5:
This is a panic
Backtrace:
0x42000100
0x42000100 - _start_rust
at ??:??
We still see where the panic occurred, but the backtrace is less helpful now.
That is because the compiler omitted debug information and optimized the code, you might have noticed the difference in the size of the flashed binary.
Generally you want to use release
always. To get a more helpful backtrace when using the release
profile you can add this to your .cargo/config.toml
[profile.release]
debug = true
This will include debug information in the ELF file - but that won't get flashed to the target so it's something you can and should always use.
If you are reusing this project for other exercises, be sure to remove the line causing the explicit panic.
Simulation
This project is available for simulation through two methods:
- Wokwi projects:
- Exercise: Currently not available
- Solution
- Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
and chooseintro/panic/wokwi.toml
- Edit the
wokwi.toml
file to select between exercise and solution simulation
- Edit the
- Build you project
- Press F1 again and select
Wokwi: Start Simulator
- Press F1, select
Blinky
Let's see how to create the iconic Blinky.
Setup
✅ Go to intro/blinky
directory.
✅ Open the prepared project skeleton in intro/blinky
.
✅ Open the docs for this project with the following command:
cargo doc --open
intro/blinky/examples/blinky.rs
contains the solution. You can run it with the following command:
cargo run --release --example blinky
Exercise
On ESP32-C3-DevKit-RUST-1 there is a regular LED connected to GPIO 7. If you use another board consult the data-sheet.
Note that most of the development boards from Espressif today use an addressable LED which works differently and is beyond the scope of this book. In that case, you can also connect a regular LED to some of the free pins (and don't forget to add a resistor).
✅ Initiate the Io peripheral, and create a led
variable from GPIO connected to the LED, using the
into_push_pull_output
function.
Here we see that we can drive the pin high
, low
, or toggle
it.
We also see that the HAL offers a way to delay execution.
✅ Initialize a Delay instance.
✅ Using the [toggle()
][toggle] and delay_ms()
methods, make the LED blink every 500 ms.
Simulation
This project is available for simulation through two methods:
- Wokwi projects:
- Exercise: Currently not available
- Solution
- Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
and chooseintro/blinky/wokwi.toml
- Edit the
wokwi.toml
file to select between exercise and solution simulation
- Edit the
- Build you project
- Press F1 again and select
Wokwi: Start Simulator
- Press F1, select
Detect a button press
We are now going to make the LED ligth only when we press a button, we will create a project that reads the state of the button GPIO and reacts to its state.
Setup
✅ Go to intro/button
directory.
✅ Open the prepared project skeleton in intro/button
.
✅ Open the docs for this project with the following command:
cargo doc --open
intro/button/examples/button.rs
contains the solution. You can run it with the following command:
cargo run --release --example button
Exercise
Most of the dev-boards have a button, in our case, we will use the one labeled BOOT
on GPIO9
.
✅ Initiate the Io peripheral, and create variable for the LED and button, the LED can be created using the
into_push_pull_output
function as before while the button can be obtained using
into_pull_up_input
function.
Similarly to turning a GPIO
into an output
we can turn it into an input
. Then we can get the current state of the input
pin with is_high
and similar functions.
✅ In the loop
, add some logic so if the button is not pressed, the LED is lit. If the button is pressed, the LED is off.
Simulation
This project is available for simulation through two methods:
- Wokwi projects:
- Exercise: Currently not available
- Solution
- Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
and chooseintro/button/wokwi.toml
- Edit the
wokwi.toml
file to select between exercise and solution simulation
- Edit the
- Build you project
- Press F1 again and select
Wokwi: Start Simulator
- Press F1, select
Detect a button press with interrupt
Now, instead of polling the button pin, we will use interrupts. Interrupts offer a mechanism by which the processor handles asynchronous events and fatal errors.
Setup
✅ Go to intro/button-interrupt
directory.
✅ Open the prepared project skeleton in intro/button-interrupt
.
✅ Open the docs for this project with the following command:
cargo doc --open
intro/button-interrupt/examples/button-interrupt.rs
contains the solution. You can run it with the following command:
cargo run --release --example button-interrupt
Exercise
Inspecting the code, the first thing we notice is the static BUTTON
. We need it since in the interrupt handler we have to clear the pending interrupt on the button and we somehow need to pass the button from main to the interrupt handler.
Since an interrupt handler can't have arguments we need a static to get the button into the interrupt handler.
We need the Mutex
to make access to the button safe.
Please note that this is not the Mutex you might know from
libstd
but it's the Mutex fromcritical-section
(and that's why we need to add it as a dependency).
✅ We need to set the interrupt handler for the GPIO interrupts.
✅ Let's add a critical-section
, using the with()
method and enable an interrupt for falling edges:
critical_section::with(|cs| {
button.listen(Event::FallingEdge);
BUTTON.borrow_ref_mut(cs).replace(button)
});
In this line we move our button into the static BUTTON
for the interrupt handler to get hold of it.
The code running inside the critical_section::with
closure runs within a critical section,
cs
is a token that you can use to "prove" that to some API.
The interrupt handler is defined via the #[handler]
macro.
Here, the name of the function must match the interrupt.
Simulation
This project is available for simulation through two methods:
- Wokwi projects:
- Exercise: Currently not available
- Solution
- Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
and chooseintro/button-interrupt/wokwi.toml
- Edit the
wokwi.toml
file to select between exercise and solution simulation
- Edit the
- Build you project
- Press F1 again and select
Wokwi: Start Simulator
- Press F1, select
Direct Memory Access (DMA)
The DMA peripheral is used to perform memory transfers in parallel to the work of the processor (the execution of the main program).
In this chapter you will learn how to use DMA with esp-hal. For the example we are going to use the SPI peripheral.
Setup
✅ Go to intro/dma
directory.
✅ Open the prepared project skeleton in intro/dma
.
✅ Open the docs for this project with the following command:
cargo doc --open
intro/dma/examples/dma.rs
contains the solution. You can run it with the following command:
cargo run --release --example dma
Exercise
The project skeleton contains code to transfer a small amount of data via SPI. To make it easy to explore the example you can connect GPIO4 and GPIO2 - this way the data we send is also the data we receive.
The blocking SPI transfer looks like this
// To transfer much larger amounts of data we can use DMA and
// the CPU can even do other things while the transfer is in progress
let mut data = [0x01u8, 0x02, 0x03, 0x04];
spi.transfer(&mut data).unwrap();
The data
array in this case serves as the data to transmit as well as the buffer to receive data.
✅ First thing we need to use DMA is initializing the DMA peripheral driver and getting a channel.
We also need to create a buffer for data we want to send as well as a separate buffer for the data we will receive.
// we need to create the DMA driver and get a channel
let dma = Dma::new(peripherals.DMA);
let dma_channel = dma.channel0;
// DMA transfers need descriptors and buffers
let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(32000);
let mut dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap();
let mut dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap();
There are also descriptors needed. That is because internally the DMA peripheral uses a linked list for the transfer and that is what the descriptors are needed for.
For convenience we use the dma_buffers!
macro to create the buffers and descriptors.
🔎 You could use
cargo expand
or Rust Analyzer'sExpand macro recursively
command to see what code the macro expands to
✅ Next we need to configure the SPI peripheral driver to use DMA
We need to call .with_dma
passing a configured DMA channel.
To configure a DMA channel we call configure
to enable burst-mode, pass the descriptors and set the priority.
// we can call `.with_dma` on the SPI driver to make it use DMA
let mut spi = Spi::new(peripherals.SPI2, 100.kHz(), SpiMode::Mode0)
.with_pins(sclk, mosi, miso, cs)
.with_dma(dma_channel.configure(false, DmaPriority::Priority0));
✅ Now we are ready to start a DMA enabled SPI transfer
Now we need to pass the buffers to transmit and receive individually. Please note that we now get a DMA transfer
from calling transmit
.
// `dma_transfer` will move the driver and the buffers into the
// returned transfer.
let transfer = spi
.dma_transfer(dma_rx_buf, dma_tx_buf)
.map_err(|e| e.0)
.unwrap();
What happens here is that the buffers and the SPI driver are moved into the Transfer
we get. This way the buffers and the driver are inaccessible during the transfer.
Now we are free to let the CPU do other things while the SPI transfer is in progress.
✅ Wait for the transfer to complete and get back the buffers and the driver instance
As mentioned before the buffers and the driver are moved into the Transfer
.
// the buffers and spi are moved into the transfer and
// we can get it back via `wait`
// if the transfer isn't completed this will block
(spi, (dma_rx_buf, dma_tx_buf)) = transfer.wait();
We call wait
on the Transfer
. It will block until the transfer is done and we get our SPI driver and the buffers.
While using DMA needs more effort than letting the CPU do all the work it's not too complex once the general idea is understood.
⚠️ You might assume that it's always preferable to use DMA.
That's not the case: Setting up a DMA transfer consumes more CPU cycles than setting up a blocking transfer. Especially if the amount of data to transfer is small this might hurt performance a lot. Also, if there is nothing to do for the CPU other than waiting for the transfer to finish it's preferable to use a blocking transfer.
However, if the amount of data to transfer is bigger, then the time needed to setup the transfer is negligible compared to the time the CPU could use to do useful things in parallel.
HTTP Client
Next, we'll write a small client that retrieves data over an HTTP connection to the internet.
For demonstration purposes we implement the http client ourselves. Usually you want to use e.g. reqwless
or edge-net
Before jumping to the exercise, let's explore how Wi-Fi works in no_std
Rust for Espressif devices.
Wi-Fi Ecosystem
Wi-Fi support comes in the esp-wifi
crate. The esp-wifi
is home to the Wi-Fi, Bluetooth and ESP-NOW driver implementations for no_std
Rust.
Check the repository README for current support, limitations and usage details.
There are some other relevant crates, on which esp-wifi
depends on:
smol-tcp
: Event-driven TCP/IP stack implementation.- It does not require heap allocation (which is a requirement for some
no_std
projects) - For more information about the crate, see the official documentation
- It does not require heap allocation (which is a requirement for some
Additionally, when using async, embassy-net
is relevant.
Setup
✅ Go to intro/http-client
directory.
✅ Open the prepared project skeleton in intro/http-client
.
✅ Add your network credentials: Set the SSID
and PASSWORD
environment variables.
intro/http-client/examples/http-client.rs
contains the solution. You can run it with the following command:
cargo run --release --example http-client
✅ Read the Optimization Level section of the esp-wifi
README.
Exercise
✅ Bump the clock
frequency at which the target operates to its maximum. Consider using ClockControl::configure
or ClockControl::max
✅ Create a timer
and initialize the Wi-Fi
let timg0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG0);
let init = init(
EspWifiInitFor::Wifi,
timg0.timer0,
Rng::new(peripherals.RNG),
peripherals.RADIO_CLK,
)
.unwrap();
✅ Configure Wi-Fi using Station Mode
let mut wifi = peripherals.WIFI;
let mut socket_set_entries: [SocketStorage; 3] = Default::default();
let (iface, device, mut controller, sockets) =
create_network_interface(&init, &mut wifi, WifiStaDevice, &mut socket_set_entries).unwrap();
✅ Create a Client with your Wi-Fi credentials and default configuration. Look for a suitable constructor in the documentation.
let client_config = Configuration::Client(ClientConfiguration {
....
});
let res = controller.set_configuration(&client_config);
println!("Wi-Fi set_configuration returned {:?}", res);
✅ Start the Wi-Fi controller, scan the available networks, and try to connect to the one we set.
controller.start().unwrap();
println!("Is wifi started: {:?}", controller.is_started());
println!("Start Wifi Scan");
let res: Result<(heapless::Vec<AccessPointInfo, 10>, usize), WifiError> = controller.scan_n();
if let Ok((res, _count)) = res {
for ap in res {
println!("{:?}", ap);
}
}
println!("{:?}", controller.get_capabilities());
println!("Wi-Fi connect: {:?}", controller.connect());
// Wait to get connected
println!("Wait to get connected");
loop {
let res = controller.is_connected();
match res {
Ok(connected) => {
if connected {
break;
}
}
Err(err) => {
println!("{:?}", err);
loop {}
}
}
}
println!("{:?}", controller.is_connected());
✅ Then we obtain the assigned IP
// Wait for getting an ip address
let now = || time::now().duration_since_epoch().to_millis();
let wifi_stack = WifiStack::new(iface, device, sockets, now);
println!("Wait to get an ip address");
loop {
wifi_stack.work();
if wifi_stack.is_iface_up() {
println!("got ip {:?}", wifi_stack.get_ip_info());
break;
}
}
If the connection succeeds, we proceed with the last part, making the HTTP request.
By default, only unencrypted HTTP is available, which limits our options of hosts to connect to. We're going to use www.mobile-j.de/
.
To make an HTTP request, we first need to open a socket, and write to it the GET request,
✅ Open a socket with the following IPv4 address 142.250.185.115
and port 80
. See IpAddress::Ipv4
documentation.
✅ write
the following message to the socket and flush
it: b"GET / HTTP/1.0\r\nHost: www.mobile-j.de\r\n\r\n"
✅ Then we wait for the response and read it out.
let deadline = time::now() + Duration::secs(20);
let mut buffer = [0u8; 512];
while let Ok(len) = socket.read(&mut buffer) {
let to_print = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) };
print!("{}", to_print);
if time::now() > deadline {
println!("Timeout");
break;
}
}
println!();
✅ Finally, we will close the socket and wait
socket.disconnect();
let deadline = time::now() + Duration::secs(5);
while time::now() < deadline {
socket.work();
}
Simulation
This project is available for simulation through two methods:
- Wokwi projects:
- Exercise: Currently not available
- Solution: Currently not available
- Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
- Press F1, select
Wokwi: Select Config File
and chooseintro/http-client/wokwi.toml
- Edit the
wokwi.toml
file to select between exercise and solution simulation
- Edit the
- Build you project
- Press F1 again and select
Wokwi: Start Simulator
- Press F1, select
defmt
In this chapter, we will cover defmt
, a highly efficient logging framework, and how to use it in the no_std
environment.
defmt
Ecosystem
esp-println
, esp-backtrace
and espflash
/cargo-espflash
provide mechanisms to use defmt
:
espflash
has support for different logging formats, one of them beingdefmt
.espflash
requires framming bytes as when usingdefmt
it also needs to print non-defmt
messages, like the bootloader prints.- It's important to note that other
defmt
-enabled tools likeprobe-rs
won't be able to parse these messages due to the extra framing bytes.
- It's important to note that other
- Uses rzcobs encoding
esp-println
has adefmt-espflash
feature, which adds framming bytes soespflash
knows that is adefmt
message.esp-backtrace
has adefmt
feature that usesdefmt
logging to print panic and exception handler messages.
Setup
✅ Go to intro/defmt
directory.
✅ Open the prepared project skeleton in intro/defmt
.
intro/defmt/examples/defmt.rs
contains the solution. You can run it with the following command:
cargo run --release --example defmt
Exercise
✅ Make sure the defmt-espflash
feature of esp-println
is enabled.
✅ Make sure the defmt
feature of esp-backtrace
is enabled.
✅ Update the linking process in the .cargo/config.toml
.
✅ Make sure, the defmt
crate is added to the dependencies.
✅ Make sure you are building esp_println
and esp_backtrace
use esp_backtrace as _;
use esp_println as _;
✅ Use the defmt::println!
or any of the logging defmt
macros to print a message.
- If you want to use any of the logging macros like
info
,debug
- Enable the
log
feature ofesp-println
- When building the app, set
DEFMT_LOG
level.
- Enable the
✅ Add a panic!
macro to trigger a panic with a defmt
message.
Advanced Examples
In this chapter you will learn about more advanced features and concepts.
Stack Overflow Detection
Rust is well known for its memory safety. Whenever possible the compiler enforces memory safety at compile.
However, the situation is different in regards to the stack memory. It's impossible to check this at compile time and even at runtime this can be difficult.
The stack is usually placed at the top of the available memory and grows from top (high addresses) to bottom (low addresses).
On desktop operating systems there are measures to prevent overflowing the stack. Also, an RTOS might include mechanisms to check for stack overflows.
In bare-metal however there is no common way to implement stack protection.
On some platforms it's done by moving the stack to the start of the RAM so that when the stack grows above its bounding an access fault will occur. We cannot do that because on our chips there is the flash/ext-mem cache at the start of RAM which we definitely shouldn't touch.
🔎 On ESP32-C6/ESP32-H2 cache is not located in the start of RAM which means we can move the stack there. esp-hal offers the feature
flip-link
which will do that and you get stack-overflow protection "for free".
🔎 esp-hal also supports stack smashing protection for all targets which in our case can also double as a simple stack overflow detector. While the overhead is very small, there is some run-time cost involved.
To enable it you need a nightly compiler and add
"-Z", "stack-protector=all",
torustflags
in.cargo/config.toml
Some of our chips (including ESP32-C3) include the debug-assist peripheral.
This peripheral can monitor the stack-pointer and detect read and/or write access to specified memory areas.
We could just use the stack-pointer monitoring which will work well as long as we don't use esp-wifi
.
The reason we cannot use that with esp-wifi
is that it runs multiple tasks by quickly switching between them which includes switching stacks. In that case the stack bounds check will trigger as soon as we switch the running task for the first time.
What we can do however is defining a protected area at the bottom of the stack and detect read and write access to it. As soon as the stack grows into this area, we will detect this.
It is important to define this area larger (ideally twice) than the largest stack allocation we expect. Otherwise, it's possible that code will start writing to memory below the stack - possibly overwriting sensitive static data or even code residing in RAM before we can detect access to the monitored memory area.
For X86 LLVM supports probestack which would allow us to use a smaller safe-area. However, this feature currently isn't available for our target platforms.
We can also test for the current stack usage by temporarily increasing the safe area until we see the stack memory protection trigger.
Setup
✅ Go to advanced/stack-overflow-detection
directory.
✅ Open the prepared project skeleton in advanced/stack-overflow-detection
.
✅ Open the docs for this project with the following command:
cargo doc --open
✅ Run the code
cargo run
You will see the application crash with an Illegal instruction
exception. This is because the recursive function is placed in RAM.
If you change it to run from flash you won't see a crash but the application will just freeze after printing a weird counter number.
In this case it's easy to guess the cause of this behavior however in a real world application you probably won't know what exactly happened.
advanced/stack-overflow-detection/examples/stack-overflow-detection.rs
contains the solution. You can run it with the following command:
cargo run --release --example stack-overflow-detection
Exercise
✅ Create a function which will set up the safe memory area and enables the appropriate interrupt
The function will take the DebugAssist
peripheral driver and the size of the safe-area.
It should move the DebugAssist
into a static variable.
The resulting function should look like this
static DA: Mutex<RefCell<Option<DebugAssist>>> = Mutex::new(RefCell::new(None));
fn install_stack_guard(mut da: DebugAssist<'static>, safe_area_size: u32) {
extern "C" {
static mut _stack_end: u32;
static mut _stack_start: u32;
}
let stack_low = unsafe { (&mut _stack_end as *mut _ as *mut u32) as u32 };
let stack_high = unsafe { (&mut _stack_start as *mut _ as *mut u32) as u32 };
println!(
"Safe stack {} bytes",
stack_high - stack_low - safe_area_size
);
da.enable_region0_monitor(stack_low, stack_low + safe_area_size, true, true);
critical_section::with(|cs| DA.borrow_ref_mut(cs).replace(da));
}
There is quite a lot going on here but most of this is setting up the interrupt. You should recognize most of this from the interrupt exercise in the previous chapter.
The most interesting part is probably da.enable_region0_monitor(stack_low, stack_low + safe_area_size, true, true)
.
This actually configures the region to monitor as well as setting it up to trigger on reads and writes to that region.
Another interesting part here is how we can get the top and bottom address of the stack from symbols created by the linker script.
✅ Create the interrupt handler
As you probably remember from the introduction to interrupts we can define the interrupt handler by using the #[interrupt]
attribute macro.
The name of the function needs to match the name of the interrupt.
#[handler(priority = esp_hal::interrupt::Priority::min())]
fn interrupt_handler() {
...
Next, we need to get access to the debug assist peripheral driver which we stored in the static variable.
We need it to get the address where the access to the monitored memory region happened.
Printing this address will enable espflash
to print the name of the function. Similar to how stack traces are printed.
We can also clear the pending interrupt and disable region monitoring here. It's not strictly needed since we won't return from the interrupt handler.
It is unfortunately not possible to generate a stack trace here since the stack is not in a correct state and we don't know the stack frame from which we can start generating the backtrace.
The whole function should look like this
#[handler(priority = esp_hal::interrupt::Priority::min())]
fn interrupt_handler() {
critical_section::with(|cs| {
println!("\n\nPossible Stack Overflow Detected");
let mut da = DA.borrow_ref_mut(cs);
let da = da.as_mut().unwrap();
if da.is_region0_monitor_interrupt_set() {
let pc = da.get_region_monitor_pc();
println!("PC = 0x{:x}", pc);
da.clear_region0_monitor_interrupt();
da.disable_region0_monitor();
loop {}
}
});
}