Guide to Developing ESP32 Series Microcontrollers Using WSL + VSCode + ESP-IDF6

Guide to Developing ESP32 Series Microcontrollers Using WSL + VSCode + ESP-IDF6

Introduction

I can no longer tolerate the Windows environment; the development experience is too poor, and the performance of the file system is not great (which may be largely due to my lack of configuration knowledge). Compiling the ESP project took me long enough to go for a walk and come back. In comparison, developing on Linux is clearly much more cost-effective. It has been a smooth experience since then.

The main purpose of this blog is to document the process of developing microcontrollers on WSL, after which other microcontroller developments can be handled using PlatformIO. Here, I will record my development process.

Preparing WSL

I am a frequent user of WSL. Here, I would like to remind you of some settings because WSL will default to appending Windows’ PATH variable. I was previously misled while writing frontend code, so if you want your WSL to be specifically for microcontroller development, I recommend adding the last two lines of configuration in <span>/etc/wsl.conf</span>. This tells WSL not to include the Windows PATH when loading environment variables. Of course, if this is not your intention, do not do this.

❯ cat /etc/wsl.conf 
[boot]
systemd=true

[automount]
enabled = true
options = "metadata"
mountFsTab = true

[interop]
appendWindowsPath = false

I prefer using Arch WSL. Here is the GitHub link for those who want to try it, or you can directly download the default Arch WSL.

Arch WSL GitHub

There are no new settings for WSL beyond this.

Installing ESP-IDF Prerequisites

I have a proxy, and I recommend that those with a proxy, especially those using Clash to speed up GitHub downloads, enable TUN mode. This way, Clash will virtualize a network card, forwarding all network activities on the computer to the virtual network card, allowing scripts that do not use environment variables to also use the proxy, speeding up downloads.

For those without a proxy, refer to the official ESP documentation on how to download:

(Tips: I recommend configuring the Python environment used for IDF development in a separate venv environment to avoid breaking the entire WSL Python environment.)

[Image and Text] Setting up ESP32/ESP32-S2 Development Environment with WSL + VSCode – wsl esp32-CSDN Blog

How to Set Up ESP32 Development Environment with WSL + VSCode – Bilibili

After downloading the prerequisites, follow the instructions to download the ESP-IDF toolchain. I will not repeat that here. I suggest you set up an export.sh pointing to your ESP-IDF toolchain, which can be useful for debugging in the command line when VSCode has issues and you cannot understand the logs.

alias export_esp_env="source /path/to/your/esp-idf/export.sh"

Check it:

❯ export_esp_env # Test if our environment is set up correctly
Checking "python3" ...
Python 3.13.7
"python3" has been detected
Activating ESP-IDF 6.1
Setting IDF_PATH to '/home/charliechen/esp-idf'.
* Checking python version ... 3.13.7
* Checking python dependencies ... OK
* Deactivating the current ESP-IDF environment (if any) ... OK
* Establishing a new ESP-IDF environment ... OK
* Identifying shell ... zsh
* Detecting outdated tools in system ... OK - no outdated tools found
* Shell completion ... Autocompletion code generated

Done! You can now compile ESP-IDF projects.
Go to the project directory and run:

  idf.py build

❯ idf.py --version # OK, it's very new, version 6.1
ESP-IDF v6.1-dev-786-g130fdc7ce7-dirty

Installing VSCode Extensions

Install the ESP-IDF extension in VSCode. After downloading, press Ctrl + Shift + P to bring up the command palette, type Configure ESP-IDF, and then select “ADVANCED” to enter the advanced configuration interface. Choose the location of the ESP-IDF toolchain you just downloaded and select the Python environment you are using (I prepared a separate venv Python environment for this). Remember to keep the proxy enabled for faster configuration.

I also recommend installing the CMake extension (CMake + CMake Tools).

Preparing the Project

USB Preparation

The next step is to prepare the USB connection. USB access in WSL can be tricky. Fortunately, there is a useful tool called <span>usbipd</span>, but unfortunately, downloading this tool also requires a VPN.

