Introduction

The goal of this book is to provide a comprehensive guide on using the Rust programming language with Espressif SoCs and modules.

Rust support for these devices is still a work in progress, and progress is being made rapidly. Because of this, parts of this documentation may be out of date or change dramatically between readings.

For tools and libraries relating to Rust on ESP, please see the esp-rs organization on GitHub.

A note in SoCs support.

The content of the books applies to ESP32, ESP32-S, and ESP32-C series of SoCs. ESP8266 series is out of the scope of this book, support for Rust in ESP8266 is limited and only some parts of the book are applicable for this SoC.

Status of This Book

This book is currently a work in progress. A number of sections may be missing information or be missing altogether. If there is a specific topic you would like to see documented please open an issue.

If you feel you can contribute something to this book, we encourage you to create a pull request!

Who This Book is For

This book assumes some experience with embedded development and the Rust programming language. Teaching these topics is outside the scope of this book.

If you are unfamiliar with either topic, please refer to the resources listed below to help you get started.

Additional Resources

Some additional resources can be found below which may prove useful for those less experienced with embedded Rust.

ResourceDescription
The Rust Programming LanguageIf you are not familiar with Rust we recommend reading this book first.
The Embedded Rust BookHere you can find several other resources provided by Rust's Embedded Working Group.
The EmbedonomiconThe nitty gritty details when doing embedded programming in Rust.

Ecosystem Overview

There are two approaches for using Rust on Espressif chips:

  1. With the full standard library available (std)
  2. Without the standard library available (no_std)

Both approaches have their advantages and disadvantages, so you should make a decision based on your project's needs. This chapter contains an overview of the two approaches followed by a brief comparison between them.

The esp-rs organization on GitHub is home to a number of repositories related to running Rust on Espressif chips. Most of the required crates have their source code hosted here.

A note on the repository naming convention In the esp-rs organization we use the following wording:

  • Repositories starting with esp-idf- are focused on std approach. E.g. esp-idf-hal
  • Repositories starting with esp- are focused on no_std approach. E.g. esp-hal

Using the Rust Standard Library (std)

Espressif provides a C-based development framework called esp-idf which has support for all Espressif chips starting with the ESP32; note that this framework does not support the ESP8266.

esp-idf in turn provides a newlib environment with enough functionality to build the Rust standard library (std) on top of it. This is the approach that is being taken to enable std support on ESP devices.

Chip Support

For applications targeting std to be built for ESP devices, two things are required:

  1. LLVM/Clang support
  2. Support for the device in esp-idf

Refer to the table below to see if your chip is supported.

ChipSupported?
ESP32
ESP32-C2planned
ESP32-C3
ESP32-S2
ESP32-S3
ESP32-H2planned
ESP8266

Since esp-idf does not have support for the ESP8266, we unfortunately are unable to use these libraries with it. See the Bare Metal page for information on the no_std HAL for this chip.

Standard Library Features

The supported std features are as follows:

  • Threads
  • Mutexes and other synchronization primitives
  • Collections
  • Random number generation
  • Sockets

In addition to the std features, there is an embedded-svc implementation for esp-idf, esp-idf-svc, which adds extra support for services/modules not available in the standard library, including:

  • Wi-Fi management
  • NVS (non-volatile storage)
  • Networking services like httpd and ping

In general, this approach should feel quite similar to developing for most normal PC environments.

Relevant esp-rs crates

RepositoryDescription
esp-rs/esp-idf-halAn implementation of the embedded-hal and other traits using the esp-idf framework.
esp-rs/esp-idf-svcAn implementation of embedded-svc using esp-idf drivers.
esp-rs/esp-idf-sysRust bindings to the esp-idf development framework. Gives raw (unsafe) access to drivers, Wi-Fi and more.
esp-rs/embedded-svcAbstraction traits for embedded services. (WiFi, Network, Httpd, Logging, etc.)

The aforementioned crates have interdependencies, and this relationship can be seen below.

graph TD;
    esp-idf-hal --> esp-idf-sys & embedded-svc
    esp-idf-svc --> esp-idf-sys & esp-idf-hal & embedded-svc

FAQ

I updated my sdkconfig.defaults file but it doesn't appear to have had any effect

You must clean your project and rebuild for changes in the sdkconfig.defaults to take effect:

cargo clean
cargo build

The documentation for the crates mentioned on this page is out of date or missing

Due to the resource limits imposed by docs.rs, internet access is blocked while building documentation and as such we are unable to build the documentation for esp-idf-sys or any crate depending on it.

Instead, we are building the documentation and hosting it ourselves on GitHub Pages:

***ERROR*** A stack overflow in task main has been detected.

If the second-stage bootloader reports this error, you likely need to increase the stack size for the main task. This can be accomplished by adding the following to the sdkconfig.defaults file:

CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000

In this example, we are allocating 7kB for the main task's stack.

How can I completely disable the watchdog timer(s)?

Add to your sdkconfig.defaults file:

CONFIG_INT_WDT=n
CONFIG_ESP_TASK_WDT=n

Recall that you must clean your project before rebuilding when modifying these configuration files.

Bare Metal (no_std)

Using no_std may be more familiar to embedded Rust developers; it does not use std (the Rust standard library) but instead uses a subset, the core library. The official Rust embedded book has a great section on this.

It's important to note that in general a no_std crate can always compile in std environment but the inverse is not true. Therefore, when creating crates it's worth keeping in mind if it needs the standard library to function.

Hardware Abstraction Layers

Previously, the primary focus of no_std development was the ESP32 and (to a lesser extent) the ESP8266.

Now there is a renewed effort to implement no_std support for the entire lineup of Espressif devices from the ESP32 and newer. These new HALs can be found in the esp-rs/esp-hal repository.

There is also some level of support for Wi-Fi and Bluetooth via esp-rs/esp-wifi for ESP32, ESP32-C3, ESP32-S2, and ESP32-S3.

Chip Support

Chip support for no_std requires LLVM/Clang support just like for std. However, this has no dependency on esp-idf. In addition to compiler support, it's necessary to have peripheral access crates (PAC) and hardware abstraction layers (HAL) for your desired chip.

Refer to the table below to see if your chip is supported. Please note that the no_std HALs are still in the early phases of development, so not all peripherals have had drivers implemented.

ChipPACHAL
ESP32
ESP32-C2
ESP32-C3
ESP32-C6plannedplanned
ESP32-S2
ESP32-S3
ESP32-H2plannedplanned
ESP8266

Relevant esp-rs Crates

RepositoryDescription
esp-rs/esp-pacsA monorepo containing PACs for each supported device.
esp-rs/esp-halAn implementation of the embedded-hal traits and more for the ESP32, ESP32-C2, ESP32-C3, ESP32-S2, and ESP32-S3.
esp-rs/esp8266-halAn implementation of the embedded-hal traits and more for the ESP8266.
esp-rs/esp-allocA simple no_std heap allocator.
esp-rs/esp-printlnProvides print! and println!.
esp-rs/esp-backtraceBacktrace support for bare-metal applications.
esp-rs/esp-storageImplementation of embedded-storage traits to access unencrypted flash memory.
esp-rs/esp-wifiWIP: Experimental Wifi and Bluetooth LE support.

Comparing std and no_std

There are several factors that must be considered when choosing between std (esp-idf-hal) and no_std (eg. esp-hal). As stated previously, each approach has its own unique set of advantages and disadvantages. While we can't decide for you, this section will hopefully allow you to make an educated decision.

