Introduction: ESP32 / ESP32C3 Blink Test Rust Development in Windows WSL

In this post, I will list out the steps I did for developing two blink test Rust programs for blinking ESP32 as well as ESP32C3.

For more details, please refer to The Rust on ESP Book, on which the steps listed here are heavily based.

The ESP32 board is a common ESP32 Dev Kit board.

The ESP32C3 board is an Ai Thinker ESP-C3-32S-Kit board.

I will be developing the Rust programs in Windows WSL (Ubuntu) using VSCode. Hence, I would assume both be installed.

Unlike with Arduino framework, developing microcontroller programs in Rust is much more intimidating [to me]. In fact, the steps to set up the development environment are [currently] pretty involving too.

Hopefully, the steps listed here will be easy enough to follow.

As termed in the Rust on ESP Book, the Rust programs here will be on no_std bare meta ecosystem.

Maybe, you would prefer the standard approach. But as stated in the Rust on ESP Book

It's important to note that since no_std uses the Rust core library, a subset of the Rust standard library, a no_std crate can compile in std environment but the opposite is not true. 

Hence, I guess it doesn't hurt to try out no_std approach. After all, the installation for the development environment will be the same; just that creating the initial Rust program template is a bit different.

Step 1: Optionally, Install ESP-IDF Tools

Since we are developing for ESP boards, It is certainly desirable to install the ESP-IDF Tools. However, do note that this step is optional, at least for the Rust programs mentioned in this post.

Install to WSL (Ubuntu) the needed libraries.

sudo apt-get install git wget flex bison gperf python3 python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

Create a directory, say esp, for the tools, as well as for Rust program development.

mkdir ~/esp
cd ~/esp
  • This will create esp at the root of your home directory
  • CD into it for the following steps
git clone --recursive https://github.com/espressif/esp-idf.git
  • This will clone the source from the GitHub esp-idf repo
  • The source will be cloned to a subdirectory esp-idf

CD into esp-idf

cd esp-idf

Run the install script there

./install.sh esp32

That is it. You have installed the ESP-IDF tools. Nevertheless, note that you will need to run the export.sh script in esp-idf in order to set up the shell environment [every time] for ESP development with the tools. Therefore, it is recommended that you create a script setup.sh in esp directory for such task

echo 'source ~/esp/esp-idf/export.sh' > ~/esp/setup.sh
chmod +x ~/esp/setup.sh

Now, every time (every shell session) you want to set up for ESP development with the ESP-IDF tools, simply

source ~/esp/setup.sh

Step 2: Install Rust

As stated in Rust Getting Started, installing Rust is as simple as running

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

Afterward, for the installation to take effect, restart (close and open) WSL

Check that Rust and related tools are installed

trevorlee@TrevorP14s:~/esp$ rustc --version
rustc 1.70.0 (90c541806 2023-05-31)
trevorlee@TrevorP14s:~/esp$ cargo --version
cargo 1.70.0 (ec8a8a0ca 2023-04-25)

Step 3: Install Espup

As stated by the espup GitHub repo

espup is a tool for installing and maintaining the required toolchains for developing applications in Rust for Espressif SoC's.

Hence, you will need to install the ESP Rust development tools, similar to the ESP-IDF tools mentioned in a prior section; but since espup is specific for Rust, take this approach for installing the needed ESP tools for Rust development.

Before installing espup, you first need to install other essential libraries for building the tools. You install those essential libraries by running

sudo apt update
sudo apt install build-essential

To actually install espup, run

cargo install espup  
espup install

This will create the script file ~/export-esp.sh. You "source" this script in order to set up environment for ESP Rust development, like

source ~/export-esp.sh

Step 4: Install Espflash

You will need espflash to flash your Rust program to your ESP board.

Before installing espflash, you will also need to install other libraries:

sudo apt-get install pkg-config libusb-1.0-0-dev libftdi1-dev libudev-dev libssl-dev

The above installations should enable the installation of espflash without issues

To install espflash, run

cargo install espflash

Step 5: Install Cargo-generate

In order to create the initial Rust program template for ESP development, you will need to have cargo-generate installed

cargo install cargo-generate

BTW. You can generate no_std Rust ESP program template like

cargo generate -a esp-rs/esp-template

