ESP32 / ESP32C3 Blink Test Rust Development in Windows WSL
by Trevor Lee in Circuits > Microcontrollers
1857 Views, 3 Favorites, 0 Comments
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.
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
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)
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
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
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
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.
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:
- Go to the latest release page for the usbipd-win project.
- Select the .msi file, which will download the installer. (You may get a warning asking you to confirm that you trust this download).
- 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.
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.
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
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.
- The block of code is repeated 2 times; hence, refactor the repeated code block using a function, like toggleLed(idx)
- 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++.
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!