Introduction

The goal of this book is to provide a comprehensive guide on using the Rust Programming Language with Espressif devices.

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. This organization is managed by employees of Espressif as well as members of the community.

Who This Book Is For

This book is intended for people with some experience in Rust and also assumes rudimentary knowledge of embedded development and electronics. For those without prior experience, we recommend first reading the Assumptions and Prerequisites and Resources sections to get up to speed.

Assumptions and Prerequisites

  • You are comfortable using the Rust programming language and have written and run applications in a desktop environment.
  • You should be familiar with the idioms of the 2021 edition, as this book targets Rust 2021.
  • You are comfortable developing embedded systems in another language such as C or C++, and are familiar with concepts such as:
    • Cross-compilation
    • Common digital interfaces like UART, SPI, I2C, etc.
    • Memory-mapped peripherals
    • Interrupts

Resources

If you are unfamiliar or less experienced with anything mentioned above, or if you would just like more information about a particular topic mentioned in this book. You may find these resources helpful.

ResourceDescription
The Rust Programming LanguageIf you aren't 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.
Embedded Rust on EspressifTraining material created in cooperation with Ferrous Systems.

Translations

This book is currently available in English only. Once the contents of the book stabilize somewhat, we plan on translating the book into additional languages. As translations become available, this section will be updated to include them.

How to Use This Book

This book assumes that you are reading it front-to-back. Content covered in later chapters may not make much sense without the context from previous chapters.

Contributing to This Book

The work on this book is coordinated in this repository.

If you have trouble following the instructions in this book or find that some section of the book isn't clear enough, then that's a bug. Please report it in the issue tracker of this book.

Pull requests fixing typos and adding new content are welcome!

Re-Using This Material

This book is distributed under the following licenses:

  • The code samples and freestanding Cargo projects contained within this book are licensed under the terms of both the MIT License and the Apache License v2.0.
  • The written prose, pictures, and diagrams contained within this book are licensed under the terms of the Creative Commons CC-BY-SA v4.0 license.

In summary, to use our text or images in your work, you need to:

  • Give the appropriate credit (i.e. mention this book on your slide, and provide a link to the relevant page)
  • Provide a link to the CC-BY-SA v4.0 license
  • Indicate if you have changed the material in any way, and make any changes to our material available under the same license

Please do let us know if you find this book useful!

Overview of Development Approaches

There are the following approaches to using Rust on Espressif chips:

  • Using the std library, a.k.a. Standard library.
  • Using the core library (no_std), a.k.a. bare metal development.

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:

See also the comparison of the different runtimes in The Embedded Rust Book.

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

Repository Naming Convention

In the esp-rs organization, we use the following wording:

  • Repositories starting with esp- are focused on no_std approach. For example, esp-hal
    • no_std works on top of bare metal, so esp- is an Espressif chip
  • Repositories starting with esp-idf- are focused on std approach. For example, esp-idf-hal

Support for Espressif Products

⚠️ Notes:

  • ✅ - The feature is implemented or supported
  • ⏳ - The feature is under development
  • ❌ - The feature isn't supported
Chipstdno_std
ESP32
ESP32-C2
ESP32-C3
ESP32-C6
ESP32-S2
ESP32-S3
ESP32-H2
ESP8266

⚠️ Note: The ESP8266 series is outside the scope of this book. Rust support for the ESP8266 series is limited and isn't being officially supported by Espressif.

The products supported in certain circumstances will be called supported Espressif products throughout the book.

Using the Standard Library (std)

Espressif provides a C-based development framework called ESP-IDF. It has, or will have, support for all Espressif chips starting with the ESP32, note that this framework doesn't 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 Epressif devices.

Current Support

The Espressif products supported for Rust std development are the ones supported by the ESP-IDF framework. For details on different versions of ESP-IDF and support of Espressif chips, see this table.

When using std, you have access to a lot of features that exist in ESP-IDF, including threads, mutexes and other synchronization primitives, collections, random number generation, sockets, etc.

Relevant esp-rs Crates

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

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