And you can generate std Rust ESP program template like

cargo generate esp-rs/esp-idf-template cargo

Step 6: First Rust ESP32 Program

For easier keeping track of Rust ESP projects, you may want to create some "Rust ESP Project Root Directory" like ~/esp/rust-esp

mkdir ~/esp/rust-esp
cd ~/esp/rust-esp

Generate template for the first Rust ESP32 program

cargo generate -a https://github.com/esp-rs/esp-template

Make sure to select esp32 when prompted; for other choices, just use the defaults

trevorlee@TrevorP14s:~/esp/rust-esp$ cargo generate -a https://github.com/esp-rs/esp-template
⚠️   Favorite `https://github.com/esp-rs/esp-template` not found in config, using it as a git repository: https://github.com/esp-rs/esp-template
🤷   Project Name: esp32hello
🔧   Destination: /home/trevorlee/esp/rust-esp/esp32hello ...
🔧   project-name: esp32hello ...
🔧   Generating template ...
✔ 🤷   Use template default values? · true
✔ 🤷   Which MCU to target? · esp32

💡 When using `espflash` version 1.x you need to edit `.cargo/config.toml`
💡 Make the runner look like this `runner = "espflash --monitor"`

Use `cargo run` to flash and run your code

🔧   Moving generated files into: `/home/trevorlee/esp/rust-esp/esp32hello`...
Initializing a fresh Git repository
✨   Done! New project created /home/trevorlee/esp/rust-esp/esp32hello

The generated template is actually a runnable program. However, as stated in the output of running cargo generate, you may need to modify .cargo/config.toml; but this will be next. First, try building it.

cd esp32hello
cargo build

If you see an error like

error: linker `xtensa-esp32-elf-gcc` not found

Likely you have not set up the environment to use ESP tools. "Source" the export-esp.sh script created when installing espup. (Note that you may not need to do this for all ESP microcontrollers supported.)

 source ~/export-esp.sh

This time, cargo build should be successful