Application Runtimes

In the case of applications (as opposed to libraries) the standard library provides a runtime that handles setting up stack overflow protection, spawning the main thread before an application's main function is invoked, and handling of command-line arguments.

Applications targeting no_std will be responsible for initializing their own runtimes instead. Runtime initialization is generally handled by an external dependency, in our case the riscv-rt and xtensa-lx-rt libraries. You can refer to their READMEs and documentation for more information.

One advantage of not including the default runtime is that you're able to write applications at a lower level. This is possible because the applications will have been linked against the core crate instead of std, which makes no assumptions about the system it is running on. As such, it's possible to write applications like bootloaders, firmware, or even operating system kernels using the no_std approach.

#![no_main]

Another interesting property of no_std applications is that we cannot use Rust's default main function as our entry point. It makes certain assumptions that are not necessarily valid in an embedded context (for example, it expects that command-line arguments exist).

Because of this, you will often see the #![no_main] attribute used to instruct the Rust compiler not to use the default entry point. Runtime crates will provide an #[entry] attribute which can be used to mark a diverging function as the application's entry point instead. For example, a minimal application might look something like this:

#![no_std]
#![no_main]

use riscv_rt::entry;

#[entry]
fn main() -> ! {
    loop {}
}

Panic Handlers

In addition to specifying the application's entry point, for no_std we must also define a panic handler. The default panic behaviour relies on std, as it prints to standard output.

You can define a panic handler manually using the #[panic_handler] attribute. Note that this function's signature must match the example below.

#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // Your implementation goes here!
}

Alternatively, there are a number of external dependencies which define various panic handlers for us. Some possible choices are panic-halt, panic-semihosting, or panic-never.

These can be used simply by installing the relevant dependency, and then importing the crate:

#![no_std]

use panic_halt as _;

Probably the most convenient panic handler for no_std on esp-hal is esp-backtrace

Rust on ESP targets

With an understanding of the ecosystem surrounding Rust on Espressif chips, we can move on to actual development. If you are not aware of the two possible development approaches or do not understand the differences between writing std and no_std applications, please first read the Ecosystem Overview chapter.

Let's take a moment to discuss the Rust support for the architectures of the Espressif chips in more detail. At this moment, Espressif SoCs are based on two different architectures: RISC-V and Xtensa. The support for those two architectures in the Rust programming language is very different.

Rust in RISC-V targets

The RISC-V architecture has support in the mainline Rust compiler so setup is relatively simple, all we must do is add the appropriate compilation target.

Hence, the dependencies required to develop Rust applications in RISC-V targets are:

  • Rust Toolchain with the proper target: Used to compile our Rust code.
  • LLVM: Used as codegen backend by the Rust compiler.
  • [Optional] GCC Toolchain: GCC linker can be used.
    • GCC is marked as optional as you can also use LLVM linker.

Aditonally when building std applications we also need:

  • ESP-IDF: Espressif IoT Development Framework.
  • ldproxy crate: Simple tool to forward linker arguments given to ldproxy to the actual linker executable. The crate can be found in the esp-rs/embuild repository.

Rust in Xtensa targets

To this day, there is no Xtensa support in the mainline Rust compiler, for this reason, we maintain the esp-rs/rust fork that adds support for our Xtensa targets.

Xtensa not being supported on Rust mainline is mainly a consequence of LLVM not supporting Xtensa targets. For that reason, we also maintain an LLVM fork with support for Espressif Xtensa targets in espressif/llvm-project

Another consequence of LLVM not suporting our Xtensa targets is that we need to provide our own linker. I.e. we'll need to install GCC toolchain to use it as our linker.

A note in upstreaming our forks.

We are trying to upstream the changes in both our LLVM and Rust forks. The first step is to upstream the LLVM project, this is already in progress and you can see the status at this tracking issue. If our LLVM changes are accepted in LLVM mainline, we will proceed with trying to upstream the Rust compiler changes.

The forked compiler can coexist with the standard Rust compiler, so it is possible to have both installed on your system. The forked compiler is invoked when using the esp channel instead of the defaults, stable or nightly.

Hence, the dependencies required to develop Rust applications in Xtensa targets are:

  • Rust Toolchain: Used to compile our Rust code.
    • We need to use our custom fork with Xtensa support.
  • LLVM: Used as codegen backend by the Rust compiler.
    • We need to use our custom fork with Xtensa support.
  • GCC Toolchain: GCC linker is used in our Rust applications as it supports all our targets architectures (Xtensa and RISC-V).

Aditonally when building std applications we also need:

  • ESP-IDF: Espressif IoT Development Framework.
  • ldproxy crate: Simple tool to forward linker arguments given to ldproxy to the actual linker executable. The crate can be found in the esp-rs/embuild repository.

This chapter, will cover how to properly install the correct Rust compiler and toolchain for our ESP chips.

Installing Rust for Espressif SoCs

Rust installation

In order to develop applications for ESP devices using Rust you must first install the Rust compiler along with the appropriate toolchain and target(s). Depending on your device it may be one of two architectures, each requiring a different setup.

If you have not yet installed Rust on your system, you can do so easily using rustup. For macOS and Linux it can be installed by running the following command:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

For installation on Windows or alternative installation methods, please refer to the instructions on the rustup website.

If you are running Windows as your host operating system, you must also install one of the available ABIs:

  • MSVC: This is the recommended ABI. When installing rustup, it will check if all the requirements are installed, and, if they are not, it allows the user to install them.
  • GNU: No checks are done in rustup and expect that the user takes care of properly installing it.

ldproxy

ldproxy crate is required when building applications using the Rust standard library, std. To install:

cargo install ldproxy

This tool is required for std applications regardless of the target architecture.

RISC-V

If you only want to target RISC-V chips, installation is simpler. In order to build applications for RISC-V targets we need to use a Rust nightly toolchain with the rust-src component, both things can be installed with:

rustup toolchain install nightly --component rust-src

There are two suitable targets for this chip:

  • For bare-metal (no_std) applications, use riscv32imc-unknown-none-elf
  • For applications that require std, use riscv32imc-esp-espidf

The bare-metal target can be installed by running:

rustup target add riscv32imc-unknown-none-elf

For std applications, the riscv32imc-esp-espidf target is currently Tier 3 and does not have prebuilt objects distributed through rustup, therefore, it does not need to be installed as riscv32imc-unknown-none-elf. The -Z build-std unstable cargo feature is required within your project, this unstable cargo feature can also be added to .cargo/config.toml of your project. Our template projects, which we will later discuss, already takes care of this.

Also, when building std applications, make sure you have LLVM and ldproxy installed. ESP-IDF will be installed by esp-idf-sys altough you can also use an existing installation, see ESP-IDF installation note.

At this point, you are ready to build applications for all the Espressif chips based on RISC-V architecture.

The installation of Rust for ESP RISC-V targets can also be handled by espup, a tool that will be introduced in the espup section.

Xtensa

Because there is no Xtensa support in the mainline Rust compiler you must use the esp-rs/rust fork instead. There are a few options available for installing this compiler fork.

  • The recommended one is using esp-rs/espup. See espup section for more details.
  • Using esp-rs/rust-build installation scripts. This was the recommended way in the past, but now, the installation scripts are feature frozen and all the new features will only be included in espup. See the repository readme for instructions.
  • Building the Rust compiler with Xtensa support from source. This process is computationally expensive and can take one or more hours to complete depending on your system, for this reason, is not recommended unless there is a major reason to go for this approach. See instructions in the Installing from Source section of the esp-rs/rust repository.