When You Might Want to Use the Standard Library (std)

  • Rich functionality: If your embedded system requires lots of functionality like support for networking protocols, file I/O, or complex data structures, you will likely want to use hosted-environment approach because std libraries provide a wide range of functionality that can be used to build complex applications.
  • Portability: The std crate provides a standardized set of APIs that can be used across different platforms and architectures, making it easier to write code that is portable and reusable.
  • Rapid development: The std crate provides a rich set of functionality that can be used to build applications quickly and efficiently, without worrying, too much, about low-level details.

Using the Core Library (no_std)

Using no_std may be more familiar to embedded Rust developers. It doesn't use std (the Rust standard library) but instead uses a subset, the core library. The Embedded Rust Book has a great section on this.

It is important to note that no_std uses the Rust core library. As this library is part of the Rust standard library, a no_std crate can compile in std environment. However, the opposite isn't true: an std crate can't compile in no_std environment. This information is worth remembering when deciding which library to choose.

Current Support

The table below covers the current support for no_std at this moment for different Espressif products.

HALWi-Fi/BLE/ESP-NOWBacktraceStorage
ESP32
ESP32-C2
ESP32-C3
ESP32-C6
ESP32-S2
ESP32-S3
ESP32-H2

⚠️ Note:

  • ✅ in Wi-Fi/BLE/ESP-NOW means that the target supports, at least, one of the listed technologies. For details, see Current support table of the esp-wifi repository.
  • ESP8266 HAL is in maintenance mode and no further development will be done for this chip.

Relevant esp-rs Crates

RepositoryDescription
esp-halHardware abstraction layer
esp-pacsPeripheral access crates
esp-wifiWi-Fi, BLE and ESP-NOW support
esp-allocSimple heap allocator
esp-printlnprint!, println!
esp-backtraceException and panic handlers
esp-storageEmbedded-storage traits to access unencrypted flash memory

When You Might Want to Use the Core Library (no_std)

  • Small memory footprint: If your embedded system has limited resources and needs to have a small memory footprint, you will likely want to use bare-metal because std features add a significant amount of final binary size and compilation time.
  • Direct hardware control: If your embedded system requires more direct control over the hardware, such as low-level device drivers or access to specialized hardware features you will likely want to use bare-metal because std adds abstractions that can make it harder to interact directly with the hardware.
  • Real-time constraints or time-critical applications: If your embedded system requires real-time performance or low-latency response times because std can introduce unpredictable delays and overhead that can affect real-time performance.
  • Custom requirements: bare-metal allows more customization and fine-grained control over the behavior of an application, which can be useful in specialized or non-standard environments.

Setting Up a Development Environment

At the moment, Espressif SoCs are based on two different architectures: RISC-V and Xtensa. Both architectures support std and no_std approaches.

To set up the development environment, do the following:

  1. Install Rust
  2. Install requirements based on your target(s)

Regardless of the target architecture, for std development also don't forget to install std Development Requirements.

Please note that you can host the development environment in a container.

Rust Installation

Make sure you have Rust installed. If not, see the instructions on the rustup website.

🚨 Warning: When using Unix based systems, installing Rust via a system package manager (e.g. brew, apt, dnf, etc.) can result in various issues and incompatibilities, so it's best to use rustup instead.

When using Windows, make sure you have installed one of the ABIs listed below. For more details, see the Windows chapter in The rustup book.

  • MSVC: Recommended ABI, included in the list of rustup default requirements. Use it for interoperability with the software produced by Visual Studio.
  • GNU: ABI used by the GCC toolchain. Install it yourself for interoperability with the software built with the MinGW/MSYS2 toolchain.

See also alternative installation methods.

RISC-V Targets Only