trevorlee@TrevorP14s:~/esp/rust-esp/esp32hello$ cargo build
   Compiling esp32hello v0.1.0 (/home/trevorlee/esp/rust-esp/esp32hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s

However, to be able to upload the Rust program to your ESP microcontroller from WSL, you will need to make WSL able to access USB ports on your Windows machine.

Step 7: WSL Connecting Connect USB Devices

As hinted by Microsoft's doc on connecting USB devices from WSL, such so expected capability does not come out of the box.

Here I will list the steps to enable such capability; in order that the previous steps on setting up WSL for Rust development for ESP microcontroller board can be complete.

In Windows:

  1. Go to the latest release page for the usbipd-win project.
  2. Select the .msi file, which will download the installer. (You may get a warning asking you to confirm that you trust this download).
  3.  Run the downloaded usbipd-win_x.msi installer file.

In WSL:

sudo apt install linux-tools-generic hwdata
sudo update-alternatives --install /usr/local/bin/usbip usbip /usr/lib/linux-tools/*-generic/usbip 20

Now, you are ready to attach USB device to WSL.

With Windows' Command Prompt (cmd) run as administrator, running usbipd wsl list should allow you to see the list of USB devices recognized by Windows

C:\Windows\System32>usbipd wsl list
BUSID  VID:PID    DEVICE                                                        STATE
1-4    04f2:b6d0  Integrated Camera, Integrated IR Camera, Camera DFU Device    Not attached
3-3    06cb:00bd  Synaptics UWP WBDI                                            Not attached
3-4    0489:e0cd  MediaTek Bluetooth Adapter                                    Not attached

Plug your ESP microcontroller board to a USB port of Windows, and run usbipd wsl list again, you should see the added USB device of your board

C:\Windows\System32>usbipd wsl list
BUSID  VID:PID    DEVICE                                                        STATE
1-4    04f2:b6d0  Integrated Camera, Integrated IR Camera, Camera DFU Device    Not attached
3-1    1a86:55d4  USB-Enhanced-SERIAL CH9102 (COM5)                             Not attached
3-3    06cb:00bd  Synaptics UWP WBDI                                            Not attached
3-4    0489:e0cd  MediaTek Bluetooth Adapter                                    Not attached

Note down the BUSID (in my case 3-1). You attach this USB device to WSL with the BUSID, like

usbipd wsl attach --busid 3-1

After running the command, you should see something like

C:\Windows\System32>usbipd wsl attach --busid 3-1
usbipd: info: Using default WSL distribution 'Ubuntu-20.04'; specify the '--distribution' option to select a different one.

That is it. Your ESP board is attached to WSL.

BTW. You can detach attached USB device like

usbipd wsl detach --busid 3-1

Note that every time you unplug and re-plug your ESP board, you will need to run the command again. It is kind of a pain since microcontroller development may involve lots of unplugging and re-plugging of the microcontroller board.

The good news is, usbipd tool comes with an option to stay watching the same USB port and reattach when you re-plug it.

To enable this option, the first time you attach, you run usbipd like

usbipd wsl attach -a -b 3-1
  • the said option is specified with -a
  • -b 3-1 is equivalent to --busid 3-1
  • you can combine the two like -ab 3-1

It will then attach your ESP board to WSL, and stay watching the port for any unplugging and re-plugging, like

C:\Windows\System32>usbipd wsl attach -ab 3-1
usbipd: info: Using default WSL distribution 'Ubuntu-20.04'; specify the '--distribution' option to select a different one.
usbipd: info: Starting endless attach loop; press Ctrl+C to quit.
Attached
Detached
usbip: error: Attach Request for 3-1 failed - Device not found
Attached
Detached
usbip: error: Attach Request for 3-1 failed - Device not found
Attached

Just keep the prompt running.

Step 8: Upload the First Rust Program to ESP32

Assuming you have plugged your ESP32 board and attached it as described in the previous section.

You should be able to build and upload the previous esphello Rust program to your ESP32 board.

Before uploading, modify .cargo/config.toml

changing

runner = "espflash flash --monitor"

to

runner = "espflash --monitor"

By removing the original "flash", you tell espflash to try to detect the USB device to use

Alternatively, if you know that the USB device, say is ttyUSB0, you can specify it like

runner = "espflash ttyUSB0 --monitor"

BTW, I would assume that you use VSCode for the development. You can start VSCode in the current WSL directory ~esp/rust-esp/esp32hello like

trevorlee@TrevorP14s:~/esp/rust-esp/esp32hello$ code .

This will start the Windows installation of VSCode to open the WSL directory for "remote" development.

To build and upload the Rust program, you open up a terminal and run

source ~/export-esp.sh
cargo run

Note that you only need to do source ~/export-esp.sh the first time you open a new terminal, to set up the terminal environment for ESP Rust development.

The command cargo run not only builds the Rust program and uploads the compiled binary to your ESP board, it also starts monitoring the board's Serial output as well.

Step 9: Developing ESP32 Blink

To start developing a new ESP Rust program for blinking the in-built LED of ESP32, first, generate the template, say esp32blink. (Make sure to select esp32 when prompted.)

cargo generate -a https://github.com/esp-rs/esp-template

CD into the generated directory esp32blink, and start VSCode

cd esp32blink
code .

With VSCode, modify .cargo/config.toml, changing "runner" like

runner = "espflash --monitor"

The main programming change is to src/main.rs

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_println::println;
use hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup, Rtc};

// use two additional modules
use hal::{IO, Delay};

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let mut system = peripherals.DPORT.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,
        &mut system.peripheral_clock_control,
    );
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(
        peripherals.TIMG1,
        &clocks,
        &mut system.peripheral_clock_control,
    );
    let mut wdt1 = timer_group1.wdt;
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();
    println!("Hello world!");

    // create a delay object
    let mut delay = Delay::new(&clocks);

    // create an io object
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);

    // from the io object, get pin GPIO2 LED
    let mut led = io.pins.gpio2.into_push_pull_output();

    loop {
        // delay for 1000 milli-seconds
        delay.delay_ms(1000u32);  

        // toggle pin led
        led.toggle().unwrap();

        // print out to serial ...
        println!("...");
    }
}
  • "Import" two additional modules -- IO is for creating IO objects; Delay is for creating a delay object for causing delay
...
// use two additional modules
use hal::{IO, Delay};
...
    // create a delay object
    let mut delay = Delay::new(&clocks);
    // create an io object
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
...
  • Via the created IO object, you get the pin object attached to the in-built LED (pin 2)
  • At last, simply loop toggling the led ON and OFF, with 1000 milli-seconds in between. Note the println!() is used to print something to Serial
    loop {
        // delay for 1000 milli-seconds
        delay.delay_ms(1000u32);  

        // toggle pin led
        led.toggle().unwrap();

        // print out to serial ...
        println!("...");
    }

With the programming change, you should be able to run the Rust program and see your ESP32 board blinking the in-built LED (pin 2).

To build and flash, run

cargo run

Step 10: Developing ESP32C3 Blink

Blinking ESP32C3 is similar. As mentioned in my YouTube video ESP32-C3 Blink Test with Arduino IDE and DumbDisplay, the Ai Thinker ESP32C3 board can be considered to have a total of 5 in-built LEDs, why not blink them all?

Generate template for the ESP32C3 blink program c3blink. (Make sure to select esp32c3 when prompted.)

cargo generate -a https://github.com/esp-rs/esp-template

CD into the generated directory c3blink, and start VSCode

cd c3blink
code .

With VSCode, modify .cargo/config.toml, changing "runner" like

runner = "espflash --monitor"

The main program is src/main.rs

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_println::println;
use hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup, Rtc};

// use two additional modules
use hal::{IO, Delay};

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let mut 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,
        &mut system.peripheral_clock_control,
    );
    let mut wdt0 = timer_group0.wdt;
    let timer_group1 = TimerGroup::new(
        peripherals.TIMG1,
        &clocks,
        &mut system.peripheral_clock_control,
    );
    let mut wdt1 = timer_group1.wdt;
    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();
    println!("Hello world!");

    // create a delay object
    let mut delay = Delay::new(&clocks);

    // create an io object
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);

    // from the io object, get use various pins as LEDs
    let mut cool_led = io.pins.gpio19.into_push_pull_output();
    let mut warm_led = io.pins.gpio18.into_push_pull_output();
    let mut red_led = io.pins.gpio3.into_push_pull_output();
    let mut green_led = io.pins.gpio4.into_push_pull_output();
    let mut blue_led = io.pins.gpio5.into_push_pull_output();

    // since the cool LED is ON by default; turn if OFF initially
    cool_led.set_low().unwrap();

    let mut idx = 0;
    loop {
        delay.delay_ms(500u32);  // delay 500 ms  
        match idx  {
            0 => cool_led.toggle().unwrap(),
            1 => warm_led.toggle().unwrap(),
            2 => red_led.toggle().unwrap(),
            3 => green_led.toggle().unwrap(),
            4 => blue_led.toggle().unwrap(),
            _ => panic!("unexpected")
        }
        delay.delay_ms(500u32);  // delay 500 ms  
        match idx {
            0 => cool_led.toggle().unwrap(),
            1 => warm_led.toggle().unwrap(),
            2 => red_led.toggle().unwrap(),
            3 => green_led.toggle().unwrap(),
            4 => blue_led.toggle().unwrap(),
            _ => panic!("unexpected")
        }
        println!("...");
        idx = (idx + 1) % 5;
    }
}

You may notice the two repeat blocks of code

        match idx  {
            0 => cool_led.toggle().unwrap(),
            1 => warm_led.toggle().unwrap(),
            2 => red_led.toggle().unwrap(),
            3 => green_led.toggle().unwrap(),
            4 => blue_led.toggle().unwrap(),
            _ => panic!("unexpected")
        }

to toggle the different LEDs depending on the variable idx.

For a challenge, you might be interested in refactoring that piece of code in order to stick to the DRY (don't repeat yourself) principle.

  1. The block of code is repeated 2 times; hence, refactor the repeated code block using a function, like toggleLed(idx)
  2. The purpose of the code block is to match idx to find out the LED to toggle(); the action on the LED is the same toggle().unwrap(). Why not find out the LED reference, then do whatever the same on it?

In fact, it is my own challenge, which I find not as obvious in Rust as in C++.

Step 11: Enjoy!

Hope you can enjoy programming ESP microcontroller board in Rust. If you are like me, you will find it very challenging. I believe Rust may soon be a good alternative to C++ for microcontroller program development.

Peace be with you! May God bless you! Jesus loves you!