Everything below was verified on real hardware (esp32 core 3.3.10, 2026-07). Follow it literally and you will not need to troubleshoot.
Paths below are relative to this skill's folder. <PORT> is the board's COM
port and <SKETCH_DIR> is a sketch folder in the user's workspace.
One-time setup (skip any step that is already true)
arduino-cli version # confirm arduino-cli exists
arduino-cli core list # is esp32:esp32 3.x installed?
# if not (this downloads ~1 GB and can take several minutes — use a long timeout):
arduino-cli core update-index
arduino-cli core install esp32:esp32
Finding the board
arduino-cli board list
The XIAO usually shows as an unidentified "Serial Port (USB)" — that row's
port (e.g. COM4) is <PORT>. If NO port appears: the board may be in deep
sleep (port only exists while awake — see pitfall 2), or needs bootloader
mode (hold the BOOT button while plugging in USB; port number may change).
Standard workflow
# sketch layout rule: folder name and .ino name must match: my_sketch\my_sketch.ino
# compile (first compile of a session takes 1-3 min; cached after)
arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32S3 <SKETCH_DIR>
# upload
arduino-cli upload -p <PORT> --fqbn esp32:esp32:XIAO_ESP32S3 <SKETCH_DIR>
# read serial for 10 s (NEVER `arduino-cli monitor` — it blocks forever)
powershell -File scripts\read_serial.ps1 -Port <PORT> -Seconds 10
# optional: send a line to the board and read the response
powershell -File scripts\read_serial.ps1 -Port <PORT> -Seconds 10 -Send "hello"
PowerShell 5.1 has no &&. Chain with ; or if ($?) { ... }.
Verified example sketches
assets/sketches/ contains 11 sketches that all compiled, uploaded, and ran
on real hardware. Copy the closest one into the user's workspace as a
starting point instead of writing from scratch. See references/examples.md
for what each proves and its expected output.
Sketch rules that prevent bugs
Serial.begin(115200);thendelay(3000);before the first print — USB CDC needs time to enumerate, otherwise early output is silently lost.- The user LED is
LED_BUILTIN(GPIO21) and is inverted:LOW= ON,HIGH= OFF.analogWrite(LED_BUILTIN, 0)= full brightness. - ADC: call
analogReadResolution(12); read raw withanalogRead(A0)and millivolts withanalogReadMilliVolts(A0). A floating pin reads noisy 60–160 raw — that is normal, not a bug. - Touch:
touchRead(T2)(T2 = GPIO2 = pin D1). Baseline ≈ 17,500; the value RISES well above 40,000 when touched (opposite of original ESP32). WiFi.macAddress()returns00:00:00:00:00:00until WiFi is started withWiFi.mode(WIFI_STA)— not an error.- Networking in core 3.x: use
NetworkClient/NetworkServer(oldWiFiClient/WiFiServernames may not exist).
Pin map (XIAO ESP32S3)
| Label | GPIO | Notes |
|---|---|---|
| D0 | 1 | A0, touch T1 |
| D1 | 2 | A1, touch T2 |
| D2 | 3 | A2, touch T3 |
| D3 | 4 | A3, touch T4 |
| D4 | 5 | SDA, touch T5 |
| D5 | 6 | SCL, touch T6 |
| D6 | 43 | UART TX (Serial1) |
| D7 | 44 | UART RX (Serial1) |
| D8 | 7 | SPI SCK |
| D9 | 8 | SPI MISO |
| D10 | 9 | SPI MOSI |
| user LED | 21 | inverted (LOW = on) |
Known pitfalls — read before "debugging"
- Opening the COM port resets the board (DTR toggle →
rst:0x15 USB_UART_CHIP_RESET). Every serial read starts from a fresh boot. This also wipesRTC_DATA_ATTRvariables, so you cannot observe deep-sleep boot counts through a serial monitor. It is not a crash. - Deep sleep makes the COM port disappear. USB CDC powers down during
sleep, so the port vanishes and Windows plays disconnect/connect sounds
every cycle. This is normal and is the correct evidence that sleep works.
Verify the cycle with
scripts\watch_port.ps1 -Port <PORT>(logs appear/disappear timestamps) instead of opening the port. To reflash a sleeping board, catch the short awake window, or hold the BOOT button while plugging USB (bootloader mode; the port number may change). arduino-cli monitornever exits. In an automated session it hangs your shell. Always usescripts\read_serial.ps1, which has a deadline.- PSRAM reads 0 bytes by default. The default build has PSRAM disabled.
If a sketch needs PSRAM (camera, big buffers), add
--board-options PSRAM=opito BOTH compile and upload commands. netsh wlan show networksis cached and may not show a SoftAP the board just created. Trust the board's serial output ("AP IP address: 192.168.4.1"); test the AP from a phone instead.- Upload fails / port busy: make sure no serial reader is still running (only one process can hold the port). If the board looks bricked, use bootloader mode (pitfall 2).
- First compile is slow (cold cache, big toolchain). Do not kill it before ~3 minutes; use a generous timeout.
- Board can get stuck in reset after a serial session (only the ROM
banner
ESP-ROM:esp32s3-...prints, app never boots, SoftAP/servers die). Cause: DTR/RTS line state left by the PC when closing the port. Recover witharduino-cli upload(esptool's reset sequence fixes it). If a headless demo (web server etc.) must keep running, avoid opening the serial port at all — use mDNS or the router's DHCP table to find the board instead of reading its IP over serial. - Same-name Edge Impulse library swap needs
--clean. Replacing the<project>_inferencinglibrary with a rebuilt copy (same version) leaves stale objects in arduino-cli's cache — symptoms:objs.a ... is not an objectlink errors or edits that appear to have no effect. Always pass--cleanon the first compile after swapping the library.
Verifying without human eyes
- LED/PWM: also print state to serial each cycle so the log proves behavior.
- WiFi scan / BLE scan: success = a numbered list of networks/devices.
- SoftAP: success = serial prints the AP IP; full HTTP test needs a phone.
- Deep sleep: success =
watch_port.ps1shows a stable disappear/appear cycle matching the programmed sleep time (+ ~3 s awake).