espup

esp-rs/espup is a tool for installing and maintaining the required ecosystem to develop applications in Rust for Espressif SoC's (both Xtensa and RISC-V targets).

espup takes care of installing the proper Rust compiler (our fork in case of Xtensa targets, and the nightly toolchain with the necessary target for RISC-V targets), LLVM toolchain, GCC toolchains, and many other things. For more details, see Usage section of the espup Readme.

In order to install espup:

cargo install espup

It's also possible to directly download the pre-compiled release binaries or to use cargo-binstall.

Once that espup is installed you can simply run:

espup install

And it will install all the necessary tools to develop Rust applications for all supported ESP targets.

espup will create and export file, by default called export-esp.sh on Unix systems and export-esp.ps1 on Windows, this file contains the required environment variables. Please, make sure to source in every terminal before building any application:

# Unix
. $HOME/export-esp.sh
# Windows
%USERPROFILE%\export-esp.ps1

A note in ESP-IDF installation.

We already mentioned that ESP-IDF is required when building std applications but, so far, no instructions on how to install ESP-IDF have been mentioned. That is because esp-idf-sys, a crate that all std applications will use, already takes care of installing the necessary ESP-IDF version. By default, this installation will take place under an .embuild folder inside the project directory.

espup install has a --espidf-version option that allows installing the desired ESP-IDF version, see Install subcommand instructions. When using this argument:

  • ldproxy will be installed if not present already.
  • The export file will include the necessary environment variables for ESP-IDF
  • When building std applications that use the installed version of ESP-IDF, esp-idf-sys will recognize there is an activated ESP-IDF environment and will use it instead of downloading and installing it.
    • This is very helpful when we want to build several projects with the same ESP-IDF version as it saves time and space.
    • Be aware that if you source the environment with an ESP-IDF version and you try to build and std application that uses a different version it will fail to build.

Using Containers

As an alternative to installing the compiler fork to your local system directly, it's also possible to run it inside of a container.

A number of container runtimes are available, and which should be used depends on your operating system. Some of the popular options are:

Espressif provides the idf-rust container image which contains several tags (generated both for linux/arm64 and linux/amd64) for every Rust release:

  • For std applications, the following naming convention is applied: <chip>_<esp-idf-version>_<rust-toolchain-version> . E.g., esp32s3_v4.4_1.64.0.0 contains the ecosystem for developing std applications based on ESP-IDF release/v4.4 for ESP32-S3 with the 1.64.0.0 Rust toolchain.
  • For no_std applications, the naming convention is: <chip>_<rust-toolchain-version>. E.g., esp32_1.64.0.0 contains the ecosystem for developing non_std applications for ESP32 with the 1.64.0.0 Rust toolchain.

There is an all <chip> for both std and no_std tags that contains the environment required for all the ESP targets.

Troubleshooting

Here, we will present a list of common errors that may appear when building a project alongside the reason and a solution to them.

Environment variable LIBCLANG_PATH not set

thread 'main' panicked at 'Unable to find libclang: "couldn't find any valid shared libraries matching: ['libclang.so', 'libclang-*.so', 'libclang.so.*', 'libclang-*.so.*'], set the `LIBCLANG_PATH` environment variable to a path where one of these files can be found (invalid: [])"', /home/esp/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.60.1/src/lib.rs:2172:31

We need libclang for bindgen to generate the Rust bindings to the ESP-IDF C headers. Make sure the environment variable LIBCLANG_PATH is set and pointing to our custom fork of LLVM:

  • Unix:
    export $HOME/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221014-x86_64-unknown-linux-gnu/esp-clang/lib
    
  • Windows:
    $Env:LIBCLANG_PATH="%USERPROFILE%/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221014-x86_64-unknown-linux-gnu/esp-clang/bin/libclang.dll"
    $Env:PATH+=";%USERPROFILE%/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221014-x86_64-unknown-linux-gnu/esp-clang/bin/"
    

Missing libtinfo.so.5

thread 'main' panicked at 'Unable to find libclang: "the `libclang` shared library at /home/user/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221014-x86_64-unknown-linux-gnu/esp-clang/lib/libclang.so.15.0.0 could not be o
pened: libtinfo.so.5: cannot open shared object file: No such file or directory"', /home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/bindgen-0.60.1/src/lib.rs:2172:31

Our current version of LLVM, 15, requires libtinfo.so.5. This dependency will probably be removed in our future LLVM releases, but for the moment, please, make sure you have it installed:

  • Ubuntu/Debian: sudo apt-get install libtinfo5
  • Fedora: sudo dnf install ncurses-compat-libs
  • openSUSE: sudo dnf install libncurses5
  • Arch Linux: sudo pacman -S ncurses5-compat-libs

Missing ldproxy

error: linker `ldproxy` not found
  |
  = note: No such file or directory (os error 2)

If you are trying to build a std application ldproxy must be installed.

cargo install ldproxy

For more information, see ldproxy section.

Using wrong Rust toolchain

$ cargo build
error: failed to run `rustc` to learn about target-specific information

Caused by:
  process didn't exit successfully: `rustc - --crate-name ___ --print=file-names --target xtensa-esp32-espidf --crate-type bin --crate-type rlib --crate-type dylib --crate-type cdylib --crate-type staticlib --crate-type proc-macro --print=sysroot --print=cfg` (exit status: 1)
  --- stderr
  error: Error loading target specification: Could not find specification for target "xtensa-esp32-espidf". Run `rustc --print target-list` for a list of built-in targets

If you are encountering the previous error or a similar one, you are probably not using the proper Rust toolchain, remember that for Xtensa targets, you need to use Espressif Rust fork toolchain, there are several ways to do it:

For more information on toolchain overriding, see the Overrides chapter of The rustup book.

Windows

Long path names

When using Windows, you may encounter issues building a new project if using long path names. Follow these steps to substitute the path of your project:

subst r:\ <pathToYourProject>
cd r:\

Missing ABI

  Compiling cc v1.0.69
error: linker `link.exe` not found
  |
  = note: The system cannot find the file specified. (os error 2)

note: the msvc targets depend on the msvc linker but `link.exe` was not found

note: please ensure that VS 2013, VS 2015, VS 2017 or VS 2019 was installed with the Visual C++ option

error: could not compile `compiler_builtins` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

The reason for this error is that we are missing the MSVC C++, hence we are not meeting the Compile-time Requirements, please install Visual Studio 2013 (or later) or the Visual C++ Build Tools 2019. For Visual Studio, make sure to check the "C++ tools" and "Windows 10 SDK" options. If using GNU ABI, install MinGW/MSYS2 toolchain.

Tooling

Now that we have our required dependencies installed, we will cover some of the tools that will make our life developing Rust applications for ESP targets a lot easier.

Text Editors and IDEs

While an often contentious subject, using the right development environment can make a significant impact on your productivity with a given programming language. Below can be found a curated list of what we feel are the best options.

Visual Studio Code

One of the more common development environments is Microsoft's Visual Studio Code text editor along with the Rust Analyzer extension.

Visual Studio Code is an open-source and cross-platform graphical text editor with a rich ecosystem of extensions. The Rust Analyzer extension provides an implementation of the Language Server Protocol for Rust and additionally includes features like autocompletion, go-to definition, and more.

Visual Studio Code can be installed via most popular package managers, and installers are available on the official website. The Rust Analyzer extension can be installed in Visual Studio Code via the built-in extension manager.