To build Rust applications for the Espressif chips based on RISC-V architecture, do the following:

  1. Install the nightly toolchain with the rust-src component:

    rustup toolchain install nightly --component rust-src
    
  2. Set the target:

    • For no_std (bare-metal) applications, run:

      rustup target add riscv32imc-unknown-none-elf # For ESP32-C2 and ESP32-C3
      rustup target add riscv32imac-unknown-none-elf # For ESP32-C6 and ESP32-H2
      

      This target is currently Tier 2. Note the different flavors of riscv32 target in Rust covering different RISC-V extensions.

    • For std applications:

      Since this target is currently Tier 3, it doesn't have pre-built objects distributed through rustup and, unlike the no_std target, nothing needs to be installed. Refer to the *-esp-idf section of the rustc book for the correct target for your device.

      • riscv32imc-esp-espidf for SoCs which don't support atomics, like ESP32-C2 and ESP32-C3
      • riscv32imac-esp-espidf for SoCs which support atomics, like ESP32-C6, ESP32-H2, and ESP32-P4
  3. To build std projects, you also need to install:

Now you should be able to build and run projects on Espressif's RISC-V chips.

RISC-V and Xtensa Targets

espup is a tool that simplifies installing and maintaining the components required to develop Rust applications for the Xtensa and RISC-V architectures.

1. Install espup

To install espup, run:

cargo install espup

You can also directly download pre-compiled release binaries or use cargo-binstall.

2. Install Necessary Toolchains

Install all the necessary tools to develop Rust applications for all supported Espressif targets by running:

espup install

⚠️ Note: std applications require installing additional software covered in std Development Requirements

3. Set Up the Environment Variables

espup will create an export file that contains some environment variables required to build projects.

On Windows (%USERPROFILE%\export-esp.ps1)

  • There is no need to execute the file for Windows users. It is only created to show the modified environment variables.

On Unix-based systems ($HOME/export-esp.sh). There are different ways of sourcing the file:

  • Source this file in every terminal:

    1. Source the export file: . $HOME/export-esp.sh

    This approach requires running the command in every new shell.

  • Create an alias for executing the export-esp.sh:

    1. Copy and paste the following command to your shell’s profile (.profile, .bashrc, .zprofile, etc.): alias get_esprs='. $HOME/export-esp.sh'
    2. Refresh the configuration by restarting the terminal session or by running source [path to profile], for example, source ~/.bashrc.

    This approach requires running the alias in every new shell.

  • Add the environment variables to your shell profile directly:

    1. Add the content of $HOME/export-esp.sh to your shell’s profile: cat $HOME/export-esp.sh >> [path to profile], for example, cat $HOME/export-esp.sh >> ~/.bashrc.
    2. Refresh the configuration by restarting the terminal session or by running source [path to profile], for example, source ~/.bashrc.

    This approach doesn't require any sourcing. The export-esp.sh script will be sourced automatically in every shell.

What espup Installs

To enable support for Espressif targets, espup installs the following tools:

  • Espressif Rust fork with support for Espressif targets
  • nightly toolchain with support for RISC-V targets
  • LLVM fork with support for Xtensa targets
  • GCC toolchain that links the final binary

The forked compiler can coexist with the standard Rust compiler, allowing both to be installed on your system. The forked compiler is invoked when using any of the available overriding methods.

⚠️ Note: We are making efforts to upstream our forks

  1. Changes in LLVM fork. Already in progress, see the status in this tracking issue.
  2. Rust compiler forks. If LLVM changes are accepted, we will proceed with the Rust compiler changes.

If you run into an error, please, check the Troubleshooting chapter.

Other Installation Methods for Xtensa Targets

  • Using rust-build installation scripts. This was the recommended way in the past, but now the installation scripts are feature frozen, and all 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. It isn't recommended unless there is a major reason to go for this approach. Here is the repository to build it from source: esp-rs/rust repository.

std Development Requirements

Regardless of the target architecture, make sure you have the following required tools installed to build std applications:

  • python: Required by ESP-IDF
  • git: Required by ESP-IDF
  • ldproxy binary crate: A tool that forwards linker arguments to the actual linker that is also given as an argument to ldproxy. Install it by running:
    cargo install ldproxy
    

The std runtime uses ESP-IDF (Espressif IoT Development Framework) as hosted environment but, users don't need to install it. ESP-IDF is automatically downloaded and installed by esp-idf-sys, a crate that all std projects need to use, when building a std application.

Using Containers