Releases · dorssel/usbipd-win

After downloading and installing this tool, open CMD or PowerShell in administrator mode, plug in your ESP32 development board, and prepare to set it up. Enter <span>usbipd list</span> to see the USB port for your ESP32. Generally, it starts with COM, and remember the BUSID, which is 2-2 in this case.

➜  usbipd list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
...
2-2    303a:1001  USB Serial Device (COM5), USB JTAG/serial debug unit               Shared
...

Persisted:
GUID                                  DEVICE

Next, we need to bind it to WSL. Using my BUSID of 2-2 as an example:

# Do not copy and paste; operate according to your own BUSID!!!
usbipd bind -b 2-2 # This only needs to be done once; you don't need to enter it again when plugging and unplugging.
usbipd attach --wsl --busid  2-2 # This command will attach the USB to WSL. Note that the USB cannot be used on the Windows host at this time unless you disconnect it!

After entering the commands, the output of the last command will be:

usbipd: info: Using WSL distribution 'simplechip' to attach; the device will be available in all WSL 2 distributions.
usbipd: info: Loading vhci_hcd module.
usbipd: info: Detected networking mode 'mirrored'.
usbipd: info: Using IP address 127.0.0.1 to reach the host.

This indicates success.

Check if there are new devices connected in your WSL. Generally, the mapped devices are <span>/dev/ttyACM*</span> or <span>/dev/ttyUSB*</span>. Mine is <span>/dev/ttyACM0</span>

❯ ls /dev/ttyACM0
/dev/ttyACM0

❯ ls -lh /dev/ttyACM0
crw-rw---- 1 root uucp 166, 0 Nov 24 12:52 /dev/ttyACM0

Press Ctrl + Shift + P to bring up the command palette, and then we can create a project. In the New Project settings, specify the chip model of your ESP32 development board, the USB port just connected, and other information. Now, start; the template no longer has sample_project, only Hello World, so select that.

The next step is a big pitfall—ESP-IDF version 6.1 supports minimal builds. If you need other modules, such as PSRAM on my chip, you need to turn off minimal builds.

idf_build_set_property(MINIMAL_BUILD OFF)

Originally, this was set to ON; change it to OFF, and then refresh. (<span>idf.py menuconfig</span> or click the small gear icon next to your chip in the VSCode status bar; note that this is not the VSCode settings but the one showing your chip). Then you can start setting your chip’s properties. If you cannot find a setting, it may be due to a name change or that MINIMAL_BUILD is enabled, so remember to check the documentation.

#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_psram.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"

void app_main(void)
{
    esp_err_t ret;
    uint32_t flash_size;
    esp_chip_info_t chip_info;
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES
        || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    esp_flash_get_size(NULL, &amp;flash_size); /* Get FLASH size */
    esp_chip_info(&amp;chip_info);
    printf("cup%d\n", chip_info.cores); /* Get CPU core count and display */
    /* Get FLASH size and display */
    printf("FLASH size:%ld MB flash\n", flash_size / (1024 * 1024));
    /* Get PSRAM size and display */
    printf("PSRAM size: %d bytes\n", esp_psram_get_size());
    while (1) {
        vTaskDelay(1000);
    }
}
            

I am working with the ESP32S3, which has PSRAM. I suggest you find the example program for your chip, copy it, and compile it to see if it works. You can also find the compile option in the status tool bar at the bottom; I will not teach that here. After a successful compilation, you will see something like this:

                            Memory Type Usage Summary                              
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Memory Type/Section ┃ Used [bytes] ┃ Used [%] ┃ Remain [bytes] ┃ Total [bytes] ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ Flash Code          │        96206 │          │                │               │
│    .text            │        96206 │          │                │               │
│ DIRAM               │        55167 │    16.14 │         286593 │        341760 │
│    .text            │        39947 │    11.69 │                │               │
│    .data            │        12820 │     3.75 │                │               │
│    .bss             │         2400 │      0.7 │                │               │
│ Flash Data          │        41428 │          │                │               │
│    .rodata          │        41172 │          │                │               │
│    .appdesc         │          256 │          │                │               │
│ IRAM                │        16384 │    100.0 │              0 │         16384 │
│    .text            │        15356 │    93.73 │                │               │
│    .vectors         │         1028 │     6.27 │                │               │
│ RTC SLOW            │           36 │     0.44 │           8156 │          8192 │
│    .force_slow      │           36 │     0.44 │                │               │
│ RTC FAST            │           24 │     0.29 │           8168 │          8192 │
│    .rtc_reserved    │           24 │     0.29 │                │               │
└─────────────────────┴──────────────┴──────────┴────────────────┴───────────────┘
Total image size: 206821 bytes (.bin may be padded larger)