Alongside Rust Analyzer (RA), there are other extensions that might be very helpful:

Tips and Tricks

If you are developing for a target that does not have std support Rust Analyzer can behave strangely, often reporting various errors. This can be resolved by creating a .vscode/settings.json file in your project and populating it with the following:

{
  "rust-analyzer.checkOnSave.allTargets": false
}

If you are using a custom toolchain, as you would with Xtensa targets, you can provide some hints to cargo via the rust-toolchain.toml file to improve the user experience:

[toolchain]
channel = "esp"
components = ["rustfmt", "rustc-dev"]
targets = ["xtensa-esp32-none-elf"]

CLion

CLion is a cross-platform IDE for C and C++ from JetBrains.

IntelliJ

vim

vim is a highly configurable text editor based on vi that also supports Rust Analyzer.

espflash

A serial flasher utility for ESP devices. Supports flashing ESP32, ESP32-C2, ESP32-C3, ESP32-S2, ESP32-S3, and ESP8266.

The esp-rs/espflash repository contains two crates, cargo-espflash and espflash. You can find more information on both of these in their respective sections below.

cargo-espflash

Provides a subcommand for cargo which handles cross-compilation and flashing. Note that this requires the unstable build-std cargo feature; for more information on this please refer to the cargo documentation.

To install:

cargo install cargo-espflash

This command must be run within a Cargo project, ie.) a directory containing a Cargo.toml file. For example, to build an example named 'blinky' in release mode, flash the resulting binary to a device, and then subsequently start a serial monitor:

cargo espflash --example=blinky --release --monitor

For more information please see to the cargo-espflash README.

espflash

Provides a standalone command-line application which flashes an ELF file to a device.

To install:

cargo install espflash

Assuming you have built an ELF binary by other means already, espflash can be used to download it to your device. For example, if you have built the getting-started/blinky example from esp-idf using idf.py you might run something like:

espflash build/blinky

For more information please see to the espflash README.

espmonitor

The esp-rs/espmonitor repository contains two crates, cargo-espmonitor and espmonitor.

cargo-espmonitor

cargo install cargo-espmonitor

espmonitor

cargo install espmonitor

Debugging

Debugging Rust applications is also possible using different tools that will be covered in this chapter.

probe-rs

The probe-rs project is a set of tools to interact with embedded MCU's using various debug probes. It is similar to openOCD, PyOCD, Segger tools, etc. There is support for ARM & RISCV architectures along with a collection of tools, including but not limited to:

  • Debugger
    • GDB support.
    • CLI for interactive debugging.
    • VSCode extension.
  • RTT (Real Time Transfer)
    • Similar to app_trace component of IDF.
  • Flashing algorithms

More info about probe-rs & how to set up a project can be found on the probe.rs website.

USB-JTAG-SERIAL peripheral for ESP32-C3

Starting from probe-rs v0.12, it is possible to flash and debug the ESP32-C3 with the builtin USB-JTAG-SERIAL peripheral, no need for any external hardware debugger. More info on configuring the interface can be found in the official documentation.

Support for Espressif chips

probe-rs currently only supports ARM & RISC-V, therefore this limits the number of Espressif chips that can be used at the moment.

ChipFlashingDebugging
ESP32-C3⚠️

Note: Items marked with ⚠️ are currently work in progress, usable but expect bugs.

Permissions - Linux

On Linux, you may run into permission issues trying to interact with Espressif probes. Installing the following udev rules and reloading should fix that issue.

# Espressif dev kit FTDI
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", MODE="660", GROUP="plugdev", TAG+="uaccess"

# Espressif USB JTAG/serial debug unit
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", MODE="660", GROUP="plugdev", TAG+="uaccess"

# Espressif USB Bridge
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1002", MODE="660", GROUP="plugdev", TAG+="uaccess"

OpenOCD

Similar to probe-rs, OpenOCD does not have support for the Xtensa architecture. However, Espressif does maintain a fork of OpenOCD under espressif/openocd-esp32 which has support for Espressif's chips.

Instructions on how to install openocd-esp32 for your platform can be found in the Espressif documentation.

Setup for Espressif chips

Once installed, it's as simple as running openocd with the correct scripts. For chips with the builtin USB JTAG, there is normally a config that will work out of the box, for example on the ESP32-C3:

openocd -f board/esp32c3-builtin.cfg

For other configurations it may require specifying the chip and the interface separately, for example, ESP32 with a J-Link:

openocd -f interface/jlink.cfg -f target/esp32.cfg

Debugging in Visual Studio Code

There is also a possibility to debug with graphical output directly in Visual Studio Code.

ESP32

Hardware Setup

ESP32 doesn't have a built-in JTAG interface so you have to connect an external JTAG adapter to the ESP32 board, for example, ESP-Prog can be used.

ESP32 PinJTAG Signal
MTDO/GPIO15TDO
MTDI/GPIO12TDI
MTCK/GPIO13TCK
MTMS/GPIO14TMS
3V3VJTAG
GNDGND

Note: On Windows USB Serial Converter A 0403 6010 00 driver should be WinUSB.

Set up VSCode

  1. Install Cortex-Debug extension for VScode.
  2. Create the .vscode/launch.json file in the project tree you want to debug. This can be used as a template file.
  3. Update executable, svdFile, serverpath paths, and toolchainPrefix field.
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      // more info at: https://github.com/Marus/cortex-debug/blob/master/package.json
      "name": "Attach",
      "type": "cortex-debug",
      "request": "attach", // attach instead of launch, because otherwise flash write is attempted, but fails
      "cwd": "${workspaceRoot}",
      "executable": "target/xtensa-esp32-none-elf/debug/.....",
      "servertype": "openocd",
      "interface": "jtag",
      "svdFile": "../../esp-pacs/esp32/svd/esp32.svd",
      "toolchainPrefix": "xtensa-esp32-elf",
      "openOCDPreConfigLaunchCommands": ["set ESP_RTOS none"],
      "serverpath": "C:/Espressif/tools/openocd-esp32/v0.11.0-esp32-20220411/openocd-esp32/bin/openocd.exe",
      "configFiles": ["board/esp32-wrover-kit-3.3v.cfg"],
      "overrideAttachCommands": [
        "set remote hardware-watchpoint-limit 2",
        "mon halt",
        "flushregs"
      ],
      "overrideRestartCommands": ["mon reset halt", "flushregs", "c"]
    }
  ]
}

ESP32-C3

Older versions with revision < 3 don't have built-in JTAG interface.

ESP32-C3 with revision 3 does have a built-in JTAG interface and you don't have to connect an external device to be able to debug. To get the chip revision, run the cargo espflash board-info command.

Hardware Setup

If your ESP32-C3's revision is lesser than 3, follow these instructions, if you have revision 3 you can jump to the Set up VSCode step.

ESP32-C3 revision 1 and revision 2 don't have a built-in JTAG interface so you have to connect an external JTAG adapter to the ESP32-C3 board, for example, ESP-Prog can be used.

ESP32-C3 PinJTAG Signal
MTDO/GPIO7TDO
MTDI/GPIO5TDI
MTCK/GPIO6TCK
MTMS/GPIO4TMS
3V3VJTAG
GNDGND

Note: On Windows USB Serial Converter A 0403 6010 00 driver should be WinUSB.