Instead of installing directly on your local system, you can host the development environment inside a container. Espressif provides the idf-rust image that supports both RISC-V and Xtensa target architectures and enables both std and no_std development.

You can find numerous tags for linux/arm64, and linux/amd64 platforms.

For each Rust release, we generate the tag with the following naming convention:

  • <chip>_<rust-toolchain-version>
    • For example, esp32_1.64.0.0 contains the ecosystem for developing std, and no_std applications for ESP32 with the 1.64.0.0 Xtensa Rust toolchain.

There are special cases:

  • <chip> can be all which indicates compatibility with all Espressif targets
  • <rust-toolchain-version> can be latest which indicates the latest release of the Xtensa Rust toolchain

Depending on your operating system, you can choose any container runtime, such as Docker, Podman, or Lima.

Writing Your Own Application

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

You can write an application in the following ways:

  • (Strongly recommended) Generate from a template: Gives you a configured project, saves time, and prevents possible errors.
  • Start from scratch using Cargo: Requires more expertise since you need to configure several parts of the project.

    ⚠️ Note: Starting a project with Cargo doesn't provide any advantage, only mentioned here since it's the usual way of generating a project in Rust.

This chapter won't cover the instructions on how to create a project from scratch with cargo, it will only focus on generating a project from a template project.

The tools used in this chapter will be covered in more detail in the next chapter Tooling, feel free to refer to it when required.

Generating Projects from Templates

We currently maintain 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 configurations and dependencies.

  1. Install cargo generate:

    cargo install cargo-generate
    
  2. Generate a project based on one of the templates:

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

  3. Build/Run the generated project:

    • Use cargo build to compile the project using the appropriate toolchain and target.
    • Use cargo run to compile the project, flash it, and open a serial monitor with our target device.

Using Dev Containers in the Templates

Both template repositories have a prompt for Dev Containers support, see details in Dev Containers section of the template README.

Dev Containers use the idf-rust container image, which was explained in the Using Container section of the Setting up a Development Environment chapter. This image provides 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.

Understanding esp-template

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

Inspecting the Generated Project

When creating a project from esp-template with the following answers:

  • Which MCU to target? · esp32c3
  • Configure advanced template options? · false

For this explanation, we will use the default values, if you want further modifications, see the additional prompts when not using default values.

It should generate a file structure like this:

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

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

  • .cargo/config.toml
    • The Cargo configuration
    • This defines a few options to correctly build the project
    • Contains runner = "espflash flash --monitor" - this means you can just use cargo run to flash and monitor your code
  • src/main.rs
    • The main source file of the newly created project
    • For details, see the Understanding main.rs section below
  • .gitignore
    • Tells git which folders and files to ignore
  • Cargo.toml
    • The usual Cargo manifest declares 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 use a different license, you can delete these files and change the license in Cargo.toml
  • rust-toolchain.toml
    • Defines which Rust toolchain to use
      • The toolchain will be nightly or esp depending on your target

Understanding main.rs

 1 #![no_std]
 2 #![no_main]
  • #![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.
 4 use esp_backtrace as _;
 5 use esp_println::println;
 6 use hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup, Rtc};
  • 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
  • use esp_println::println;
    • Provides println! implementation
  • use hal:{...}
    • We need to bring in some types we are going to use
    • These are from esp-hal
 8 #[entry]
 9 fn main() -> ! {
10    let peripherals = Peripherals::take();
11    let mut system = peripherals.SYSTEM.split();
12    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
13
14    // Disable the RTC and TIMG watchdog timers
15    let mut rtc = Rtc::new(peripherals.RTC_CNTL);
16    let timer_group0 = TimerGroup::new(
17        peripherals.TIMG0,
18        &clocks,
19        &mut system.peripheral_clock_control,
20    );
21    let mut wdt0 = timer_group0.wdt;
22    let timer_group1 = TimerGroup::new(
23        peripherals.TIMG1,
24        &clocks,
25        &mut system.peripheral_clock_control,
26    );
27    rtc.swd.disable();
28    rtc.rwdt.disable();
29    wdt0.disable();
30    wdt1.disable();
31
32    println!("Hello world!");
33
34    loop {}
35 }