The next step is to prepare for flashing.

Flashing and Debugging ESP32

Flashing

Flashing and debugging can be quite troublesome.

ls -lh /dev/ttyACM0                 
crw-rw---- 1 root uucp 166, 0 Nov 24 12:52 /dev/ttyACM0

As you can see, by default, we do not have permission to read and write this USB device. The solution is to add yourself to the uucp group.

sudo usermod -aG uucp $USER
newgrp uucp
groups # Check your groups
charliechen uucp wheel

Now it’s OK, and you can try flashing.

Connected to ESP32-S3 on /dev/ttyACM0:
Chip type:          ESP32-S3 (QFN56) (revision v0.2)
Features:           Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency:  40MHz
USB mode:           USB-Serial/JTAG
MAC:                98:88:e0:00:86:f4

Stub flasher running.
Changing baud rate to 460800...
Changed.

Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x00010000 to 0x00042fff...
SHA digest in image updated.
Wrote 22720 bytes (14489 compressed) at 0x00000000 in 0.4 seconds (429.3 kbit/s).
Hash of data verified.
Wrote 3072 bytes (132 compressed) at 0x00008000 in 0.1 seconds (485.9 kbit/s).
Hash of data verified.
Wrote 206944 bytes (114556 compressed) at 0x00010000 in 1.9 seconds (885.1 kbit/s).
Hash of data verified.

Hard resetting via RTS pin...

Click the small button on the display:

I (24) boot: ESP-IDF v6.1-dev-786-g130fdc7ce7-dirty 2nd stage bootloader
I (25) boot: compile time Nov 24 2025 10:36:40
I (25) boot: Multicore bootloader
I (27) boot: chip revision: v0.2
I (30) boot: efuse block revision: v1.3
I (33) qio_mode: Enabling default flash chip QIO
I (38) boot.esp32s3: Boot SPI Speed : 80MHz
I (41) boot.esp32s3: SPI Mode       : QIO
I (45) boot.esp32s3: SPI Flash Size : 16MB
I (49) boot: Enabling RNG early entropy source...
I (53) boot: Partition Table:
I (56) boot: ## Label            Usage          Type ST Offset   Length
I (62) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (69) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (75) boot:  2 factory          factory app      00 00 00010000 001f0000
I (82) boot:  3 vfs              Unknown data     01 81 00200000 00a00000
I (88) boot:  4 storage          Unknown data     01 82 00c00000 00400000
I (95) boot: End of partition table
I (98) esp_image: segment 0: paddr=00010020 vaddr=3c020020 size=09fach ( 40876) map
I (112) esp_image: segment 1: paddr=00019fd4 vaddr=3fc91d00 size=03214h ( 12820) load
I (115) esp_image: segment 2: paddr=0001d1f0 vaddr=40374000 size=02e28h ( 11816) load
I (123) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=169b0h ( 92592) map
I (142) esp_image: segment 4: paddr=000369d8 vaddr=40376e28 size=0ae18h ( 44568) load
I (151) esp_image: segment 5: paddr=000417f8 vaddr=50000000 size=00024h (    36) load
I (158) boot: Loaded app from partition at offset 0x10000
I (158) boot: Disabling RNG early entropy source...
I (169) octal_psram: vendor id    : 0x0d (AP)
I (169) octal_psram: dev id       : 0x02 (generation 3)
I (169) octal_psram: density      : 0x03 (64 Mbit)
I (171) octal_psram: good-die     : 0x01 (Pass)
I (176) octal_psram: Latency      : 0x01 (Fixed)
I (180) octal_psram: VCC          : 0x01 (3V)
I (184) octal_psram: SRF          : 0x01 (Fast Refresh)
I (189) octal_psram: BurstType    : 0x01 (Hybrid Wrap)
I (194) octal_psram: BurstLen     : 0x01 (32 Byte)
I (198) octal_psram: Readlatency  : 0x02 (10 cycles@Fixed)
I (203) octal_psram: DriveStrength: 0x00 (1/1)
I (208) MSPI Timing: Enter psram timing tuning
I (213) esp_psram: Found 8MB PSRAM device
I (216) esp_psram: Speed: 80MHz
I (219) cpu_start: Multicore app
I (654) esp_psram: SPI SRAM memory test OK
I (663) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (663) cpu_start: Pro cpu start user code
I (663) cpu_start: cpu freq: 240000000 Hz
I (665) app_init: Application information:
I (669) app_init: Project name:     demo0
I (672) app_init: App version:      1
I (676) app_init: Compile time:     Nov 24 2025 10:33:56
I (681) app_init: ELF file SHA256:  b624351c6...
I (685) app_init: ESP-IDF:          v6.1-dev-786-g130fdc7ce7-dirty
I (691) efuse_init: Min chip rev:     v0.0
I (695) efuse_init: Max chip rev:     v0.99 
I (699) efuse_init: Chip rev:         v0.2
I (703) heap_init: Initializing. RAM available for dynamic allocation:
I (709) heap_init: At 3FC95860 len 00053EB0 (335 KiB): RAM
I (714) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (719) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (725) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (730) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator
I (737) spi_flash: detected chip: boya
I (740) spi_flash: flash io: qio
I (743) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (749) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (756) main_task: Started on CPU0
I (759) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (767) main_task: Calling app_main()
cup2
FLASH size:16 MB flash
PSRAM size: 8388608 bytes

OK, the program is running normally, no issues!

Debugging

Debugging is also quite troublesome. I found that my OpenOCD would not run for a long time.

Info : esp_usb_jtag: VID set to 0x303a and PID to 0x1001
Info : esp_usb_jtag: capabilities descriptor set to 0x2000

Info : Listening on port 6666 for tcl connections

Info : Listening on port 4444 for telnet connections

❌ Error: libusb_open() failed with LIBUSB_ERROR_ACCESS

❌ Error: esp_usb_jtag: could not find or open device! 
/home/charliechen/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg:9: Error: 
Traceback (most recent call last):
  File "/home/charliechen/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg", line 9, in script

For assistance with OpenOCD errors, please refer to our Troubleshooting FAQ: https://github.com/espressif/openocd-esp32/wiki/Troubleshooting-FAQ
OpenOCD Exit with non-zero error code 1
[Stopped]

This indicates that there are still some permission issues with OpenOCD. At this point, we check:

ls -l /dev/bus/usb/001/002

crw-rw-r-- 1 root root 189, 1 Nov 24 12:08 /dev/bus/usb/001/002

ls /dev/ttyACM*
/dev/ttyACM0

The current USB device belongs to root:root, which is problematic. No wonder OpenOCD is failing. We need to inform the USB subsystem of the correct permissions.

sudo nano /etc/udev/rules.d/99-espressif.rules

Write the following content:

Note,<span>ATTR{idVendor}=="303a", ATTR{idProduct}=="1001"</span> should not be altered; you need to check the hints from OpenOCD here:<span>esp_usb_jtag: VID set to 0x303a and PID to 0x1001</span>

# Espressif USB JTAG/Serial
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", ATTR{idProduct}=="1001", MODE="0666", GROUP="wheel", TAG+="uaccess"

Restart, and remember to unplug and reinsert the USB device.

sudo udevadm control --reload-rules
sudo udevadm trigger

Note that you also need to reattach the USB in WSL for the device to appear correctly.

Test again, and now debugging should work!

Leave a Comment