Set up VSCode

  1. Install Cortex-Debug extension for VScode.
  2. Create the .vscode/launch.json file in the project tree you want to debug. This can be used as a template file.
  3. Update executable, svdFile, serverpath paths, and toolchainPrefix field.
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      // more info at: https://github.com/Marus/cortex-debug/blob/master/package.json
      "name": "Attach",
      "type": "cortex-debug",
      "request": "attach", // attach instead of launch, because otherwise flash write is attempted, but fails
      "cwd": "${workspaceRoot}",
      "executable": "target/riscv32imc-unknown-none-elf/debug/examples/usb_serial_jtag", //
      "servertype": "openocd",
      "interface": "jtag",
      "svdFile": "../../esp-pacs/esp32c3/svd/esp32c3.svd",
      "toolchainPrefix": "riscv32-esp-elf",
      "openOCDPreConfigLaunchCommands": ["set ESP_RTOS none"],
      "serverpath": "C:/Espressif/tools/openocd-esp32/v0.11.0-esp32-20220411/openocd-esp32/bin/openocd.exe",
      "configFiles": ["board/esp32c3-builtin.cfg"],
      "overrideAttachCommands": [
        "set remote hardware-watchpoint-limit 2",
        "mon halt",
        "flushregs"
      ],
      "overrideRestartCommands": ["mon reset halt", "flushregs", "c"]
    }
  ]
}

Simulating

Simulating projects can be very handy. It allows users to test projects using CI, try projects without having hardware available, and many other scenarios.

At the moment, there are a few ways of simulating Rust projects on Espressif chips, all of them have some limitations, but it's quickly evolving and getting better every day.

In this chapter, we will discuss the different ways of simulation that are available.

Wokwi

Wokwi is an online simulator that supports simulating Rust projects (both std and no_std) in ESP Chips, see wokwi.com/rust for a list of examples and a way to start new projects.

Wokwi offers WiFi simulation, Virtual Logic Analyzer, and GDB debugging among many other features, see Wokwi documentation for more details. For ESP chips, there is a table of simulation features that are currently supported.

Using wokwi-server

wokwi-server is a CLI tool for launching a Wokwi simulation of your project. I.e., it allows you to build a project on your machine, or in a container, and simulate the resulting binary.

wokwi-server also allows simulating your resulting binary on other Wokwi projects, with more hardware parts other than the chip itself. See the corresponding section of the wokwi-server Readme for detailed instructions.

QEMU

Espressif maintains a fork of QEMU in espressif/QEMU with the necessary patches to make it work on Espressif chips. See the QEMU wiki for instructions on how to build QEMU and emulate projects with it.

Once you have built QEMU, you should have qemu-system-xtensa.

Running our project using QEMU

NOTE: Only ESP32 is currently supported, so make sure you are compiling for xtensa-esp32-espidf target.

For running our project in QEMU, we need a firmware/image with bootloader and partition table merged in it. We can use cargo-espflash to generate it:

cargo espflash save-image --merge ESP32 <OUTFILE> --release

If you prefer to use espflash, you can achieve the same result by building the project first and then generating image:

cargo build --release
espflash save-image --merge ESP32 target/xtensa-esp32-espidf/release/<NAME> <OUTFILE>

Now, run the image in QEMU:

/path/to/qemu-system-xtensa -nographic -machine esp32 -drive file=<OUTFILE>,if=mtd,format=raw

Writing Your Own Application

With the appropriate Rust compiler and toolchain installed, you're now ready to create an application.

There are essentially two ways to do this: generating from a template or starting from scratch using only cargo.

We strongly recomend starting projects from templates as it gives you a configured project saving all the time that would require to setup a project started from scratch with cargo.

If you are looking for inspiration, check the Projects section of our Awesome ESP Rust repository

Generating Projects from Templates

We currently maintin two template repositories:

Both templates are based on cargo-generate, a tool that allows you to create a new project based on some existing template. In our case esp-idf-template or esp-template can be used to generate an application with all the required configuration and dependencies.

cargo generate can be installed by running:

cargo install cargo-generate

When the cargo generate subcommand is invoked, you will be prompted to answer a number of questions regarding the target of your application. Upon completion of this process you will have a buildable project with all the correct configuration.

The generated application can be built as normal using the appropriate toolchain and target simply by running cargo build when using either templates.

Using cargo run will compile the project, flash it, and open a serial monitor with our chip.

esp-idf-template

When using the Rust standard library (std) you can use the esp-idf-template template, which will look something like:

$ cargo generate --git https://github.com/esp-rs/esp-idf-template cargo
🤷   Project Name : esp-rust-app
🔧   Destination: /home/alice/esp-rust-app ...
🔧   Generating template ...
✔ 🤷   MCU · esp32
✔ 🤷   Configure project to use Dev Containers (VS Code, GitHub Codespaces and Gitpod)? (beware: Dev Containers not available for esp-idf v4.3.2) · false
✔ 🤷   STD support · true
✔ 🤷   ESP-IDF native build version (v4.3.2 = previous stable, v4.4 = stable, mainline = UNSTABLE) · v4.4
[ 1/10]   Done: .cargo/config.toml
[ 2/10]   Done: .cargo
[ 3/10]   Done: .gitignore
[ 4/10]   Done: .vscode
[ 5/10]   Done: Cargo.toml
[ 6/10]   Done: build.rs
[ 7/10]   Done: rust-toolchain.toml
[ 8/10]   Done: sdkconfig.defaults
[ 9/10]   Done: src/main.rs
[10/10]   Done: src
🔧   Moving generated files into: `/home/alice/esp-rust-app`...
💡   Initializing a fresh Git repository
✨   Done! New project created /home/alice/esp-rust-app

See Understanding esp-idf-template for more details on the template project.

esp-template

For bare-metal applications (no_std) you can instead use the esp-template template:

cargo generate --git https://github.com/esp-rs/esp-template
🤷   Project Name : esp-rust-app
🔧   Destination: /home/alice/esp-rust-app ...
🔧   Generating template ...
✔ 🤷   Which MCU to target? · esp32c3
✔ 🤷   Configure project to use Dev Containers (VS Code, GitHub Codespaces and Gitpod)? · false
✔ 🤷   Enable allocations via the esp-alloc crate? · false
[ 1/11]   Done: .cargo/config.toml
[ 2/11]   Done: .cargo
[ 3/11]   Done: .gitignore
[ 4/11]   Done: .vscode/settings.json
[ 5/11]   Done: .vscode
[ 6/11]   Done: Cargo.toml
[ 7/11]   Done: LICENSE-APACHE
[ 8/11]   Done: LICENSE-MIT
[ 9/11]   Done: rust-toolchain.toml
[10/11]   Done: src/main.rs
[11/11]   Done: src
🔧   Moving generated files into: `/home/alice/esp-rust-app`...
✨   Done! New project created /home/alice/esp-rust-app

See Understanding esp-template for more details on the template project.

Using Dev Containers in the templates

Both template repositories have a prompt for Dev Containers support, when using Dev Containers in the templates it will add support for:

Dev Containers use the idf-rust container image that was explained in the Using Container section of the Installing Rust chapter and provide an environment ready to develop Rust applications for Espressif chips with no installation required. Dev Containers also have integration with Wokwi simulator, to simulate the project, and allow flashing from the container using web flash.

For more details about on Dev Containers, see Dev Container section of the template Readme.

Writing no_std applications

The goal of this chapter is to provide a getting-started guide on using the Rust programming language with Espressif SoCs and modules using esp-hal.