Inside the main function we can find:

  • 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 mut 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 can't 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
  • println!("Hello world!");
    • Prints "Hello world!"
  • 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:

[2023-04-17T14:17:08Z INFO ] Serial port: '/dev/ttyACM0'
[2023-04-17T14:17:08Z INFO ] Connecting...
[2023-04-17T14:17:09Z INFO ] Using flash stub
[2023-04-17T14:17:09Z WARN ] Setting baud rate higher than 115,200 can cause issues
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:    203,920/4,128,768 bytes, 4.94%
[00:00:00] [========================================]      13/13      0x0
[00:00:00] [========================================]       1/1       0x8000
[00:00:01] [========================================]      64/64      0x10000
[2023-04-17T14:17:11Z INFO ] Flashing has completed!
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

...
Hello world!

What you see here are messages from the first and second stage bootloader, and then, our "Hello world" message!

And that is exactly what the code is doing.

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

If you encounter any issues while building the project, please, see the Troubleshooting chapter.

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 with the following answers:

  • Which MCU to target? · esp32c3
  • Configure advanced template options? · false

For this explanation, we will use the default values, if you want further modifications, see the additional prompts when not using default values.

It should generate a file structure like this:

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

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

  • .cargo/config.toml
    • The Cargo configuration
    • Contains our target
    • Contains runner = "espflash flash --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 environment 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
    • For details, see the Understanding main.rs section below
  • .gitignore
    • Tells git which folders and files to ignore
  • build.rs
    • Propagates linker arguments for ldproxy
  • Cargo.toml
    • The usual Cargo manifest declaring some meta-data and dependencies of the project
  • rust-toolchain.toml
    • Defines which Rust toolchain to use
      • The toolchain will be nightly or esp depending on your target
  • sdkconfig.defaults
    • Contains the overridden values from the ESP-IDF defaults

Understanding main.rs

1 use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported
2
3 fn main() {
4     // It is necessary to call this function once. Otherwise some patches to the runtime
5     // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
6     esp_idf_sys::link_patches();
7     println!("Hello, world!");
8 }

The first line is 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 a usual main function with a few 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 on 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:

[2023-04-18T08:05:09Z INFO ] Connecting...
[2023-04-18T08:05:10Z INFO ] Using flash stub
[2023-04-18T08:05:10Z WARN ] Setting baud rate higher than 115,200 can cause issues
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:    478,416/4,128,768 bytes, 11.59%
[00:00:00] [========================================]      13/13      0x0
[00:00:00] [========================================]       1/1       0x8000
[00:00:04] [========================================]     227/227     0x10000
[2023-04-18T08:05:15Z INFO ] Flashing has completed!
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

...
I (344) 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!" is printed.

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

If you encounter any issues while building the project, please, see the Troubleshooting chapter.

Writing no_std Applications

If you want to learn how to develop no_std application, see the following training materials:

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

The training contains:

⚠️ Note: There are several examples covering the use of specific peripherals under the examples' folder of every SoC esp-hal. E.g. esp32c3-hal/examples

Writing std Applications

If you want to learn how to develop std application, see the following training materials developed alongside Ferrous Systems:

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

The training is split into two parts:

⚠️ Note: There are several examples covering the use of specific peripherals under the examples' folder of esp-idf-hal. I.e. esp-idf-hal/examples.

Tooling

Now that we have our required dependencies installed, and we know how to generate a template project, we will cover, in more detail, some tools. These tools will make developing Rust applications for Espressif chips a lot easier.

In this chapter, we will present espflash/cargo-espflash, suggest Visual Studio Code as IDE and, dig into the currently available simulation and debugging methods.

Visual Studio Code

One of the more common development environments is Microsoft's Visual Studio Code text editor along with the Rust Analyzer, also known as RA, 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 the 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 there are other extensions that might be helpful:

Tips and Tricks

Using Rust Analyzer with no_std

If you are developing for a target that doesn't 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
}

Cargo Hints When Using Custom Toolchains

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"]

Other IDEs