Note that there are several examples covering the use of specific peripherals under the examples folder of every SoC esp-hal. E.g. esp32c3-hal/examples

Examples shown here usually apply to ESP32-C3 using the ESP32-C3-DevKit-RUST-1 board.

You can use any other ESP32, ESP32-C3, ESP32-S2, or ESP32-S3 development board but smaller code changes and configuration changes might be needed.

Also, this section of the book will only cover working locally. I.e. we will be using our host machine to develop, not devcontainers, so make sure you have the ecosystem properly installed.

Understanding esp-template

Now that we know how to generate a no_std project, let's inspect what the generated project contains and try to understand every part of it.

Inspecting the generated Project

When creating a project from esp-template using:

  • MCU: esp32c3
  • Devcontainer support: false
  • esp-alloc crate support: flase

It should generate a file structure like this:

├── .cargo
│   └── config.toml
├── src
│   └── main.rs
├── .vscode
│   └── settings.json
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
└── rust-toolchain.toml

Before going further let's see what these files are for.

  • .gitignore
    • tells git which folders and files to ignore
  • Cargo.toml
    • the usual Cargo manifest declaring some meta-data and dependencies of the project
  • LICENSE-APACHE, LICENSE_MIT
    • those are the most common licenses used in the Rust ecosystem
    • if you want to apply a different license you can delete these files and change the license in Cargo.toml
  • rust-toolchain.toml
    • defines which Rust toolchain to use
    • depending on your target this will use nightly or esp
  • .cargo/config.toml
    • the Cargo configuration
    • this defines a few options to correctly build the project
    • also contains runner = "espflash --monitor" - this means you can just use cargo run to flash and monitor your code
  • .vscode/settings.json
    • settings for Visual Studio Code - if you are not using VSCode you can delete the whole folder
  • src/main.rs
    • the main source file of the newly created project
    • we will examine its content in the next section

main.rs

#![no_std]
#![no_main]

use esp32c3_hal::{clock::ClockControl, pac::Peripherals, prelude::*, timer::TimerGroup, Rtc};
use esp_backtrace as _;

#[riscv_rt::entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Disable the RTC and TIMG watchdog timers
    let mut rtc = Rtc::new(peripherals.RTC_CNTL);
    let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks);
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
    let mut wdt1 = timer_group1.wdt;

    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();

    loop {}
}

That is quite a lot of code. Let's see what it is good for.

  • #![no_std]
    • this tells the Rust compiler that this code doesn't use libstd
  • #![no_main]
    • The no_main attribute says that this program won't use the standard main interface, which is tailored for command-line applications that receive arguments. Instead of the standard main, we'll use the entry attribute from the riscv-rt crate to define a custom entry point. In this program we have named the entry point main, but any other name could have been used. The entry point function must be a diverging function. I.e. it has the signature fn foo() -> !; this type indicates that the function never returns – which means that the program never terminates.
  • use esp32c3_hal:{...}
    • we need to bring in some types we are going to use
    • these are from esp-hal
  • use esp_backtrace as _;
    • since we are in a bare-metal environment we need a panic-handler that runs if a panic occurs in code
    • there are a few different crates you can use (e.g panic-halt) but esp-backtrace provides an implementation that prints the address of a backtrace - together with espflash/espmonitor these addresses can get decoded into source code locations
  • let peripherals = Peripherals::take().unwrap();
    • HAL drivers usually take ownership of peripherals accessed via the PAC
    • here we take all the peripherals from the PAC to pass them to the HAL drivers later
  • let system = peripherals.SYSTEM.split();
    • sometimes a peripheral (here the System peripheral) is coarse-grained and doesn't exactly fit the HAL drivers - so here we split the System peripheral into smaller pieces which get passed to the drivers
  • let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
    • here we configure the system clocks - in this case, we are fine with the defaults
    • we freeze the clocks which means we cannot change them later
    • some drivers need a reference to the clocks to know how to calculate rates and durations
  • the next block of code instantiates some peripherals (namely RTC and the two timer groups) to disable the watchdog which is armed after boot
    • without that code, the SoC would reboot after some time
    • there is another way to prevent the reboot: feeding the watchdog
  • loop {}
    • since our function is supposed to never return we just "do nothing" in a loop

Running the Code

Building and running the code is as easy as

cargo run

This builds the code according to the configuration and executes espflash to flash the code to the board.

Since our runner configuration also passes the --monitor argument to espflash we can see what the code is printing.

Make sure that you have espflash installed, otherwise this step will fail. To install espflash: cargo install espflash

You should see something similar to this:

Connecting...

Chip type:         ESP32-C3 (revision 3)
Crystal frequency: 40MHz
Flash size:        4MB
Features:          WiFi
MAC address:       60:55:f9:c0:0e:ec
App/part. size:    198752/4128768 bytes, 4.81%
[00:00:00] ########################################      12/12      segment 0x0
[00:00:00] ########################################       1/1       segment 0x8000
[00:00:01] ########################################      57/57      segment 0x10000
Flashing has completed!
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xc (SPI_FAST_FLASH_BOOT)
Saved PC:0x4004c72e
0x4004c72e - _stack_start
    at ??:??
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd6100,len:0x172c
load:0x403ce000,len:0x928
0x403ce000 - _erwtext
    at ??:??
load:0x403d0000,len:0x2ce0
0x403d0000 - _erwtext
    at ??:??
entry 0x403ce000
0x403ce000 - _erwtext
    at ??:??
I (24) boot: ESP-IDF v4.4-dev-2825-gb63ec47238 2nd stage bootloader
I (24) boot: compile time 12:10:40
I (25) boot: chip revision: 3
I (28) boot_comm: chip revision: 3, min. bootloader chip revision: 0
I (35) boot.esp32c3: SPI Speed      : 80MHz
I (39) boot.esp32c3: SPI Mode       : DIO
I (44) boot.esp32c3: SPI Flash Size : 4MB
I (49) boot: Enabling RNG early entropy source...
I (54) boot: Partition Table:
I (58) boot: ## Label            Usage          Type ST Offset   Length
I (65) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (73) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (80) boot:  2 factory          factory app      00 00 00010000 003f0000
I (88) boot: End of partition table
I (92) boot_comm: chip revision: 3, min. application chip revision: 0
I (99) esp_image: segment 0: paddr=00010020 vaddr=3c030020 size=04a6ch ( 19052) map
I (110) esp_image: segment 1: paddr=00014a94 vaddr=40380000 size=00910h (  2320) load
I (116) esp_image: segment 2: paddr=000153ac vaddr=00000000 size=0ac6ch ( 44140)
I (131) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=2081ch (133148) map
I (152) boot: Loaded app from partition at offset 0x10000

What you see here are messages from the first and second stage bootloader and then ... nothing.

And that is exactly what the code is doing.

You can reboot with CTRL+R or exit with CTRL+C.

In the next chapter, we will add some more interesting output.

Hello World

In the last chapter you flashed and run your first piece of code on the SoC - while that is already really exciting we can do better.

Traditionally the first thing to run on a microcontroller is blinky.

However, we will start with Hello World here.

Add a Dependency

You can add a dependency by any of the following methods:

  • By Editing Cargo.toml. In Cargo.toml in the [dependencies] section add this line:
esp-println = { version = "0.3.1", features = ["esp32c3"] }
cargo add esp-println --features "esp32c3"

esp-println is an additional crate that calls ROM functions to print text that is shown by espflash (or any other serial monitor).

We need to pass the feature esp32c3 since that crate targets multiple SoCs and needs to know which one it is supposed to run on.