We chose to cover VS Code because it has good support for Rust and is popular among developers. There are also other IDEs available that have comparable Rust support, such as CLion and vim, but these are outside of this book's scope.

espflash

espflash is a serial flasher utility, based on esptool.py, for Espressif SoCs and modules.

The espflash repository contains two crates, cargo-espflash and espflash. For more information on these crates, see the respective sections below.

⚠️ Note: The espflash and cargo-espflash commands shown below, assume that version 2.0 or greater is used.

cargo-espflash

Provides a subcommand for cargo that handles cross-compilation and flashing.

To install, run:

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', flash the resulting binary to a device, and then subsequently start a serial monitor:

cargo espflash flash --example=blinky --monitor

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

espflash

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

To install, run:

cargo install espflash

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

espflash flash build/blinky --monitor

For more information, please see the espflash README.

espflash can be used as a Cargo runner by adding the following to your project's .cargo/config.toml file:

[target.'cfg(any(target_arch = "riscv32", target_arch = "xtensa"))']
runner = "espflash flash --monitor"

With this configuration, you can flash and monitor your application using cargo run.

Debugging

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

Refer to the table below to see which chip is supported in every debugging method:

probe-rsOpenOCDVS Code
ESP32
ESP32-C2
ESP32-C3
ESP32-C6
ESP32-H2
ESP32-S2
ESP32-S3

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 & RISC-V architectures along with a collection of tools, including but not limited to:

  • Debugger
    • GDB support.
    • CLI for interactive debugging.
    • VS Code extension.
  • Real Time Transfer (RTT)
    • 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 built-in 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 doesn't 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 built-in 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, 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

Configuration

  1. Connect an external JTAG adapter: 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.

  1. Set up VSCode
    1. Install Cortex-Debug extension for VS Code.
    2. Create the .vscode/launch.json file in the project tree you want to debug.
    3. Update executable, svdFile, serverpath paths, and toolchainPrefix fields.
{
  // 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

The availability of built-in JTAG interface depends on the ESP32-C3 revision:

  • Revisions older than 3 don't a have built-in JTAG interface.
  • Revisions 3 (and newer) do have a built-in JTAG interface, and you don't have to connect an external device to be able to debug.

To find your ESP32-C3 revision, run:

cargo espflash board-info
# or
espflash board-info

Configuration

  1. (Only for revisions older than 3) Connect an external JTAG adapter, 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.

  1. Set up VSCode
    1. Install Cortex-Debug extension for VS Code.
    2. Create the .vscode/launch.json file in the project tree you want to debug.
    3. Update executable, svdFile, serverpath paths, and toolchainPrefix fields.
{
  // 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 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. Every way has some limitations, but it's quickly evolving and getting better every day.

In this chapter, we will discuss currently available simulation tools.

Refer to the table below to see which chip is supported in every simulating method:

WokwiQEMU
ESP32
ESP32-C2
ESP32-C3
ESP32-C6
ESP32-H2
ESP32-S2
ESP32-S3

Wokwi

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

Wokwi offers Wi-Fi 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 for VS Code extension

Wokwi offers a VS Code extension that allows you to simulate a project directly in the code editor by only adding a few files. For more information, see Wokwi documentation. You can also debug your code using the VS Code debugger, see Debugging your code.

When using any of the templates and not using the default values, there is a prompt (Configure project to support Wokwi simulation with Wokwi VS Code extension?) that generates the required files to use Wokwi VS Code extension.

Wokwi VS Code example

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.

Custom Chips

Wokwi allows generating custom chips that let you program the behavior of a component not supported in Wokwi. For more details, see the official Wokwi documentation.

Custom chips can also be written in Rust! See Wokwi Custom Chip API for more information. For example, custom inverter chip in Rust.

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 the qemu-system-xtensa file.

Running Your 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 --chip esp32 --merge <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

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 you have sourced the export file generated by espup, see Set up the environment variables.

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. See std Development Requirements

cargo install ldproxy

Using the 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 aren't 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.

FAQ

sdkconfig.defaults File is Updated 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. For this reason, 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:

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 7 kB for the main task's stack.

How to Disable 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.