Note that there might be new versions by the time you are reading this, please check crates.io.

In main.rs before the loop {} add this line

esp_println::println!("Hello World");

See Results

Again run

cargo run

You should see the text Hello World printed!

Panic!

When something goes terribly wrong in Rust there might occur a panic.

Let's see what it looks like for us.

In main.rs put this line somewhere, e.g. before our println

panic!("This is a panic");

Again run the code.

You should see something like this



!! A panic occured in 'src\main.rs', at line 25, column 5

PanicInfo {
    payload: Any { .. },
    message: Some(
        This is a panic,
    ),
    location: Location {
        file: "src\\main.rs",
        line: 25,
        col: 5,
    },
    can_unwind: true,
}

Backtrace:

0x420019aa
0x420019aa - main
    at C:\tmp\getting-started\src\main.rs:25
0x4200014c
0x4200014c - _start_rust
    at ...\.cargo\registry\src\github.com-1ecc6299db9ec823\riscv-rt-0.9.0\src\lib.rs:389

We see where the panic occured and we even see a backtrace!

While in this example things are obvious, this will come 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 'src\main.rs', at line 25, column 5

PanicInfo {
    payload: Any { .. },
    message: Some(
        This is a panic,
    ),
    location: Location {
        file: "src\\main.rs",
        line: 25,
        col: 5,
    },
    can_unwind: true,
}

Backtrace:

0x42000140
0x42000140 - _start_rust
    at ??:??

We still see where the panic occured but the backtrace is less helpful now.

That is because the compiler omitted debug information and optimized the code.

But you might have noticed the difference in the size of the flashed binary.

It went from 199056 bytes down to 86896 bytes!

Please note that this is still huge for what we get. There are a lot of options to get the binary smaller which is beyond the scope of this book.

Before going further remove the line causing the explicit panic.

Blinky

Let's see how to create the iconic Blinky.

Change the code in main.rs to this

#![no_std]
#![no_main]

use esp32c3_hal::{
    clock::ClockControl, pac::Peripherals, prelude::*, timer::TimerGroup, Delay, Rtc, IO,
};
use esp_backtrace as _;

#[riscv_rt::entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Disable the RTC and TIMG watchdog timers
    let mut rtc = Rtc::new(peripherals.RTC_CNTL);
    let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks);
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
    let mut wdt1 = timer_group1.wdt;

    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();

    esp_println::println!("Hello World");

    // Set GPIO7 as an output, and set its state high initially.
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let mut led = io.pins.gpio7.into_push_pull_output();

    led.set_high().unwrap();

    // Initialize the Delay peripheral, and use it to toggle the LED state in a
    // loop.
    let mut delay = Delay::new(&clocks);

    loop {
        led.toggle().unwrap();
        delay.delay_ms(500u32);
    }
}

We need two new types in scope: IO and Delay

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).

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.

Detect a button press

Most of the dev-boards have a button, in our case, we will use the one labeled BOOT on GPIO9. Let's see how to check the state of the button.

#![no_std]
#![no_main]

use esp32c3_hal::{
    clock::ClockControl, pac::Peripherals, prelude::*, timer::TimerGroup, Rtc, IO,
};
use esp_backtrace as _;

#[riscv_rt::entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Disable the RTC and TIMG watchdog timers
    let mut rtc = Rtc::new(peripherals.RTC_CNTL);
    let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks);
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
    let mut wdt1 = timer_group1.wdt;

    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();

    // Set GPIO7 as an output, GPIO9 as input
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let mut led = io.pins.gpio7.into_push_pull_output();
    let button = io.pins.gpio9.into_pull_up_input();

    loop {
        if button.is_high().unwrap() {
            led.set_high().unwrap();
        } else {
            led.set_low().unwrap();
        }
    }
}

Now if the button is not pressed the LED is lit. If the button is pressed the LED is off.

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.

Detect a button press with interrupt

Interrupts offer a mechanism by which the processor handles asynchronous events and fatal errors.

Let's add the critical-section crate (see instructions on how to add a dependency), and change main.rs to look like this:

#![no_std]
#![no_main]

use core::cell::RefCell;
use critical_section::Mutex;
use esp32c3_hal::{
    clock::ClockControl,
    gpio::Gpio9,
    gpio_types::{Event, Input, Pin, PullUp},
    interrupt,
    pac::{self, Peripherals},
    prelude::*,
    timer::TimerGroup,
    Rtc, IO,
};
use esp_backtrace as _;

static BUTTON: Mutex<RefCell<Option<Gpio9<Input<PullUp>>>>> = Mutex::new(RefCell::new(None));

#[riscv_rt::entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Disable the RTC and TIMG watchdog timers
    let mut rtc = Rtc::new(peripherals.RTC_CNTL);
    let timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks);
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
    let mut wdt1 = timer_group1.wdt;

    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();

    // Set GPIO9 as input
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let mut button = io.pins.gpio9.into_pull_up_input();
    button.listen(Event::FallingEdge); // raise interrupt on falling edge

    critical_section::with(|cs| BUTTON.borrow_ref_mut(cs).replace(button));

    interrupt::enable(pac::Interrupt::GPIO, interrupt::Priority::Priority3).unwrap();

    loop {}
}

#[interrupt]
fn GPIO() {
    critical_section::with(|cs| {
        esp_println::println!("GPIO interrupt");
        BUTTON
            .borrow_ref_mut(cs)
            .as_mut()
            .unwrap()
            .clear_interrupt();
    });
}

There are quite a lot of new things here.

First thing 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 from critical-section (and that's why we need to add it as a dependency).

Then we need to call listen on the output pin to configure the peripheral to raise interrupts. We can raise interrupts for different events - here we want to raise the interrupt on the falling edge.

In the next line we move our button into the static BUTTON for the interrupt handler to get hold of it.

Last thing we need to do is actually enable the interrupt.

First parameter here is the kind of interrupt we want. There are several possible interrupts.

Second parameter is the priority of the interrupt.

The interrupt handler is defined via the #[interrupt] macro. Here the name of the function must match the interrupt.

Writing std applications

If you want to learn how to develop std application, there is a training developed alongside Ferrous Systems:

The training is based on ESP32-C3-DevKit-RUST-1. You can use any other ESP32, ESP32-C3, ESP32-S2, or ESP32-S3 development board but code changes and configuration changes might be needed.

The training is split into two parts:

Note that there are several examples covering the use of specific peripherals under the examples folder of esp-idf-hal. I.e. esp32-idf-hal/examples.

Understanding esp-idf-template

Now that we know how to generate a std project, let's inspect what the generated project contains and try to understand every part of it.

Inspecting the generated Project

When creating a project from esp-idf-template using:

  • MCU: esp32c3
  • ESP-IDF version: v4.4
  • STD support: true
  • Devcontainer support: false

It should generate a file structure like this:

├── build.rs
├── .cargo
│   └── config.toml
├── Cargo.toml
├── .gitignore
├── rust-toolchain.toml
├── sdkconfig.defaults
└── src
    └── main.rs

Before going further let's see what these files are for.

  • .gitignore
    • tells git which folders and files to ignore
  • Cargo.toml
    • the usual Cargo manifest declaring some meta-data and dependencies of the project
  • LICENSE-APACHE, LICENSE_MIT
    • those are the most common licenses used in the Rust ecosystem
    • if you want to apply a different license you can delete these files and change the license in Cargo.toml
  • rust-toolchain.toml
    • defines which Rust toolchain to use
    • depending on your target this will use nightly or esp
  • .cargo/config.toml
    • the Cargo configuration
    • contains our target
    • contains runner = "espflash --monitor" - this means you can just use cargo run to flash and monitor your code
    • contains the linker to use, in our case, ldproxy
    • contains the unstable build-std cargo feature enabled.
    • contains the ESP-IDF-VERSION envrionment variable that tells esp-idf-sys which ESP-IDF version the project will use.
  • src/main.rs
    • the main source file of the newly created project
    • we will examine its content in the next section
  • build.rs
    • propagates linker arguments for ldproxy.
  • sdkconfig.defaults
    • contains the overriden values from the ESP-IDF defaults.

main.rs

use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported

fn main() {
    esp_idf_sys::link_patches();
    println!("Hello, world!");
}

The first line its an import that defines the esp-idf entry-point when the root crate is a binary crate that defines a main function.

Then, we have an usual main function with two lines on it:

  • A call to esp_idf_sys::link_patches function that makes sure that a few patches to the ESP-IDF which are implemented in Rust are linked to the final executable.
  • We print in our console the famous "Hello World!".

Running the Code

Building and running the code is as easy as

cargo run

This builds the code according to the configuration and executes espflash to flash the code to the board.

Since our runner configuration also passes the --monitor argument to espflash we can see what the code is printing.

Make sure that you have espflash installed, otherwise this step will fail. To install espflash: cargo install espflash You should see something similar to this:

Connecting...

Chip type:         ESP32-C3 (revision 3)
Crystal frequency: 40MHz
Flash size:        4MB
Features:          WiFi
MAC address:       60:55:f9:c0:39:7c
App/part. size:    409728/4128768 bytes, 9.92%
[00:00:00] ########################################      12/12      segment 0x0
[00:00:00] ########################################       1/1       segment 0x8000
[00:00:04] ########################################     210/210     segment 0x10000
Flashing has completed!
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xc (SPI_FAST_FLASH_BOOT)
Saved PC:0x4004c97e
0x4004c97e - chip726_phyrom_version_num
    at ??:??
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd6100,len:0x172c
load:0x403ce000,len:0x928
0x403ce000 - _iram_text_end
    at ??:??
load:0x403d0000,len:0x2ce0
0x403d0000 - _iram_text_end
    at ??:??
entry 0x403ce000
0x403ce000 - _iram_text_end
    at ??:??
I (24) boot: ESP-IDF v4.4-dev-2825-gb63ec47238 2nd stage bootloader
I (24) boot: compile time 12:10:40
I (24) boot: chip revision: 3
I (28) boot_comm: chip revision: 3, min. bootloader chip revision: 0
I (35) boot.esp32c3: SPI Speed      : 80MHz
I (39) boot.esp32c3: SPI Mode       : DIO
I (44) boot.esp32c3: SPI Flash Size : 4MB
I (49) boot: Enabling RNG early entropy source...
I (54) boot: Partition Table:
I (58) boot: ## Label            Usage          Type ST Offset   Length
I (65) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (73) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (80) boot:  2 factory          factory app      00 00 00010000 003f0000
I (88) boot: End of partition table
I (92) boot_comm: chip revision: 3, min. application chip revision: 0
I (99) esp_image: segment 0: paddr=00010020 vaddr=3c050020 size=17640h ( 95808) map
I (122) esp_image: segment 1: paddr=00027668 vaddr=3fc89c00 size=0146ch (  5228) load
I (123) esp_image: segment 2: paddr=00028adc vaddr=40380000 size=0753ch ( 30012) load
I (133) esp_image: segment 3: paddr=00030020 vaddr=42000020 size=419d8h (268760) map
I (176) esp_image: segment 4: paddr=00071a00 vaddr=4038753c size=02644h (  9796) load
I (178) esp_image: segment 5: paddr=0007404c vaddr=50000010 size=00010h (    16) load
I (185) boot: Loaded app from partition at offset 0x10000
I (188) boot: Disabling RNG early entropy source...
I (205) cpu_start: Pro cpu up.
I (213) cpu_start: Pro cpu start user code
I (213) cpu_start: cpu freq: 160000000
I (213) cpu_start: Application information:
I (216) cpu_start: Project name:     libespidf
I (221) cpu_start: App version:      1
I (226) cpu_start: Compile time:     Nov  3 2022 13:16:23
I (232) cpu_start: ELF file SHA256:  0000000000000000...
I (238) cpu_start: ESP-IDF:          755ce10-dirty
I (243) heap_init: Initializing. RAM available for dynamic allocation:
I (250) heap_init: At 3FC8BF90 len 00050780 (321 KiB): DRAM
I (257) heap_init: At 3FCDC710 len 00002950 (10 KiB): STACK/DRAM
I (263) heap_init: At 50000020 len 00001FE0 (7 KiB): RTCRAM
I (270) spi_flash: detected chip: generic
I (274) spi_flash: flash io: dio
I (279) sleep: Configure to isolate all GPIO pins in sleep state
I (285) sleep: Enable automatic switching of GPIO sleep configuration
I (292) cpu_start: Starting scheduler.
Hello, world!

As you can see, there are messages from the first and second stage bootloader and then, our "Hello, world!" its printed.

You can reboot with CTRL+R or exit with CTRL+C.

Resources

  • esp-rs Matrix Channel: The perfect place to ask questions and discuss any topic related with Rust on Espressif chips.
  • Embedded Rust on Espressif - Training for Rust development on ESP32-C3 by Ferrous Systems.
  • Awesome ESP Rust - A curated list of resouces for development in the Rust programming language for Espressif chips.
  • esp-rs: Github organization with libraries, crates and examples for using Rust on Espressif SoC's.

Crates referenced in this book

  • cargo-binstall - Binary installation for rust projects
  • cargo-generate - Tool to help you get up and running quickly with a new Rust project by leveraging a pre-existing git repository as a template.
  • esp-hal - no_std HAL for Espressif microcontrollers.
  • esp-idf-hal - std HAL for Espressif microcontrollers.
  • esp-idf-template - A minimal esp-idf-hal application template to use with cargo-generate.
  • esp-template - A minimal esp-hal application template to use with cargo-generate.
  • espflash - Serial flasher utility for Espressif SoCs and modules based on esptool.
  • espup: About Tool for installing and maintaining the required toolchains for developing applications in Rust for Espressif SoC's.
  • ldproxy - Tool to forward linker arguments to the actual linker executable.
  • wokwi-server - WebSocket server for running simulations in Wokwi from Visual Studio Code Remote Containers using a web browser.

Appendix A: Glossary

A number of acronyms are used in the embedded development space. This glossary attempts to define any acronyms used in this book.

SVD

System View Description.

The CMSIS-SVD specification formalizes the description of the system contained within a microcontroller. This specification was designed with ARM Cortex-M microcontrollers in mind, however it is still applicable to other architectures.

SVD files are XML and contain definitions for peripherals which can be consumed by tools such a svd2rust to generate Peripheral Access Crates.

PAC

Peripheral Access Crate.

Provides a type-safe, low-level API for interacting with the device's hardware peripherals. For more information on the generated API please refer to the svd2rust documentation.

HAL

Hardware Abstraction Layer.

Provides higher-level abstractions over hardware peripherals which are more easily used by developers. These libraries are generally implemented on top of Peripheral Access Crates, and often implement the various traits provided by embedded-hal.