2026-02-06
md
First Look at the Seeed Studio XIAO ESP32C5
<-First Look at the Seeed Studio XIAO ESP32C6
<-First Look at the Seeed Studio XIAO ESP32C3

photo XIAO-ESP32-C5There's a new member of the XIAO family: the XIAO ESP32C5 which is somewhat disconcerting because the XIAO ESP32C6 appeared back in November 2024. Don't blame Seeed Studio for the awkward sequence. It was Espressif that released the ESP32-C5 at large in May 2025 almost two years after the ESP32-C6 became officially available in January 2023. How does Seeed Studio or any other development board maker, keep up with the ever-increasing number of systems on a chip (SoCs)? The ESP32C5 is the 15th board in the XIAO series and the third based on an Espressif RISC-V SoC.

This post aims to review the newest member of the XIAO family, in much the same way as older members of the XIAO boards were looked at on this site. As before, this is an idiosyncratic exploration of a new development board. Basically, I am testing the capabilities of the board by running modified example sketches that sometimes investigate matters at a slightly less superficial level than needed for an example. The conceit here is that most everything of interest to the hobbyist can be verified to work with a minimum of gear: a LED and bits of wire and a couple of alligator clips are sufficient for most of the sketches presented below. The more complex tests used a second microcontroller and a logic analyzer. The latter, which cost less than a sad meal at a local burger joint, was a no name 24MHz, 8 channel, USB device readily found in far away places.

Table of Content

  1. Disclosure
  2. Documentation
  3. Almost a Spitting Image of Its Siblings
  4. Pins and Pads
  5. Ultra Low Power Core
  6. Antenna, Buttons, and Bootloader
  7. Where is the Board?
  8. Programming the XIAO
  9. Source Code
  10. Explorations
    1. Using esptool
    2. Do Nothing Sketch
    3. Information Sketches
  11. Presentation of a Typical Sketch
  12. Digital Output and Input
    1. Digital Output: blink
    2. Digital Output: Pulse Width Modulation
    3. Digital Input: Polling
    4. Digital Input: Interrupts
  13. Analogue Input
  14. Deep Sleep
    1. Timer Wake-up
    2. GPIO Wake-up
  15. Serial Communication
    1. UART
    2. SPI
    3. I2C
  16. Wi-Fi Communication
    1. Wi-Fi Scan
    2. Wi-Fi Connect
    3. Wi-Fi Throughput
  17. Zigbee
  18. Bluetooth LE
    1. Scanning for Bluetooth LE Devices
    2. Implementing a Bluetooth Server
    3. Adding a Bluetooth Client
  19. Thread and Matter
  20. Conclusions

Disclosure toc

In late 2025, Seeed Studio informed me of the upcoming launch of the XIAO ESP32C5 and invited me to join an early adopter program to help in testing the new device. Flattered, I eagerly accepted although some of the proposed tests were clearly beyond my limited capabilities. I was not asked to write anything about the new XIAO. So as before, there was no pressure or expectations on the part of Seeed Studio. What I have written below is entirely my work with no direct input from Seeed Studio. Consequently, any and all mistakes are mine.

As for testing the new XIAO, all that can be said is that everything I have tried up to now works very well.

Documentation toc

The new board is being launched with a Getting Started with Seeed Studio XIAO ESP32-C5 Wiki (the Getting Started Guide for short) that is chock full of information and example code. Given the initial paucity of information with some of the XIAOs, this is a very welcome improvement. I said that Seeed had no direct impact on my review, but the documentation did. Where possible, I will not reproduce sketches that are found in the Getting Started Guide. Consequently, every user of the XIAO ESP32C5 should read the guide carefully and work through the examples. In all modesty this review can only be a small contribution given what Seeed Studio has already done. Hopefully, that did not sound pompous or obsequious. Now is a good time to state that I do not claim any expertise about the subject matter covered in this post, or any other post on this site for that matter.

Understandably, there is not much documentation on the XIAO ESP32C5 specifically. Luckily, the DroneBotWorkshop already contains a very informative article entitled Seeeduino XIAO ESP32-C5 as well as an accompanying video. The link is in the article.

There are other ESP32-C5 based boards, of course. Sites such as Github do contain some repositories related to that SoC. A search on Github returned 23 hits including the repository associated with this post. More on that later.

Almost a Spitting Image of Its Siblings toc

The XIAO ESP32C5 by Seeed Studio is the latest addition to the Seeed Studio XIAO Series of diminutive development boards. Diminutive is redundant, XIAO means small and these boards are tiny as can be seen on the image to the right. Seeed Studio says 21 x 17.8 mm; let's not quibble about 1 mm differences. Here's another measurement I have never given before: the printed circuit board is 1.2mm thick according to my plastic digital caliper. The thickness of some disappointing SuperMini boards was only 0.6mm. Also, the corners of those boards were not rounded. These two facts explain why a couple had damaged crenelations. The XIAOs are mechanically solid devices, except for the buttons on some models. That little problem has been corrected on the XIAO ESP32C5 as discussed later.

All boards in the XIAO family share the same form factor and the same pin layout for the I2C, SPI and UART serial peripherals. That consistency makes the whole family of boards a valuable resource for those interested in sampling different SoC with a minimum of fuss.

The SoC and other components such as a crystal oscillator and extra storage are under a metal can which is itself covered by an identifying label. This shielding limits RF interference meaning that the XIAO development boards are FCC certified. That is often not the case for development boards from other sources. Consequently, the boards are not just for hobbyists. The castellated pads along the edges of the board and the absence of components on the reverse side means that they can be easily soldered flat onto another circuit board. Of course, 0.1" headers can be soldered into the through holes of the XIAO allowing it to be mounted on a socket or soldered in matching through holes on a bigger circuit board as is more common. In other words, these boards could easily be integrated into commercial products.

The component side also has various buttons, LEDs, and in some models, sensors, antenna or antenna connector. There are solder pads on the reverse side of each model which provides connections to a debug port and in many models to a battery. Given that the debug interface can be quite different from one SoC to another, there is no consistent layout for the pads on the reverse side of the XIAO boards. With that said, a single pattern has been used for the reverse side pads on latter XIAOs and all the Risc V based boards share that common 8-pad layout for the JTAG debug interface. The two pads to connect to a battery are not compatible unfortunately.

As the name implies the new XIAO is based on the ESP32-C5 microcontroller from Espressif which makes it the third board of the series based on a RISC-V microprocessor. The table below compares the three offerings.

XIAO ESP32C5XIAO ESP32C6XIAO ESP32C3
Espressif SoCESP32-C5HR8ESP32-C6FH4ESP32-C3FN4
High Performance CoreRISC-V 32-bit core @240 MHzRISC-V 32-bit core @160 MHzRISC-V 32-bit core @160 MHz
Low Power CoreRISC-V 32-bit core @20MhzRISC-V 32-bit core @20Mhz
WiFi2.4 GHz & 5 GHz dual-band Wi-Fi 62.4 GHz Wi-Fi 6 2.4 GHz Wi-Fi
BluetoothBluetooth 5 (LE)
Memory384 KB SRAM, 8 MB Flash, 8 MB PSRAM512KB SRAM, 4 MB Flash400KB SRAM, 4MB Flash
InterfacesI2C / UART / SPI and JTAG on back side pads
Digital and PWM Pins11
Analog (ADC) Pins1 + 4 on back side pads73
Onboard ButtonsReset and boot
Onboard LEDsCharge and userCharge
Battery Charge ChipSGM40567ETA4054S2F

As stated in the above table the XIAO ESP32C5 is built around the Espressif ESP32-C5HR8. Here is a link to the SoC's datasheet.

Pins and Pads toc

Below is my simplified rendering for the specific edge pin layout and reverse side pad layout of the XIAO ESP32C5 insofar as the high performance core is concerned.

XIAO ESP32C5 pinout

< bigger image with a legend >
Notes:

Ultra Low Power Core toc

Like the XIAO ESP32C6, the low power Risc-V core has access to only some of the I/O pins found on the development board. On the XIAO ESP32C6, this means that 6 of the 7 ULP I/O pins are available, but only two are on an edge pad. The others represent alternate uses of some of the reverse side pads principally used for JTAG debugging.

I/O pinULP pinULP peripheralArduino
0 LP_GPIO0LP_UART_DTRND1
1 LP_GPIO1LP_UART_DSRND0
2 LP_GPIO2LP_I2C_SDAA1
3 LP_GPIO3LP_I2C_SCLA2
4 LP_GPIO4LP_UART_RXDA3
5 LP_GPIO5LP_UART_TXDA4

These symbolic names for the LP pins and peripherals were taken from the XIAO ESP32C6 Pin Definition diagram in the Getting Started Guide. As far as I can make out, these are not defined in any files in the ESP32 Arduino core. There is an interesting discussion on Building low power applications on Espressif chips: Ultra-Low-Power (ULP) coprocessor by Marius Vikhammer that is a few months old. The LP-core found on the ESP32-C5, ESP32-C6 and ESP32-P4, which is the third and latest iteration of low power cores, has powerful capabilities.

However, support for the Low Power core of the ESP32-C6 (and presumably ESP32-C5) remains on the Feature request todo list of the Arduino ESP32 Core Project since being placed there in May of 2024. Hopefully, this will eventually be done, but until then the ESP-IDF must be used to program the low power core.

Antenna, Buttons, and Bootloader toc

The XIAO comes with an elegant mat black antenna almost twice its size but much thinner. It's the 2.4 GHz FPC Antenna (2.9 dBi), the same that comes with the XIAO ESP32C3. The U.FL coaxial connector is very small, a bit fiddly and will probably not survive very many connections (the female connector on the antenna is rated for only 30 reconnections, see Hirose U.FL), so it's best to avoid disconnecting it. The FPC antenna is probably not optimal for the 5 GHz band. Seeed Studio offers a 2.4G/5G External [rod] Antenna with RP-SMA Male Connector [which] delivers much higher gain than the built-in FPC Antenna included in the package!. It is available here.

There are two small push buttons at the bottom of the board on either side of the U.FL antenna connector, opposite the USB-C connector. The button on the left resets the controller, while the boot mode button on the right can be used to put the system in bootloader mode and is available as a run time user input. That's the opposite placement of those buttons on the XIAO ESP32C3. I am very glad to see that Seeed Studio managed to return to solid 2x3 mm metal tact push buttons instead of the dome buttons with a thin plastic cover on the XIAO ESP32C6 and XIAO ESP32S3. I have managed to destroy a few of those rather delicate buttons.

It is not usually required, but it may be necessary to restart the board in bootloader mode. The sequence of button presses and releases to get into bootloader mode is pretty common.

  1. Press and keep depressed the reset button.
  2. Press and keep depressed the boot button.
  3. Release the reset button.
  4. Release the boot button.

Another way of putting the board in bootloader mode.

  1. Remove power to the board, usually by disconnecting the USB cable.
  2. Press and keep depressed the boot button.
  3. Apply power to the board, usually by connecting the USB cable.
  4. Release the boot button.

Be deliberate about these steps, there is no rush. Once the sequence is completed, the board will be in bootloader mode waiting for new firmware. Uploading should now work. Remember that after the upload is finished, the board remains in bootloader mode and the reset button must be pressed and released to return the board to normal mode where it will execute the newly uploaded firmware.

In all probability, it will be necessary to explicitly set the board in bootloader mode if the SoC is in a sleep mode that turns off the serial port. This is what happens in the deep sleep sketches presented below. While experimenting, I have at times put a XIAO in a state where it is rebooting almost immediately on booting. There again it will be necessary to reset the board into bootloader mode to upload a new firmware. However, most times when there's a problem in uploading the firmware to the XIAO, it's because I am trying to upload a sketch from an IDE while another application (another copy of the IDE, or a communication program) is connected to the board. With a bit of discipline ensuring that only one application is connected to the board up at a time, it's almost certain that an upload will go through without a problem.

Where is the Board? toc

Enough with the introduction, it's time to play with the board. When a XIAO is connected to a desktop, or portable, computer with a USB cable it will show up as terminal (TTYxxxx) device if the desktop is running Linux. In Windows, one should hear the usual beep-boop sound when the USB cable is connected and the board will be reachable through a COMxxx port. Here is how to find the particular device in Linux. Start printing the kernel ring buffer with the dmesg -w command where the -w (or --follow) flag means the utility will stay active and print messages as the kernel publishes them. Wait until dmesg has finished printing all the old kernel messages and then plug in the USB cable from the XIAO to a USB port on the desktop.

michel@trig:~$ dmesg -w ... plug in USB C cable [104299.614311] usb 1-1.1: new full-speed USB device number 33 using xhci_hcd [104299.777990] usb 1-1.1: New USB device found, idVendor=303a, idProduct=1001, bcdDevice= 1.02 [104299.778006] usb 1-1.1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [104299.778014] usb 1-1.1: Product: USB JTAG/serial debug unit [104299.778020] usb 1-1.1: Manufacturer: Espressif [104299.778052] usb 1-1.1: SerialNumber: D0:C0:B0:03:02:01 --- ficticious [104299.831308] cdc_acm 1-1.1:1.0: ttyACM0: USB ACM Device

As usual the CtrlC keyboard combination halts execution of the process. Listing all USB devices once the XIAO is connected is another way of finding the board. That's useful for the very first time a development board is connected to the desktop. Once one know that its USB device time, then it is fairly easy to identify the board from then on.

michel@trig:~$ ls /dev/ttyACM* /dev/ttyACM0

Once the device is know, it becomes possible to connect to it with a serial communication program. I now use picocom for this purpose.

michel@trig:~$ picocom -q --imap lfcrlf /dev/ttyACM0

No baud is specified and it would not matter if it were 115200, 9600 or 500000 or probably anything else when connecting to the board. Just like the XIAO SAMD21, ESP32C3 and ESP32C6, the USB data lines are connected directly to the microcontroller which has a built-in USB serial controller with a CDC-ACM virtual serial port. It is USB 2.0 compliant and capable of 12 Mbits/s transfer speeds (Source ESP32C5 Series Datasheet, Version 1.0 pp 23).

Programming the XIAO toc

All the sketches presented below were initialy developed in the Arduino IDE using the Espressif ESP32 Arduino core version 3.3.5 (arduino-esp32 on GitHub). Using version 3.3.6 is recommended to avoid a few error messages.

This post is much too long to include instructions on how to get the free IDE and how to install the ESP32 Arduino core. If this is all new, here are two references that can help going about doing this.

There are so many more articles and numerous videos on the subject. Just make sure that anything consulted is recent enough. Avoid anything that is about the Arduino IDE previous to version 2. The current version of the IDE is 2.3.7 which is what I used.

The Arduino IDE is a simple yet effective stand-alone application which is highly recommended especially for beginners. However some prefer to add the PlaformIO IDE extension in a code editor. The most often used code editor is Visual Studio Code (VSCode). Once VSCode is installed, the extension must be installed. Thankfully, it is in the VSCode marketplace, so that will be a simple operation. Follow the instructions in the Platfom IO with Seeed Studio XIAO ESP32-C5 Wiki.

Each PIO project must contain a configuration file called platformio.ini in its root directory. Here is a simplified version.

[env:seeed_xiao_esp32_c5] platform = https://github.com/Seeed-Studio/platform-seeedboards.git board = seeed-xiao-esp32-c5 framework = arduino

Note: In a quick test, setting platform = Seeed Studio as suggested in the Wiki and in the Seeed repository did not work for me, the explicit URL shown above did.

The platform used, platform-seeedboards, is from Seeed Studio. It is actively maintained and supports the XIAO boards and other development boards from Seeed. The platform-espressif32 platform may be another possibility, but PlatformIO has been slow or reluctant in the past to support the latest esp32 Arduino core. I would not currently recommend using that platform, but it has not been tested in quite a few months. Currently the platform does not contain a board definition for the XIAO ESP32C5.

Unfortunately, the PlatformIO IDE extension can no longer be easily installed in VSCodium which is a fork of VSCode without the telemetry and AI additions. On the other hand, the pioarduino IDE extension is available in the VSCodium marketplace and is installed in the same way as the PlatformIO IDE extension is installed in VSCode. One would have hoped that the above configuration file would then have worked, but that was not the case. The platform-seeedboards could not be installed without manually adding some missing requirements in the Python virtual environment. Instead I recommend using the pioarduino-espressif32 platform. So the platformio.ini file must be modified.

[env:seeed_xiao_esp32c5] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = seeed_xiao_esp32c5 framework = arduino

That almosts works. There's a pattern here, but thankfully that is the last hurdle. The platform-espressif32 from pioarduino does not yet contain a board definition for the XIAO ESP32C5. To get around that problem, a local copy of the board definition from the Seeed Studio repository is used. That required some modifications to the configuration file.

As currently configured, all the projects in the companion GitHub repository can be compiled successfully with version 55.03.36 of the pioarduino-espressif32 platform released on January 21, 2026. All the messy details about the location of the board definition, the platform to use, the local library and so on are confined to the platformion.ini file available in the repository.

Source Code toc

The sketches presented in this post are available in a GitHub repository: XIAO ESP32C5 Sketches. The repository can be easily obtained free of charge.

sigmdel/xiao_esp32c5_sketches on github

The complete repository can be cloned if git is installed on the desktop machine. Click on the green Code button and then click on the copy icon to get the URL of the git repository.

The complete repository can be downloaded as a Zip archive. Click on the green Code button and then on the Download ZIP link.

Individual files can be downloaded also.

sigmdel/xiao_esp32c5_sketches on github

Navigate to the file, open it in the web interface and then click on the Download raw file icon.

The file structure is a bit more complicated than need be if one uses the Arduino IDE exclusively. Each project or sketch (PIO or Arduino terminology respectively) is stored in a directory structure and adheres to file and directory names conventions so that they may be compiled in either the Arduino IDE or the PlatformIDE.

Explorations toc

Let's see what we can find out about the XIAO ESP32C5 using the Espressif esptool utility and then from the ESP32 Arduino Core itself.

Using esptool toc

A Python script from Espressif called esptool does the heavy lifting when it comes to uploading firmware on the Espressif SoCs. It can be instructive to use it explicitly to get some information about the XIAO. If this is a first use of this tool, look at the help screen to get an idea of its capabilities. Of course, it will be necessary to adjust the path to the script.

michel@trig:~$ /home/michel/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool --help Usage: esptool [OPTIONS] COMMAND [ARGS]... esptool v5.1.0 - serial utility for flashing, provisioning, and interacting with Espressif SoCs. ╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ --chip -c [auto|esp8266|esp32|esp32s2|esp32s3|esp32 Target chip type. │ │ c3|esp32c2|esp32c6|esp32c61|esp32c5|esp32 │ │ h2|esp32h21|esp32p4|esp32h4] │ │ --port -p PATH Serial port device. │ │ --baud -b INTEGER Serial port baud rate used when │ │ flashing/reading. │ │ --port-filter [TEXT] Serial port device filter, can be │ │ vid=NUMBER, pid=NUMBER, name=SUBSTRING, │ │ serial=SUBSTRING. │ │ --before [default-reset|usb-reset|no-reset|no-rese Which reset to perform before connecting │ │ t-no-sync] to the chip. │ │ --after -a [hard-reset|soft-reset|no-reset|no-reset- Which reset to perform after operation is │ │ stub|watchdog-reset] finished. │ │ --no-stub Disable launching the flasher stub, only │ │ talk to ROM bootloader. Some features will │ │ not be available. │ │ --trace -t Enable trace-level output of esptool │ │ interactions. │ │ --verbose -v Print all output, disable collapsing │ │ output stages. │ │ --silent -s Silence all output except for errors. │ │ --override-vddsdio [1.8V|1.9V|OFF] Override ESP32 VDDSDIO internal voltage │ │ regulator (use with care). │ │ --connect-attempts INTEGER Number of attempts to connect, negative or │ │ 0 for infinite. Default: 7. │ │ --help -h Show this message and exit. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Basic commands ──────────────────────────────────────────────────────────────────────────────────────────────╮ │ write-flash Write a binary blob to flash. The address is followed by binary filename, │ │ separated by space. │ │ read-flash Read SPI flash memory content. │ │ erase-flash Erase the SPI flash memory. │ │ erase-region Erase a region of the SPI flash memory. │ │ read-mac Print the device MAC address. │ │ flash-id Print the SPI flash memory manufacturer and device ID. │ │ elf2image Create an application image from ELF file │ │ image-info Print information about a firmware image (bootloader or application). │ │ merge-bin Merge multiple raw binary files into a single flashable file. │ │ version Print esptool version. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Advanced commands ───────────────────────────────────────────────────────────────────────────────────────────╮ │ verify-flash Verify a binary blob against the flash memory content. │ │ load-ram Download an image to RAM and execute. │ │ dump-mem Dump arbitrary memory to a file. │ │ read-mem Read arbitrary memory location. │ │ write-mem Modify or write to arbitrary memory location. │ │ read-flash-status Read SPI flash memory status register. │ │ write-flash-status Write SPI flash memory status register. │ │ read-flash-sfdp Read SPI flash SFDP (Serial Flash Discoverable Parameters). │ │ get-security-info Print security information report. │ │ chip-id Print the device chip ID. │ │ run Run application code loaded in flash. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

If like me, the reader finds this is mostly unintelligible, do not worry because the Arduino IDE invokes the script setting all those flags as needed. But here is how to get some information about the SoC with the tool.

michel@trig:~$ /home/michel/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool --chip esp32c5 --port /dev/ttyACM0 flash-id esptool v5.1.0 Connected to ESP32-C5 on /dev/ttyACM0: Chip type: ESP32-C5 (revision v1.0) Features: Wi-Fi 6 (dual-band), BT 5 (LE), IEEE802.15.4, Single Core + LP Core, 240MHz Crystal frequency: 48MHz MAC: D0:C0:B0:ff:fe:03:02:01 BASE MAC: D0:C0:B0:03:02:01 MAC_EXT: ff:fe Stub flasher running. Flash Memory Information: ========================= Manufacturer: 85 Device: 2017 Detected flash size: 8MB Hard resetting via RTS pin...

We already knew that the ESP32-C5 can use the 5 GHz Wi-Fi band in addition to the 2.4 GHz Wi-Fi band that almost all ESP modules can use. IEEE 802.15.4 is a technical standard that defines low-rate wireless personal area networking (LR-WPAN). It is the basis for the Zigbee and Thread wireless protocols often used in the home automation sector.

I am not too sure what to make with the definitions of the MAC, BASE_MAC and MAC_EXT. This is my first encounter with an 8 octet MAC address. Something that I may have to look into, although I'll present a sketch about the base MAC address later.

Do Nothing Sketch toc

We can get what seems to be a lot more information by uploading a sketch that does nothing.

do nothing sketch in IDE

Before compiling and uploading this sketch, turn debugging on by setting Core Debug Level to Verbose in the Tools menu (screen capture).

=========== Before Setup Start =========== Chip Info: ------------------------------------------ Model : ESP32-C5 Package : 0 Revision : 1.00 Cores : 1 CPU Frequency : 240 MHz XTAL Frequency : 48 MHz Features Bitfield : 0x00000052 Embedded Flash : No Embedded PSRAM : No 2.4GHz WiFi : Yes Classic BT : No BT Low Energy : Yes IEEE 802.15.4 : Yes ------------------------------------------ INTERNAL Memory Info: ------------------------------------------ Total Size : 338096 B ( 330.2 KB) Free Bytes : 309084 B ( 301.8 KB) Allocated Bytes : 25204 B ( 24.6 KB) Minimum Free Bytes: 304072 B ( 296.9 KB) Largest Free Block: 286708 B ( 280.0 KB) ------------------------------------------ Flash Info: ------------------------------------------ Chip Size : 8388608 B (8 MB) Block Size : 65536 B ( 64.0 KB) Sector Size : 4096 B ( 4.0 KB) Page Size : 256 B ( 0.2 KB) Bus Speed : 80 MHz Flash Frequency : 80 MHz (source: 80 MHz, divider: 1) Bus Mode : QIO ------------------------------------------ Partitions Info: ------------------------------------------ nvs : addr: 0x00009000, size: 20.0 KB, type: DATA, subtype: NVS otadata : addr: 0x0000E000, size: 8.0 KB, type: DATA, subtype: OTA app0 : addr: 0x00010000, size: 3264.0 KB, type: APP, subtype: OTA_0 app1 : addr: 0x00340000, size: 3264.0 KB, type: APP, subtype: OTA_1 spiffs : addr: 0x00670000, size: 1536.0 KB, type: DATA, subtype: SPIFFS coredump : addr: 0x007F0000, size: 64.0 KB, type: DATA, subtype: COREDUMP ------------------------------------------ Software Info: ------------------------------------------ Compile Date/Time : Jan 6 2026 22:02:13 Compile Host OS : linux ESP-IDF Version : v5.5.1-931-g9bb7aa84fe Arduino Version : 3.3.5 ------------------------------------------ Board Info: ------------------------------------------ Arduino Board : XIAO_ESP32C5 Arduino Variant : XIAO_ESP32C5 Arduino FQBN : esp32:esp32:XIAO_ESP32C5:UploadSpeed=921600,CDCOnBoot=cdc,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=8M,PartitionScheme=default_8MB,DebugLevel=verbose,PSRAM=disabled,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default ============ Before Setup End ============ [ 656][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RX (2) cleared [ 656][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_TX (3) cleared [ 657][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_CTS (4) cleared [ 657][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RTS (5) cleared [ 657][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 12 successfully set to type UART_RX (2) with bus 0x4080cd04 [ 658][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_RX (2) successfully set to 0x420032ee [ 658][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 11 successfully set to type UART_TX (3) with bus 0x4080cd04 [ 659][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_TX (3) successfully set to 0x420031b4 =========== After Setup Start ============ INTERNAL Memory Info: ------------------------------------------ Total Size : 338096 B ( 330.2 KB) Free Bytes : 308996 B ( 301.8 KB) Allocated Bytes : 25260 B ( 24.7 KB) Minimum Free Bytes: 303836 B ( 296.7 KB) Largest Free Block: 286708 B ( 280.0 KB) ------------------------------------------ GPIO Info: ------------------------------------------ GPIO : BUS_TYPE[bus/unit][chan] -------------------------------------- 11 : UART_TX[0] 12 : UART_RX[0] 13 : USB_DM 14 : USB_DP ============ After Setup End =============

Surprisingly there's no mention of the 5 GHz Wi-Fi capability of the ESP32-C5. As can be seen in the listing, the PSRAM is not enabled. Let's enable PSRAM in the Tools menu (screen capture) and, compile and upload the empty sketch and run it again.

[ 1][I][esp32-hal-psram.c:106] psramAddToHeap(): PSRAM added to the heap. [ 12][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type USB_DM (38) successfully set to 0x42004250 [ 23][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 13 successfully set to type USB_DM (38) with bus 0x4080f798 [ 34][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type USB_DP (39) successfully set to 0x42004250 [ 45][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 14 successfully set to type USB_DP (39) with bus 0x4080f798 =========== Before Setup Start =========== Chip Info: ------------------------------------------ Model : ESP32-C5 Package : 0 Revision : 1.00 Cores : 1 CPU Frequency : 240 MHz XTAL Frequency : 48 MHz Features Bitfield : 0x00000052 Embedded Flash : No Embedded PSRAM : No 2.4GHz WiFi : Yes Classic BT : No BT Low Energy : Yes IEEE 802.15.4 : Yes ------------------------------------------ INTERNAL Memory Info: ------------------------------------------ Total Size : 335744 B ( 327.9 KB) Free Bytes : 306680 B ( 299.5 KB) Allocated Bytes : 25240 B ( 24.6 KB) Minimum Free Bytes: 301668 B ( 294.6 KB) Largest Free Block: 278516 B ( 272.0 KB) ------------------------------------------ SPIRAM Memory Info: ------------------------------------------ Total Size : 8388608 B (8192.0 KB) Free Bytes : 8386296 B (8189.7 KB) Allocated Bytes : 0 B ( 0.0 KB) Minimum Free Bytes: 8386296 B (8189.7 KB) Largest Free Block: 8257524 B (8064.0 KB) Bus Mode : QSPI ------------------------------------------ Flash Info: ------------------------------------------ Chip Size : 8388608 B (8 MB) Block Size : 65536 B ( 64.0 KB) Sector Size : 4096 B ( 4.0 KB) Page Size : 256 B ( 0.2 KB) Bus Speed : 80 MHz Flash Frequency : 80 MHz (source: 80 MHz, divider: 1) Bus Mode : QIO ------------------------------------------ Partitions Info: ------------------------------------------ nvs : addr: 0x00009000, size: 20.0 KB, type: DATA, subtype: NVS otadata : addr: 0x0000E000, size: 8.0 KB, type: DATA, subtype: OTA app0 : addr: 0x00010000, size: 3264.0 KB, type: APP, subtype: OTA_0 app1 : addr: 0x00340000, size: 3264.0 KB, type: APP, subtype: OTA_1 spiffs : addr: 0x00670000, size: 1536.0 KB, type: DATA, subtype: SPIFFS coredump : addr: 0x007F0000, size: 64.0 KB, type: DATA, subtype: COREDUMP ------------------------------------------ Software Info: ------------------------------------------ Compile Date/Time : Jan 6 2026 22:42:10 Compile Host OS : linux ESP-IDF Version : v5.5.1-931-g9bb7aa84fe Arduino Version : 3.3.5 ------------------------------------------ Board Info: ------------------------------------------ Arduino Board : XIAO_ESP32C5 Arduino Variant : XIAO_ESP32C5 Arduino FQBN : esp32:esp32:XIAO_ESP32C5:UploadSpeed=921600,CDCOnBoot=cdc,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=8M,PartitionScheme=default_8MB,DebugLevel=verbose,PSRAM=enabled,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default ============ Before Setup End ============ [ 174][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RX (2) cleared [ 175][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_TX (3) cleared [ 175][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_CTS (4) cleared [ 175][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RTS (5) cleared [ 176][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 12 successfully set to type UART_RX (2) with bus 0x4080d504 [ 176][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_RX (2) successfully set to 0x42003500 [ 176][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 11 successfully set to type UART_TX (3) with bus 0x4080d504 [ 177][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_TX (3) successfully set to 0x420033c6 =========== After Setup Start ============ INTERNAL Memory Info: ------------------------------------------ Total Size : 335744 B ( 327.9 KB) Free Bytes : 306592 B ( 299.4 KB) Allocated Bytes : 25296 B ( 24.7 KB) Minimum Free Bytes: 301432 B ( 294.4 KB) Largest Free Block: 278516 B ( 272.0 KB) ------------------------------------------ SPIRAM Memory Info: ------------------------------------------ Total Size : 8388608 B (8192.0 KB) Free Bytes : 8386296 B (8189.7 KB) Allocated Bytes : 0 B ( 0.0 KB) Minimum Free Bytes: 8386296 B (8189.7 KB) Largest Free Block: 8257524 B (8064.0 KB) ------------------------------------------ GPIO Info: ------------------------------------------ GPIO : BUS_TYPE[bus/unit][chan] -------------------------------------- 11 : UART_TX[0] 12 : UART_RX[0] 13 : USB_DM 14 : USB_DP ============ After Setup End =============

I could not measure power consumption when running this small sketch because it was below the threshold of my small inline USB power meter.

Information Sketches toc

Instead of the classic "Hello World!" program, I decided that the first non-empty sketches uploaded to the XIAO would print out some information about the SoC and about the XIAO board.

Output of sys_info.ino

ESP32 Chip Information Chip model: ESP32-C5, Revision: 100 Core count: 1 CPU frequency: 240 MHz Cycle count: 506424179 MAC: D0:C0:B0:fe:ff:03:02:01 SDK version: v5.5.1-931-g9bb7aa84fe Flash Memory Flash size: 8388608 Flash speed: 80000000 Flash mode: FM_QIO (0) SPI RAM Memory (PSRAM) PSRAM size: 0 Free PSRAM: 0 Min free PSRAM: 0 Max PSRAM alloc size: 0 Sketch Size: 289600 Free space: 3342336 Heap Size: 338048 Free: 309424 Minimum free since boot: 304176 Maximum allocation size: 286708

Note that the MAC address (obtained using the ESP.getEfuseMac()) is the extended 8 octet MAC address encountered before.

The second sketch in the same vein prints out the pin assignment as defined in pins_arduino.h found in the Arduino Espressif32 package in the following directory: ~/.platformio/packages/framework-arduinoespressif32/variants/XIAO_ESP32C3/ or ~/.arduino15/packages/esp32/hardware/esp32/3.0.2/variants/XIAO_ESP32C3/ if using Arduino IDE in Linux.

Output of pin_names

XIAO ESP32C5 I/O Pin Names and Numbers BOOT_PIN defined in esp32-hal.h all others in pins_arduino.h The symbolic name and corresponding I/O number of the 11 digital pins D0 = 1 D1 = 0 D2 = 25 D3 = 7 D4 = 23 D5 = 24 D6 = 11 D7 = 12 D8 = 8 D9 = 9 D10 = 10 The symbolic name and corresponding I/O number of the 5 analogue pins (4 are JTAG pads on the back side) A0 = 1 A1 = 2 [JTAG MTMS] A2 = 3 [JTAG MTDI] A3 = 4 [JTAG MTCK] A4 = 5 [JTAG MTDO] The symbolic name and corresponding I/O number of the 8 serial pins TX = 11 [UART] (=D6) RX = 12 [UART] (=D7) SDA = 23 [I2C] (=D4) SCL = 24 [I2C] (=D5) SS = 7 [SPI] (=D3) MOSI = 10 [SPI] (=D10) MISO = 9 [SPI] (=D9) SCK = 8 [SPI] (=D8) Battery BAT_VOLT_PIN = 6 BAT_VOLT_PIN_EN = 26 User yellow LED LED_BUILTIN = 27 BUILTIN_LED = 27 // backward compatibility Boot Button BOOT_PIN = 28 (defined in esp32-hal.h) Other macros USB_VID = 0x2886 USB_PID = 0x0067 Package macro ESP32 defined Board macro ARDUINO_XIAO_ESP32C5 defined Native USB support ARDUINO_USB_CDC_ON_BOOT is defined and equal to 1 Reverse listing GPIO C Names 0 e D1 1 e D0, A0 2 p A1 3 p A2 4 p A3 5 p A4 6 - BAT_VOLT_PIN 7 e D3, SS [SPI] 8 e D8, SCK [SPI] 9 e D9, MISO [SPI] 10 e D10, MOSI [SPI] 11 e D6, TX [UART] 12 e D7, RX [UART] 23 e D4, SDA [I2C] 24 e D5, SCL [I2C] 25 e D2 26 - BAT_VOLT_PIN_EN 27 - BUILTIN_LED, LED_BUILTIN 28 - BOOT_PIN C(onnection): e = edge and through-hole, p = pad on reverse side

Note that the ESP32, ARDUINO_XIAO_ESP32C3, and ARDUINO_USB_CDC_ON_BOOT are not defined in the pins_arduino.h. They are nevertheless useful when creating sketches that accommodate slight differences between different boards.

Output of macs.ino

Since almost all ESP32 boards come with a radio peripheral that can be run as a Wi-Fi station or a Wi-Fi access point or both at once. They usually have Bluetooth capabilities. All these must have a unique identifier which is called a MAC (medium access control) address in the case of Wi-Fi or a BD_ADDR in the case of Bluetooth. The macs.ino sketch will print out these addresses to the serial monitor.

Project: Print MACs Board: Seeed XIAO ESP32C5 Antenna: A-01 FPC Default MAC D0:C0:B0:FE:FF:03:02:01 Base MAC D0:C0:B0:03:02:01 Wi-Fi STA MAC D0:C0:B0:03:02:01 Wi-Fi soft AP MAC D0:C0:B0:E2:22:8D Bluetooth BD_ADDR D0:C0:B0:E2:22:8E Ethernet MAC D0:C0:B0:E2:22:8F

Recall the output from the esptool

MAC: D0:C0:B0:ff:fe:03:02:01 BASE MAC: D0:C0:B0:03:02:01 MAC_EXT: ff:fe

Long default 8-byte MACs are only found on ESP32 SoC which have Zigbee and Thread capabilities (formally the IEEE 802.15.4 standard). These default MACs are Zigbee IEEE addresses. The 8-byte default MAC is obviously the base MAC with the 2-byte MAC extension spliced into the middle.So far I have only seen ff:fe or fe:ff extensions. Make of that what you will.

Output of heap.ino

Since the XIAO has 8 Mbytes of external SPI RAM called PSRAM for pseudo-static random access memory, I wanted to know if it is significantly slower than the SRAM (static random access memory) on the SoC. The sketch provides an answer, but that turned out to be rather underwhelming compared to the complicated nature of heap memory on ESP32 microcontrollers.

heap of Turbo Pascal program in CP/M If the term heap does not mean anything, here is a quick overview which corresponds to the full extent of my knowledge on the subject. The figure on the right shows the allocation of memory in a 8080 or Z80 CP/M computer running a Turbo Pascal program. The OS owns two blocks of memory, a small workspace starting at address 0x0000 and a larger chunk at the very top of the memory (ending at 0xFFFF if the computer has a full complement of 64 Kbytes of memory). The memory between, called the TPA (transient program area) was handed over to the program. A Turbo Pascal program would put its code just above the CP/M workspace and it would store its global variables just below the OS. The dynamically allocated memory was in between. At the top growing downward is the stack where local variables are created as functions and procedures were entered and then destroyed when the same functions and procedures were exited. The heap was at the bottom of the dynamic memory area growing upward towards the stack. The heap was for blocks of memory that the running program explicitly created and destroyed. These blocks of heap memory could exist for as long as the program ran or could be destroyed at any time during the execution of the program. The programmer had to ensure that an allocated heap block was properly freed (destroyed) when no longer needed so that the heap manager could reuse the memory. The programmer also hoped very much that the heap and stack never collided or there would be a very messy situation.

What's the point of this journey down memory lane? The external PSRAM of the XIAO is made available to firmware running on the SoC as an extension of the heap. I naively imagined that this meant that the PSRAM was just tucked into the memory map in an area next to where the SRAM heap was. The fact that the malloc function would automatically select the PSRAM when allocating large blocks of memory from the heap reinforced that delusion that I was working with.

In my initial investigation, small one-byte blocks were allocated with the ps_malloc function which explicitly allocates memory from the PSRAM and with the malloc function which I assumed would use the SRAM at first. I was trying to find the start of the SRAM and PSRAM heaps and also in which direction the heap was being filled. Then I would try with very large blocks of memory that could not possibly be contained in the SRAM so see if ps_malloc and malloc would allocate the block at the same address. It all seemed to work correctly until I tried consecutive 10-byte and 100-byte blocks, which ended up being allocated in what looked to be a haphazard way. We were not in Kansas anymore Toto.

From the little reading I have since done on the subject, I can't pretend to really understand how the heap works. Here is a short quote from the Espressif documentation.

PSRAM on ESP32-C5

The ESP32-C5 contains multiple types of RAM:

...

It is also possible to connect external SPI RAM to the ESP32-C5. The external RAM is integrated into the ESP32-C5's memory map via the cache, and accessed similarly to DRAM.

All DRAM memory is single-byte accessible, thus all DRAM heaps possess the MALLOC_CAP_8BIT capability. Users can call heap_caps_get_free_size(MALLOC_CAP_8BIT) to get the free size of all DRAM heaps.

The gist seems to be that the heap is a mixture of different types of memory with different capabilities. Here is part of the output of the heap.ino with my interpretation of its meaning in bold and italic.

heap_caps_print_heap_info(MALLOC_CAP_DEFAULT) // Memory can be returned in a non-capability-specific memory allocation (e.g. malloc(), calloc()) call Heap summary for capabilities 0x00001000: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 This is the 8 MBytes PSRAM - all free and available. At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 Don't know what this is, but there's only 32 bytes free, and the largest block that could be allocated would have only 4 bytes because each allocated block contains a header for heap management purposes. At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 That is the real time clock fast memory. This is a precious resource because it will not be erased if the SoC is restarted or awaken from deep sleep. At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 This is the "normal" internal heap area Totals: free 8691704 allocated 25520 min_free 8686800 largest_free_block 8257524

The sketch gets a 1000-byte block at 0x4081507c which is from the normal internal heap area and a 1000-byte block at 0x423050908 which is from the external PSRAM. A test is done to compare the read and write times in the two blocks:

Time to test internal heap block: 29 ms Time to test psram heap block: 34 ms

That was a pretty constant difference, which amounts to about 17% slower access with the PSRAM. Here is the full ouput of the sketch.

Project: heap Purpose: Investigate PSRAM and the heap Board: Seeed Studio XIAO ESP32C5 Variant: XIAO_ESP32C5 Heap in Internal Memory Size: 334832 Free: 305768 Mininum free since boot: 300756 Maximum allocation size: 278516 Heap in Pseudo Static RAM Memory (PSRAM) size: 8388608 Free: 8386296 Minimum free since boot: 8386296 Maximum allocation size: 8257524 heap_caps_print_heap_info(MALLOC_CAP_DEFAULT) // Memory can be returned in a non-capability-specific memory allocation (e.g. malloc(), calloc()) call Heap summary for capabilities 0x00001000: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 8691704 allocated 25520 min_free 8686800 largest_free_block 8257524 heap_caps_print_heap_info(MALLOC_CAP_SPIRAM) // Memory must be in SPI RAM (PSRAM)) Heap summary for capabilities 0x00000400: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 Totals: free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 heap_caps_print_heap_info(MALLOC_CAP_INTERNAL) // Memory must be internal; specifically it should not disappear when flash/spiram cache is switched off Heap summary for capabilities 0x00000800: At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 305408 allocated 25520 min_free 300504 largest_free_block 278516 heap_caps_print_heap_info(MALLOC_CAP_8BIT) // Memory must allow for 8/16/...-bit data accesses Heap summary for capabilities 0x00000004: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 8691704 allocated 25520 min_free 8686800 largest_free_block 8257524 heap_caps_print_heap_info(MALLOC_CAP_32BIT) // Memory must allow for aligned 32-bit data accesses Heap summary for capabilities 0x00000002: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8386296 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 8691704 allocated 25520 min_free 8686800 largest_free_block 8257524 heap_caps_print_heap_info(MALLOC_CAP_DMA) // Memory must be able to accessed by DMA Heap summary for capabilities 0x00000008: At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 289492 allocated 25520 min_free 284588 largest_free_block 278516 heap_caps_print_heap_info(MALLOC_CAP_RTCRAM) // Memory must be in RTC fast memory Heap summary for capabilities 0x00008000: At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 Totals: free 15916 allocated 0 min_free 15916 largest_free_block 15348 Allocated two 1000 byte blocks heapblock at 0x4081507c in internal heap (MALLOC_CAP_INTERNAL) psramblock at 0x42050908 in PSRAM heap (MALLOC_CAP_SPIRAML) Heap in Internal Memory Size: 334832 Free: 304392 Minimum free since boot: 300504 Maximum allocation size: 278516 Heap in Pseudo Static RAM Memory (PSRAM) size: 8388608 Free: 8385280 Minimum free since boot: 8385280 Maximum allocation size: 8257524 No errors reading and writing to allocated ram at 0x4081507c No errors reading and writing to allocated ram at 0x42050908 Time to test internal heap block: 29 ms Time to test psram heap block: 34 ms Both blocks freed Heap in Internal Memory Size: 334832 Free: 305408 Minimum free since boot: 300504 Maximum allocation size: 278516 Heap in Pseudo Static RAM Memory (PSRAM) size: 8388608 Free: 8386296 Minimum free since boot: 8385280 Maximum allocation size: 8257524 heap_caps_print_heap_info(MALLOC_CAP_INTERNAL) // Memory must be internal; specifically it should not disappear when flash/spiram cache is switched off 00800: At 0x4085c5a0 len 12120 free 32 allocated 11132 min_free 32 largest_free_block 4 alloc_blocks 33 free_blocks 2 total_blocks 35 At 0x50000000 len 16344 free 15916 allocated 0 min_free 15916 largest_free_block 15348 alloc_blocks 0 free_blocks 1 total_blocks 1 At 0x408118e0 len 306368 free 289460 allocated 14388 min_free 284556 largest_free_block 278516 alloc_blocks 46 free_blocks 3 total_blocks 49 Totals: free 305408 allocated 25520 min_free 300504 largest_free_block 278516 heap_caps_print_heap_info(MALLOC_CAP_SPIRAM) // Memory must be in SPI RAM (PSRAM)) Heap summary for capabilities 0x00000400: At 0x42050000 len 8388608 free 8386296 allocated 0 min_free 8385280 largest_free_block 8257524 alloc_blocks 0 free_blocks 1 total_blocks 1 Totals: free 8386296 allocated 0 min_free 8385280 largest_free_block 8257524

Strangely, there appears to be a discrepancy between the free internal heap size as reported by ESP.getFreeHeap() and by heap_caps_print_heap_info(MALLOC_CAP_INTERNAL) when the sketch first starts. After the 1000-byte block is allocated on the internal heap and then freed, the two functions report the same free heap size.

Presentation of a Typical Sketch toc

Because the macs.ino sketch is relatively short and simple and it is in the form that most of the following sketches use, it will be presented here is some detail.

The actual macs.ino file only contains comments.

/* * macs.ino * Print out 6 byte MAC addresses * * This is a stub to satisfy the Arduino IDE, the source code is in * the main.cpp file in the same directory. * * This sketch will compile in the Arduino IDE * * 1- Add https://espressif.github.io/arduino-esp32/package_esp32_index.json * in the Additional Boards Manager URLS in the Preferences window. * 2- Install platform esp32 by Espressif version 3.0.1 or newer with the Boards Manager * 3- Select the XIAO_ESP32C5 board * * Michel Deslierres * Jan 12, 2026 * * Copyright 2026, Michel Deslierres. No rights reserved, this code is in the public domain. * In those jurisdictions where this may be a problem, the BSD Zero Clause License applies. * <https://spdx.org/licenses/0BSD.html> */ // SPDX-License-Identifier: 0BSD

In this case, it has a copyright notice under my name. As can be seen I am implicitly placing all of my code in this project into the public domain. If that is not acceptable then the 0BSD license applies. Many other sketches are variations on other open source code and where possible, that will be acknowledged in the *.ino file and licensing details will be provided.

There must be a properly named .ino file in an Arduino project. PlatformIO prefers that the code be in a file named main.cpp, which Arduino will happily compile along with all the other source files in the directory. So it makes sense to leave the usual header informational comments at the start of a traditional Arduino sketch in the .ino file and to move the code into the main.cpp file. There is boilerplate at the start of the main.cpp file in all the remaining sketches in the repository.

First there's a reminder to check the .ino file for license information and attribution for the code when it is known.

< !-- HTML generated using hilite.me -- > /* * See macs.ino for license and attribution. */

Then I like to list the dependencies near the top. The basic header file Arduino.h is usually the first included file. It must be specified in a PlatformIO project. The Arduino compiler will automatically add the file in all cases which explains why it does not appear in the typical .ino file. In this project, the header file MACs.h is also included so that the compiler will know how the functions declared in the header file can be called. The actual implementation is in the MAC.cpp file which will be compiled later. Those two files are included along with the project .ino and main.cpp files because the MAC address of a board is unique and that can be useful when making comparisons such as connect times or Wi-Fi throughput.

< !-- HTML generated using hilite.me -- > #include <Arduino.h> #include "MACs.h"

Some settings can be changed in a configuration block. If the board being used is a XIOA ESP32C6 to which an external antenna is connected the USE_EXTERNAL_ANTENNA macro must be defined. The setup() block will then switch the SoC antenna input to the external antenna.

< //////// User configuration ////// /// /// Define this when using XIAO ESP32C6 with a connected external antenna ///#define USE_EXTERNAL_ANTENNA /// /// Rate of USB to Serial chip if used on the development board. /// This is ignored when the native USB peripheral of the /// ESP SoC is used. #define SERIAL_BAUD 115200 /// //////////////////////////////////

The other setting is the SERIAL_BAUD which is the speed of the serial connection between the development board and the desktop machine. The value specified is a typical rate when the dev board has a discrete USB to serial adapter between the SoC and the USB connector. None of the XIAO boards have such an adapter, the USB connection is directly to the SoC which has a native USB peripheral as explained before. In that case, the SERIAL_BAUD is completely ignored. From there on, the source code should not need user modifications to work.

After the configuration, a couple of sanity checks are performed. Is an ESP32 based board is being used? While all the sketches are meant to be run on a XIAO ESP32C5, I find it useful to try them out with other devices for comparisons. The second check ensures that the ESP32 Arduino core installed in the IDE is recent enough.

#if !defined(ESP32) #error An ESP32 based board is required #endif #if (ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 3, 4)) #error ESP32 Arduino core version 3.3.4 or newer needed #endif #if (ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 3, 4)) #error ESP32 Arduino core version 3.3.4 or newer needed #endif

The following section defines two macros: TITLE which purports to provide a readable label identifying the development board and ANTENNA which tries to identify the type of antenna on board.

//---- Identify the ESP32 board and antenna ---- #if defined(ARDUINO_XIAO_ESP32C5) #define TITLE "Seeed XIAO ESP32C5" #define ANTENNA "A-01 FPC" #elif defined(ARDUINO_XIAO_ESP32C6) // The onboard ceramic antenna is used by default. #define TITLE "Seeed XIAO ESP32C6" #ifdef USE_EXTERNAL_ANTENNA #define ANTENNA "EXTERNAL" #else #define ANTENNA "INTERNAL CERAMIC" #endif #elif defined(ARDUINO_XIAO_ESP32C3) #define TITLE "Seeed XIAO ESP32C3" #define ANTENNA "V1.2 FPC" #elif defined(ARDUINO_XIAO_ESP32S3) #define TITLE "Seeed XIAO ESP32S3" #define ANTENNA "V1.2 FPC" #elif defined(ESP32) #define TITLE "Unknown ESP32 board" #define ANTENNA "Unknown" #else #error "An ESP32 SoC required" #endif

As can be seen, only XIAO boards are identified explicitly, any other ESP32 based board will be labelled as "Unknown ESP32 board" and its antenna will be said to be "Unknown". It is probably a PCP printed antenna but many ESP32 boards have an external antenna connector.

Let's go straight to the setup() function which in all cases starts very much as shown below.

void setup() { #if (ARDUINO_USB_CDC_ON_BOOT > 0) Serial.begin(); delay(2000); #else Serial.begin(115200); delay(1000); Serial.println(); #endif #if defined(SWITCH_TO_EXTERNAL_ANTENNA) && defined(XIAO_ESP32C6) digitalWrite(WIFI_ANT_CONFIG, HIGH); #endif Serial.println("\n\nProject: Print MACs"); Serial.printf(" Board: %s\n", TITLE); Serial.printf("Antenna: %s\n\n", ANTENNA);

That is just the first part of the setup() function. It starts with the initialization of the Serial peripheral. There are two scenarios. The first is for SoC with native USB support such as the XIAO boards. There is no need to specify a baud in the Serial.begin() function. For other types of boards, the transmission rate has to be specified. There is a delay just after the peripheral is initialized. I set the delay to 2 seconds when a native USB peripheral is used and only 1 second if a USB-serial adapter is used. When a XIAO is reset, it is disconnected from the desktop. While the Arduino IDE is very fast in reestablishing the serial connection as soon as the XIAO has finished rebooting, output to the serial monitor will be lost if there is no delay. When a USB-serial adapter chip is involved, the serial connection is not broken when the SoC is reset and hence the shorter delay. However there is usually some more or less meaningful data that is transmitted which explains the extra empty line being printed.

After that the external antenna is connected to the SoC if that is required. Then a short header is printed to the serial monitor to identify the project or sketch, the board type and the board antenna. Often the pertinent MAC address is also printed to identify the actual development board being used.

All the above is meant for sketches that could be run on other development boards. Some of the sketches presented below, the blink.ino and pwm.ino sketches for example, are specifically for the XIAO ESP32C5. A test is done to ensure that board is being used, and most of the boilerplate code shown above is not necessary. Similarly, identifying the antenna type is not useful when the sketch does not use a radio. But none of this stuff hurts except for the flash memory usage.

Digital Output and Input toc

If you are impatient to run a program that does something you can run blink sketch from the Getting Started Guide. I would suggest changing the delay after turning the built-in LED on to 100 milliseconds to visually confirm that setting the pin connected to the built-in LED low turns the latter on.

You could also run the LED toggle sketch using the built-in LED and the boot button. In that case, I suggest turning the LED off initially in the setup() routine. The logic for turning the LED on or off in the last statement also needs to be inversed.

const int buttonPin = BOOT_PIN; // Button pin const int ledPin = LED_BUILTIN; // LED pin bool ledState = false; // LED current state (OFF/ON) // Debounce const unsigned long DEBOUNCE_MS = 30; bool lastReading = HIGH; // because INPUT_PULLUP idle is HIGH bool stableState = HIGH; unsigned long lastChangeTime = 0; void setup() { pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); pinMode(buttonPin, INPUT_PULLUP); } void loop() { bool reading = digitalRead(buttonPin); // Detect a level change and start timing (for debouncing) if (reading != lastReading) { lastChangeTime = millis(); lastReading = reading; } if (millis() - lastChangeTime >= DEBOUNCE_MS) { if (stableState != reading) { stableState = reading; if (stableState == LOW) { ledState = !ledState; // toggle digitalWrite(ledPin, ledState ? LOW : HIGH); } } } }

If you want the state of the built-in LED to follow the state of the boot push button, try the following code.

const int buttonPin = BOOT_PIN; // on board boot button const int ledPin = LED_BUILTIN; // LED connected to digital pin 10 int buttonState; // variable for reading the pushbutton status void setup() { // initialize the LED pin as an output: pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); // initialize the pushbutton pin as an input: pinMode(buttonPin, INPUT_PULLUP); buttonState = digitalRead(buttonPin); } void loop() { // check for a change in the state of the button if (buttonState != digitalRead(buttonPin)) { delay(100); // a bit of a debounce // check again if (buttonState != digitalRead(buttonPin)) { // the state has changed, save the new state buttonState = digitalRead(buttonPin); // When buttonState is HIGH, the button is released, // turn the LED OFF which is done by setting its pin HIGH. Neat! digitalWrite(ledPin, buttonState); } } delay(50); }

It's short, simple and the button debouncing is elementary but it works well enough.

These sketches show how the general digital input/output pins work. Logical the pins are logically either on or off. Electrically it is more complex but we don't need to worry about that here. I wanted to run similar tests for all pins. My electronic tinkering is limited by a frugal budget, but I spared no expense in creating the equipment to run these tests. The probe to test the digital write function was first built when looking at the XIAO SAMD21.

led probe

The two LED version was too expensive. The header pins have not been soldered on the XIAO ESP32C5, so the connection to the ground pin of the board is done with a mini alligator clip. The other end, used to probe each of the edge pads, is just a male Dupont wire. The second instrument, used to test the digital read function, is even simpler.

wire probe

It's a male Dupont wire with an mini alligator clip at one end which can be connected to the ground pad or the 3.3 volt pad as needed. It may look like a wire, but it can easily double as a test push button.

My version of the blink sketch, imaginatively named blink.ino, flashes on and off in a heartbeat pattern each gpio pin in turn. It goes around the board starting with the D0 pad in an anti-clockwise fashion. The built-in LED is also toggled. Connect the alligator clip of the LED probe to the ground pad of the XIAO and hold the other lead of the LED against the pad of the gpio pin being toggled. It will turn on in a heartbeat pattern confirm that the corresponding GPIO pin can be used as an output and that the digitalWrite function works with that pin.

Project: blink all i/o pins Board: XIAO ESP32C5 Check the digital write function of the GPIO pins of an XIAO ESP32C5. Hold the board, component side up, with the USB connector towards the top. Probe each pad in a anti-clockwise fashion starting with the top left pad labelled D0. Probe board pad D0 as gpio pin 1 is turned on and off. --all--off-- Probe board pad D1 as gpio pin 0 is turned on and off. --all--off-- Probe board pad D2 as gpio pin 25 is turned on and off. --all--off-- ... Probe board pad D10 as gpio pin 10 is turned on and off. --all--off-- The yellow user LED (gpio pin 27) is turned on and off. --all--off-- Probe board pad D0 as gpio pin 1 is turned on and off. --all--off-- Probe board pad D1 as gpio pin 0 is turned on and off. ...

Follow the output on the serial monitor to also confirm the correspondence between the Arduino names of the I/O pin and their GPIO number.

Digital Output: Pulse Width Modulation toc

This is basically the same as the blink sketch, but instead of an on-off pattern, a pulse width modulation signal is applied to each pin in a way that makes it seem as if the voltage is being slowly ramped up and down. If a LED is connected to a pin, it will appear to pulsate.

A pwm signal is a square (ON/OFF) wave. It's duty ratio is the proportion of the ON time to the period of the signal. In other words, it is the ratio (ON time)/(ON time + OFF time). The period a wave is 1/frequency. So the true length of time that the signal is on and off depends on the frequency of the signal. As far as I know, there is no function in the Arduino framework to set the PWM frequency. Perhaps there is a non-standard way to do that in the ESP32 Arduino core.

Using the duty ratio is set by using the analogWrite which accepts a value from 0 to 255. Consequently, the command analogWrite(pin, x) means that the pin will be high for x/256 of the time and it will be off for (256-x)/256 of the time. So the voltage at the pin is on average (x/256)*3.3 volts. This is the nearest one can come to a true analogue signal with the ESP32-C5.

Digital Input: Polling toc

In the first two examples the digital pin to which the boot button is connected was "polled" constantly to see if its state had changed. Polling means the value of the gpio pin connected to the boot button is read using the digitalRead(pin) function every time the loop() function is executed. My poll.ino sketch is just an extension of that. It saves each pin's level and monitors each pin in turn to see if the digitalRead() function return a different value compared to the saved value. In that case a report is printed to the serial terminal and the newly read pin value is saved to spot the next transition of that particular pin. Here is part of the output to the serial monitor as the test was performed.

Project: poll all i/o pins Board: XIAO ESP32C5 Checking digitalRead() function of the GPIO pins of a XIAO ESP32C5. All the pins are polled sequentially for a change in their value. There are three modes of operation corresponding to the pin modes INPUT_PULLUP, INPUT_PULLDOWN, or OUTPUT. Switch the mode by pressing the BOOT button. The mode of all I/O pins set to INPUT_PULLUP, probe pins with a connection to GND. ... Pin D5 (gpio 24) is LOW Pin D5 (gpio 24) is HIGH Pin D6 (gpio 11) is LOW Pin D6 (gpio 11) is HIGH Pin D7 (gpio 12) is LOW Pin D7 (gpio 12) is HIGH ... The mode of all I/O pins set to INPUT_PULLDOWN, probe pins with a connection to VCC (3V3). ... Pin D5 (gpio 24) is HIGH Pin D5 (gpio 24) is LOW Pin D6 (gpio 11) is HIGH Pin D6 (gpio 11) is LOW Pin D7 (gpio 12) is HIGH Pin D7 (gpio 12) is LOW ... The mode of all I/O pins set to OUTPUT, probe pins with a connection to GND. ... Pin D5 (gpio 24) is HIGH Pin D5 (gpio 24) is LOW Nothing reported for D6 Pin D7 (gpio 12) is HIGH Pin D7 (gpio 12) is LOW ... The mode of all I/O pins set to INPUT_PULLUP, probe pins with a connection to GND. The mode of all I/O pins set to INPUT_PULLDOWN, probe pins with a connection to VCC (3V3). The mode of all I/O pins set to OUTPUT, probe pins with a connection to VCC. ... Pin D5 (gpio 24) is LOW Pin D5 (gpio 24) is HIGH Nothing reported for D6 Pin D7 (gpio 12) is LOW Pin D7 (gpio 12) is HIGH ...

Everything works as expected when the io pins are in either INPUT_PULLUP or INPUT_PULLDOWN mode. The OUTPUT seems counterintuitive but it does make sense because the ESP32-C5 general purpose io pins support reading the value written to them when in OUTPUT mode. That is a useful property which makes it possible to avoid saving in a variable the value written to an output pin for future reference.

It was not possible to read the value of the D6 (gpio 11) pin when the latter was put in OUTPUT mode. I think that may have to do with the fact that gpio 11 is the TX signal of Serial0 which is the interface to the hardware UART0 peripheral. It is used for debugging during the boot process. I have tried to decouple the pin from the UART0 port with the Serial0.stop() command but that does not work. This remains something to investigate.

Digital Input: Interrupts toc

Instead of polling a pin to see if its state has changed, an interrupt service routine (ISR) can be attached to the pin. Be careful with the ISR, there are things which may not be done while handling an interrupt. Also it is best to keep the ISR short. Consequently a common approach is to set a flag in the ISR. The loop() routine should check the flag and when it is set, it should do whatever is needed in response to the action that caused the interrupt. Don't forget to reset the flag after.

The sketch interrupt.ino illustrates how to handle a button press in the fashion described above. An ISR is attached to each button in turn. The interrupt will occur when the pin is grounded as it would be with an activated push button. To move on to test the next pin, press the boot button. Of course the boot button is handled by an interrupt service routine also.

Project: check interrupts on all i/o pins Board: XIAO ESP32C5 Check the interrupt capabilities of the GPIO pins of an XIAO ESP32C5. An interrupt service routine (ISR) will be attached to each gpio pin in turn. Probe the pin with a grounded wire to test the interrupt. Press the boot button to move on to the next pin. An ISR has been attached to pin D0 (gpio 1) for testing. Interrupt on pin D0 (gpio 1) has been raised 3 times Interrupt on pin D0 (gpio 1) has been raised 1 times Boot button pressed An ISR has been attached to pin D1 (gpio 0) for testing. Boot button pressed without testing D1 An ISR has been attached to pin D2 (gpio 25) for testing. Interrupt on pin D2 (gpio 25) has been raised 1 times Interrupt on pin D2 (gpio 25) has been raised 1 times Interrupt on pin D2 (gpio 25) has been raised 1 times Interrupt on pin D2 (gpio 25) has been raised 2 times Boot button pressed An ISR has been attached to pin D3 (gpio 7) for testing. Interrupt on pin D3 (gpio 7) has been raised 1 times Interrupt on pin D3 (gpio 7) has been raised 1 times Interrupt on pin D3 (gpio 7) has been raised 3 times Interrupt on pin D3 (gpio 7) has been raised 2 times ....

As can be seen by the output to the serial monitor reproduced above, interrupts are very fast. If handling a true button in this fashion, some sort of debounce measure will be needed.

Analogue Input toc

The Getting Started Guide contains a sketch on reading analogue inputs based on the Grove-Rotary Angle Sensor. Initially I was a bit confused because that product is a potentiometer, a variable resistor if preferred. It is not a rotary encoder as I first thought. I may have that item, I certainly have a pot or two somewhere but it was easier to use another XIA0, the first one of them all, the SAMD21 which has a true analogue output. On connecting the A0 output from the SAMD21 to the A0 ADC input of the XIA0 ESP32C5, the following graph was obtained.

Here is the gist of the sketch running on the ESP32-C5

void setup() { Serial.begin(); // delay(2000); // 2 second delay should be sufficient for USB-CDC // Set the ADC resolution to 12 bits (0-4095) analogReadResolution(12); pinMode(A0, INPUT); } void loop() { int inmv = analogReadMilliVolts(iopins[currentindex]); float volts = inmv/1000.0; Serial.printf("Volts:%.3f\tMax:3.3\tGND:0\n", volts); delay(100); }

The actual code in the adc.ino sketch is slightly more complicated because it uses all analogue pads available on the XIAO ESP32C5. The D0 (gpio 1) edge connector is the only analogue input available on the board headers. There are four analogue inputs on the reverse side of the board: MTMS, MTDI, MTCK and MTDO which have the Arduino names A1 to A4 (gpio 2 to 5)respectively.

To run the program, I connected the SAMD21 and the ESP32C5 grounds together, clipped my simple probe to the A0 pin of the SAMD21 which was running the samd21_dac/dac.ino sketch. Then I pressed the other pin of the simple probe into the appropriate solder pad as instructed by adc.ino running on the ESP32C5. Don't be too shy about pressing the probe pin against the pad. A sharp DVM test lead pin would have been better because a good connection was not easily made with the Dupont male pin.

There's a second sketch entitled adc_dvm.ino. It is a bit of a misnomer, but a digital voltmeter was used to monitor the voltage from a small bench power supply as it was connected to the A0 analogue input of the XIAO ESP32C5. The voltage was measured with the dvm and the ESP32-C5 five times at voltages from 0.25 to 3.25 volts increasing 0.25 volts each time. The sketch kept track of the lowest and highest values and calculated the average measured by the XIAO.

DVM (v)Average (v)Min (v)Max (v)Rel. Errormin delta (mv)max delta (mv)
0.2560.2590.2480.2721.17 %-816
0.5070.5080.5030.5150.20 %-48
0.7560.7550.7430.763-0.13 %-137
1.0031.0020.9881.020-0.10 %-1517
1.2531.2561.2431.2680.24 %-1015
1.5021.4921.4821.504-0.67 %-202
1.7521.7561.7521.7650.23 %013
2.0011.9991.9872.006-0.10 %-145
2.2522.2532.2312.2700.04 %-2118
2.5002.2512.4982.513-9.96 %-213
2.7512.7592.7432.7800.29 %-829
2.9963.0012.9913.0260.17 %-530
3.2453.2513.2493.2560.18 %411

This is by no means a scientific test. The connections were not very solid, the leads were much too long and a run of 5 measurements was too short. I have no way of knowing how accurate the DVM is, but I do know that it was a very cheap instrument. Nevertheless, it's comforting to see that the relative error between the DVM and the XIAO is often less than half a percent. The 9.96% error at the nominal voltage 2.5 volts is clearly a transcription error. I will have to redo this test.

Deep Sleep toc

It can be stated with confidence that I have learned nothing more about the sleep modes of RISC-V microcontrollers since I looked at the XIAO ESP32C6. However, the Deep sleep mode code in the Staring Guide contains a very important comment.

esp_deep_sleep_enable_gpio_wakeup(mask, ESP_GPIO_WAKEUP_GPIO_HIGH); delay(2000); //Delay time depends on the serial port / Give the PC time to stabilize Serial.println("Going to sleep now");

That delay after enabling gpio wake-up or timer wake-up from deep sleep makes all the difference. There's no need to explicitly flush and close the serial port as was done in my previous post. Aside from that the logic of the code remains the same. It is based on two facts.

The ideal method to check if the XIAO is in deep sleep mode is to measure its current draw. Unfortunately, I don't have equipment to measure current at such low levels. So imitating others, I have resorted to other means to signal when the board is in deep sleep mode and when it returns to normal mode. Foremost of these are messages sent out via the serial port which works rather well in the Arduino IDE but fails when using the PlaformIDE terminal or a communication program such as picocom. Flashing the onboard LED is another signalling method that works in simple situations.

Finally before looking at the sketches, be warned: it will be necessary to put the XIAO is bootloader mode to download any new compiled firmware to the board if it is running a deep sleep mode sketch. That's because when running these sketches the board is in sleep mode most of the time and that means that its serial port is shut down.

Timer Wake-up toc

In this project, the XIAO is in deep sleep mode for a fixed period; 15 seconds long by default. This is done by enabling a wakeup timer before going into deep sleep mode. Here is a simplified version of the code used to put the board in deep sleep mode.

esp_sleep_enable_timer_wakeup(SLEEP_PERIOD); delay(SETTLE_TIME); Serial.printf("Going into sleep mode for %.1f seconds\n", SLEEP_PERIOD/(1000*1000.0)); ... esp_deep_sleep_start();

Running the sketch on the XIAO ESP32C5 within the Arduino IDE works quite well. Interestingly, the program works just as well with a first generation ESP32.

deep_sleep_tmr.ino in Arduino IDE

As can be seen the IDE signals quite clearly that it is not connected to the XIAO when the latter is in sleep mode. When the board wakes and restarts its serial peripheral, the IDE is very good at reestablishing the connection automatically, so that the serial output of the sketch continues to appear in the IDE serial monitor. By turning on timestamps in the serial monitor, the timing can be verified.

17:51:18.325 -> Project: Test waking the SoC from deep sleep with a timer 17:51:18.325 -> Board: Seeed XIAO ESP32C5 17:51:18.325 -> 17:51:18.325 -> Boot number: 1 17:51:18.325 -> Wakeup was not caused by deep sleep: 0 17:51:20.060 -> Going into sleep mode for 15.0 seconds 17:51:37.585 -> 17:51:37.585 -> Boot number: 2 17:51:37.585 -> Wakeup caused by timer 17:51:40.839 -> Going into sleep mode for 15.0 seconds 17:51:58.368 -> 17:51:58.368 -> Boot number: 3 17:51:58.368 -> Wakeup caused by timer ...

The ESP32-C5 woke up for the 3rd time at 17:51.58 after going into deep sleep at 17:51:40. That was a 17,979 second interval which seems reasonable when adding to the 15 second deep sleep period the 2 second delay after opening the serial port to which must be added the time to perform the boolean test to see if the boot counter is equal to 0, then the time to increment the counter and finally the time required to create the Boot number: xx message and the time to send the latter out to the serial monitor.

PlatformIO Notes

Frankly, running the project in the PlatformIO IDE is painful because, most of the time, it does not reconnect automatically quickly enough if at all. So I added feed back using the onboard user LED. It does two things. First, as the board wakes from deep sleep, it slowly flashes out the number of times the setup() function has been executed since the SoC was powered up. Then the just before the SoC is put back into deep sleep mode, the LED quickly flashes for 5 times. The LED provides just enough information to know if the system is in deep sleep mode or if it is operating normally.

That works well, but it is difficult to show on a static Web page. There is another trick in Linux to see when the board is in deep sleep mode using the watch utility to show the output of the ls -l /dev/ttyACM* command. continuously.

michel@mv:~$ watch -n0 ls -l /dev/ttyACM*

When the XIAO is in deep sleep, its serial peripheral is shut down. Linux therefore closes the device /dev/ttyACM0 which means that it cannot be listed.

Every 0.1s: ls -l /dev/ttyACM* mv: Sat Jan 24 14:12:45 2026 ls: cannot access '/dev/ttyACM*': No such file or directory

When the XIAO returns to normal mode, its serial peripheral is turned on. Linux sees that it is connected to one of the serial ports of the desktop and recreates the /dev/ttyACM0 device. At that point, it will be listed and watch will display the result.

Every 0.1s: ls -l /dev/tty mv: Sat Jan 24 14:31:37 2026 crw-rw---- 1 root dialout 166, 0 jan 24 14:15 /dev/ttyACM0

Exit watch with a CTRL+C keyboard shortcut.

This is why the SLEEP_PERIOD should not be very short. It takes time for Linux to shutdown the ttyACM0 device. If the serial port of the board is reconnected to the desktop during that time, a new serial device will be created, most likely ttyACM1. Then there's no hope that even the Arduino IDE can reconnect to the board.

GPIO Wake-up toc

Another way of waking the system in deep sleep is to apply a signal to a specific low power digital pin that is in INPUT mode. The signal can either be low (0 volts) or high (3.3 volts). If trigger to wake is to be low signal applied to a designated low power pin, then it must be otherwise maintained high with a high value resistor connected to 3.3V. This can be an internal pull up resistor (the pin mode should then be INPUT_PULLUP), or an external resistor. If the wake signal is to be high, then the logic must be inverted and the designated pin must be in INPUT_PULLDOWN mode, or an high value external resistor must connect the pin to ground. There are power usage consequences to using an internal versus an external resistor which will not be broached here because I don't know what they are.

Here is a simplified version of the code used to put the board in deep sleep mode.

Serial.println("\nWake the SoC by setting D0 LOW"); pinMode(D0, INPUT_PULLUP); uint64_t mask = 1ULL << D0; esp_deep_sleep_enable_gpio_wakeup(mask, ESP_GPIO_WAKEUP_GPIO_LOW); delay(SETTLE_TIME); Serial.println("Going to sleep now"); esp_deep_sleep_start();

The sketch is slightly more complex because it tests waking up the system from deep sleep with every low power pin available on the board one after the other. There are six low power pins available on the XIAO ESP32C5.

Low Power PinLabel
LP_GPIO0D1
LP_GPIO1D0
LP_GPIO2MTMS
LP_GPIO3MTDI
LP_GPIO4MTCK
LP_GPIO5MTDO

As before this works well in the Arduino IDE.

deep_sleep_tmr.ino in Arduino IDE

All low power pins are tested in turn. Once all the pins have been tested, the signal needed to wake the board is inverted before another round of tests is performed. Just before going into deep sleep mode, the sketch sends to the serial monitor messages indicating which pin is the designated wake up pin and which signal must be applied to awake the board.

Unfortunately, I could not think of a practical way of running this test in the PlatformIDE. Adding LED blinks as before would not be sufficient. I tried remebering the sequence of the pins to activate and the signal that needs to be applied. It was too much to juggle.

Serial Communication toc

This section is about the serial communication peripherals that are available on all the XIAO boards:

I can conceive of three ways to test the implementation of these communication protocols.

So I'll do my best in the short time available to look at these communication protocols on the XIAO ESP32C5.

UART toc

On my first attempt at running a loop back test, uart.uno, on the hardware serial port, I ran into difficulties. So before wasting too much time, I connected an input pin of a cheap knockoff logic analyzer to the TX (D6) of the XIAO and captured the output from the following short sketch.

#if !defined(ARDUINO_XIAO_ESP32C5) #error "This program is meant to run on the XIAO ESP32C5" #endif #define SERIAL_BAUD 115200 unsigned long writetimer; void setup() { Serial.begin(); delay(2000); Serial.println("\n\nUART TX Test with Logic Analyzer"); Serial1.begin(SERIAL_BAUD, SERIAL_8N1, RX, TX); while (!Serial1) delay(10); Serial.println(); Serial.printf("Hardware serial port (Serial1) initialized with %d baud.\n", SERIAL_BAUD); Serial.println("Starting loop()."); Serial.println("Try to capture 2 M samples at 1MHz after seeing "Now:" in the serial monitor"); } int count = 0; void loop() { Serial.print("Now:"); delay(1000); for (char c='a'; c<'z'; c++) { Serial1.print(c); } Serial.println(); }

As can be seen, the lower-case ASCII letters characters 'a' to 'z' are being transmitted.

PulseView capture of UART TX

It is a bit finicky to capture serial output, so here are the UARD decoder settings that I used.

PulseView UART decoder settings

The run was not completed; as soon as it seemed that enough serial data had been received by the analyzer, I stopped the capture to make it easier to zoom in on the data. In any case, this proved that the UART does transmit.

By the bye, this sketch made the XIAO work harder: on average 70 mA at 5.06 volts or 390 mWatts.

Knowing that the UART transmitted correctly, its TX pin and GND pin were connected to a second XIAO ESP32C5 which printed to its serial monitor any character received. That worked without problems. So I again ran the loop test and this time it worked as output to the serial monitor attests.

Project: UART Loop Back Test Board: Seeed XIAO ESP32C5 Connect the UART TX (D6) output and the RX (D7) input together. Hardware serial port (Serial1) initialized with its baud set to 5242880. setup() completed, starting loop(). Transmitting "Message 1" Rx: Message 1 Transmitting "Message 2" Rx: Message 2 Transmitting "Message 3" Rx: Message 3 Transmitting "Message 4" Rx: Message 4 Transmitting "Message 5" Rx: Message 5

Just to make sure, I disconnected the link between the RX and TX signal line for a few seconds and then I reconnected.

Transmitting "Message 179" Rx: Message 179 Transmitting "Message 180" Rx: Message 180 Transmitting "Message 181" Rx: Transmitting "Message 182" Rx: Transmitting "Message 183" Rx: Transmitting "Message 184" Rx: Transmitting "Message 185" Rx: Message 185 Transmitting "Message 186" Rx: Message 186

I have no idea what the initial problem was; a bad connection perhaps? In any case not only did the loop test work but it was possible to set the baud to 5 megabits. I did not have time to try to capture that serial output with the logic analyzer to check if that rate was truly achieved. I don't even know if the analyzer can work at such a rate. However by setting the Core Debug Level to Verbose in the Tools menu and running the loop test again the following information about the initialization of the UART was sent to the serial monitor.

UART Loop Back Test Connect the UART TX (D6) output to the and RX (D7) input. [ 3185][V][esp32-hal-uart.c:782] uartBegin(): UART1 baud(5242880) Mode(800001c) rxPin(12) txPin(11) [ 3186][V][esp32-hal-uart.c:880] uartBegin(): UART1 not installed. Starting installation [ 3186][V][esp32-hal-uart.c:890] uartBegin(): UART1 RX FIFO full threshold set to 120 (value requested: 120 || FIFO Max = 128) [ 3186][V][esp32-hal-uart.c:916] uartBegin(): Setting UART1 to use XTAL clock [ 3187][V][esp32-hal-uart.c:955] uartBegin(): UART1: RX and TX signals are set not inverted. [ 3187][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RX (2) cleared [ 3188][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_TX (3) cleared [ 3188][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_CTS (4) cleared [ 3188][V][esp32-hal-periman.c:251] perimanClearBusDeinit(): Deinit function for type UART_RTS (5) cleared [ 3189][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 12 successfully set to type INIT (0) with bus 0x0 [ 3189][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 12 successfully set to type UART_RX (2) with bus 0x4080cea8 [ 3189][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_RX (2) successfully set to 0x42003e34 [ 3190][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 11 successfully set to type INIT (0) with bus 0x0 [ 3190][V][esp32-hal-periman.c:160] perimanSetPinBus(): Pin 11 successfully set to type UART_TX (3) with bus 0x4080cea8 [ 3190][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_TX (3) successfully set to 0x42003cfa [ 3191][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_RX (2) successfully set to 0x42003e34 [ 3191][V][esp32-hal-periman.c:235] perimanSetBusDeinit(): Deinit function for type UART_TX (3) successfully set to 0x42003cfa [ 3192][V][esp32-hal-uart.c:983] uartBegin(): UART1 initialization done. [ 3192][V][esp32-hal-uart.c:1034] uartSetRxFIFOFull(): UART1 RX FIFO Full value set to 120 from a requested value of 120

Clearly, the compiler did not have a problem with the specified speed. These phenomenal speeds may not be meaningful in a true application when a serial connection is made to a sensor that only communicates at 9600 baud.

SPI toc

The SPI serial port can be tested in a loop back configuration just like the UART. As with the UART, the serial output pin (MOSI=D10=GPIO10) is connected to the serial input pin (MISO=D9=GPIO9). There is no need to worry about the SPI clock and the chip select (SS) signals in this configuration. Here is the output of the little test program.

Project: SPI Loopback Test Board: Seeed XIAO ESP32C5 Initializing SPI port MOSI (output pin): 10 - connected to MISO (D9) MISO (input pin): 9 - connected to MOSI (D10) initially SCK (clock): 8 - not connected SS (select): 7 - not connected Default SPI clock divider: 2371586 setup() completed, starting loop(). It is necessary to reset the board after the firmware is uploaded. Otherwise, it will seem as if the test fails. Transmitting "Message 1" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 " Transmitting "Message 2" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 " It is necessary to reset the board after the firmware is uploaded. Otherwise, it will seem as if the test fails. setup() completed, starting loop(). Transmitting "Message 1" Received: "Message 1" Transmitting "Message 2" Received: "Message 2" ... connecting MISO to 3.3 volts Transmitting "Message 13" Received: " 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff " Transmitting "Message 14" Received: " 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff " ... connecting MISO to ground Transmitting "Message 21" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 " Transmitting "Message 22" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 "

The received message was exactly what was sent which is what was expected. When the SPI data input pin was connected to the 3.3 volt rail so that the input value of each character was 0xFF corresponding to all bits being equal to 1. When the MISO pin was tied to ground and all received bytes were then equal to 0.

Note to self: find out what the clock dividers mean in terms of SPI transfer rates.

I2C toc

At this point, I will just say that the I2C peripheral to successfully used to display text on a 128x64 OLED screen using the Adafruit SH110X library.

Wi-Fi Communication toc

The three sketches in this section test the Wi-Fi functionality of the XIAO ESP32C5. Its support of the 5 GHz Wi-Fi band is the most obvious difference with other ESP32 modules. That is immediately obvious when running the Wi-Fi scan example.

Wi-Fi Scan toc

Here is the output from running the wifi_scan.ino sketch with the XIAO ESP32C5. Note that this sketch is

Project: Wi-Fi Scan Board: Seeed XIAO ESP32C5 STA MAC: D0:C0:B0:03:02:01 Antenna: A-01 FPC >>>>>>>>>> Starting Wi-Fi STA <<<<<<<<<<<< E (4245) wifi:can not get wifi protocol under WiFi band mode WIFI_BAND_MODE_AUTO, please use esp_wifi_get_protocols instead Wi-Fi STA started Setup done ------------------------------------- Default wifi band mode scan: ------------------------------------- Scan start Scan done 8 networks found Nr | SSID | RSSI | CH | Encryption 1 | BELLHUB-3000 | -30 | 5 | WPA2 2 | devlan | -40 | 6 | WPA2 3 | iotlan | -40 | 6 | WPA2 4 | BELLHUB-3000 | -43 | 104 | WPA2 5 | devlan | -46 | 149 | WPA2 6 | BELLHUB-3000 | -47 | 36 | WPA2 7 | BELL000 | -86 | 6 | WPA2 8 | BELL000_EXT | -97 | 6 | WPA2 ------------------------------------- ------------------------------------- 2.4 Ghz wifi band mode scan: ------------------------------------- Scan start Scan done 4 networks found Nr | SSID | RSSI | CH | Encryption 1 | BELLHUB-3000 | -29 | 5 | WPA2 2 | iotlan | -40 | 6 | WPA2 3 | devlan | -40 | 6 | WPA2 4 | BELL000 | -86 | 6 | WPA2 ------------------------------------- ------------------------------------- 5 Ghz wifi band mode scan: ------------------------------------- Scan start Scan done 3 networks found Nr | SSID | RSSI | CH | Encryption 1 | BELLHUB-3000 | -43 | 104 | WPA2 2 | devlan | -46 | 149 | WPA2 3 | BELLHUB-3000 | -47 | 36 | WPA2 -------------------------------------

That's notably different from the results obtained with a XIAO ESP32C3 which can only connect to Wi-Fi networks on the 2.4GHz Wi-Fi band.

Project: Wi-Fi Scan Board: Seeed XIAO ESP32C3 STA MAC: A0:B0:C0:01:02:03 Antenna: V1.2 FPC Setup done ------------------------------------- Default wifi band mode scan: ------------------------------------- Scan start Scan done 6 networks found Nr | SSID | RSSI | CH | Encryption 1 | BELLHUB-3000 | -27 | 5 | WPA2 2 | iotlan | -30 | 6 | WPA2 3 | devlan | -31 | 6 | WPA2 4 | BELL000 | -80 | 6 | WPA2 5 | mynetwrk | -84 | 6 | WPA2 6 | BELL000_EXT | -88 | 6 | WPA2 -------------------------------------

Here is a warning for those wondering what is the exact nature of the RSSI measurement: it's a rabbit hole. As far as I can make out, there is no standard. What RSSI measures is pretty much up to the manufacturer. It does seem that Espressif is measuring decibels-milliwatt. Here is an interpretation of what these values mean.

RSSIInterpretation
-30 dBmVery strong signal, guaranteeing an excellent connection
-50 dBmExcellent signal, suitable for most demanding uses (streaming, etc.)
-65 dBmAcceptable signal for a stable and reliable connection
-90 dBmVery weak signal, not allowing for a stable connection
Source: tutoduino

Sharp-eyed readers will have noticed the error message in the ESP32-C5 output. That message is printed if the Core Debug Level is set to something other than None. The error a minor bug which has no consequence on the execution of the code. The problem was reported in issue #12200 on January 4, 2026 and a fix was proposed on the next day. The latter has been merged into the first release candidate of version 3.3..6 of the ESP32 Arduino (3.3.6-RC1) (fix(5ghz): Rework WiFi LR logic for 5 GHz chips by @me-no-dev in #12202). I assume the error message will no longer appear in the next stable release of the ESP32 Arduino core.

The power consumption of the XIAO while running this scan is undetectable with my in-line USB DVM when the SoC is idling and it peaks near 90 mA or about 0.45 mW when the radio is used to scan. This was too much for the much solicited USB hub to which the XIAO was connected. The board seemed to be in a tight boot loop as it would reset very quickly after a few print statements in the setup. Setting the Core Debug Level to Debug to try to understand why the board was rebooting, revealed the error message seen above. That sent me on a wild goose chase because it was not the real problem. Eventually, I noticed that a brownout error was signalled at times. That is when I connected the XIAO directly to a USB port on the desktop and found that the scan completed without problems. I should have paid attention to the following warning in the README.md file that accompanied the WiFiScan.ino sketch from the esp32 WiFi examples.

Troubleshooting

Important: Be sure you're using a good quality USB cable and you have enough power source for your project.


Wi-Fi Connect toc

An important difference between the ESP32-C5 and the ESP32-C6 is the former's dual band Wi-Fi capability. Consequently, I feel that it is a bit of a shame that the example program for Wi-Fi connection in the Getting Started Guide makes no explicit mention of the 5 GHz band. Let's be clear, a connection on the 5 GHz band can be made because by default the ESP32-C5 automatically chooses which band it will use. Unfortunately, the sketch does not report that, nor can it be requested to use that band. The following is the source code with a little bit of tweaking to make the situation clearer.

#include <Arduino.h> #include <WiFi.h> // Replace with your network credentials const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; // Which Wi-Fi band should be used // ***Select only one*** //#define USE_BAND WIFI_BAND_MODE_2G_ONLY #define USE_BAND WIFI_BAND_MODE_5G_ONLY //#define USE_BAND WIFI_BAND_MODE_AUTO // Prints current Wi-Fi band mode void printWiFiBandMode(char msg[], wifi_band_mode_t mode ) { if (msg) Serial.printf("%s ", msg); switch (mode) { case WIFI_BAND_MODE_2G_ONLY: Serial.println("2.4 GHz only"); break; case WIFI_BAND_MODE_5G_ONLY: Serial.println("5 GHz only"); break; default: Serial.println("Automatic (2.4 or 5 GHz)"); break; } } void setup() { Serial.begin(115200); delay(2000); Serial.println("\n\n_WiFiConnect.ino"); // Explicitly set mode to Station WiFi.mode(WIFI_STA); printWiFiBandMode("Switching to WiFi Band Mode ", USE_BAND); if (!WiFi.setBandMode(USE_BAND)) // this may fail with SoCs other than ESP32-C5 Serial.println("Failed"); printWiFiBandMode("WiFi Band Mode set to:", WiFi.getBandMode()); // report the actual mode Serial.printf("Connecting to %s ", ssid); WiFi.begin(ssid, password); // Wait for connection while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nCONNECTED!"); // Print connection details Serial.print("IP Address: "); Serial.println(WiFi.localIP()); Serial.print("Subnet Mask: "); Serial.println(WiFi.subnetMask()); Serial.print("Gateway IP: "); Serial.println(WiFi.gatewayIP()); Serial.print("DNS IP: "); Serial.println(WiFi.dnsIP()); // C5 Specific: Check which band and channel we are on Serial.print("Channel: "); Serial.println(WiFi.channel()); Serial.printf("Wi-Fi Band: %s\n", (WiFi.channel() > 14) ? "5GHz" : "2.4GHz"); Serial.print("RSSI (Signal Strength): "); Serial.println(WiFi.RSSI()); } void loop() { // Check if WiFi is still connected if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi lost. Reconnecting..."); WiFi.disconnect(); WiFi.reconnect(); } delay(5000); }

This is the result when using a XIAO ESP32C3 when the USE_BAND macro is set to WIFI_BAND_MODE_5G_ONLY.

_WiFiConnect.ino Default WiFi Band Mode: 2.4 GHz only Switching to WiFi Band Mode: 5 GHz only Failed WiFi Band Mode set to: 2.4 GHz only Connecting to COROBRUN-2 ..... CONNECTED! IP Address: 192.168.22.210 Subnet Mask: 255.255.255.0 Gateway IP: 192.168.22.1 DNS IP: 192.168.22.1 Channel: 5 Wi-Fi Band: 2.4GHz RSSI (Signal Strength): -42

And here is the serial output of the same sketch running on the XIAO ESP32C5 which connected to a 5 GHz network.

_WiFiConnect.ino Default WiFi Band Mode: Automatic (2.4 or 5 GHz) Switching to WiFi Band Mode: 5 GHz only WiFi Band Mode set to: 5 GHz only Connecting to COROBRUN-2 .... CONNECTED! IP Address: 192.168.22.209 Subnet Mask: 255.255.255.0 Gateway IP: 192.168.22.1 DNS IP: 192.168.22.1 Channel: 36 Wi-Fi Band: 5 GHz RSSI (Signal Strength): -46

Many access points have two wireless networks with the same SSID, one on the 2.4 band and the other on the 5 GHz bands. Indeed our ISP provided router/modem runs 3 wireless networks. I believe the second 5 GHz network is for the TV box sets. I wanted a sketch that would handle such a situation so I cobbled together the wifi_connect.ino sketch. When it begins, it lists all found wireless networks with the SSID specified in the secrets.h file. Running this code on the XIAO ESP32C5 gives the best results.

Project: Wi-Fi Connect / Disconnect Board: Seeed XIAO ESP32C5 STA MAC: D0:C0:B0:03:02:01 Antenna: A-01 FPC WiFi Band Mode: Automatic (2.4 or 5 GHz) Searching for networks named COROBRUN-2 Scanning both the 2.4 and 5 GHz WiFi bands Found 3 networks named COROBRUN-2 1: BSSID: EE:BB:DD:88:99:AA, RSSI: -38 dBm, channel: 5, band: 2.4GHz, encrpytion: WPA2 2: BSSID: E6:BB:DD:88:99:BB, RSSI: -40 dBm, channel: 157, band: 5GHz, encrpytion: WPA2 3: BSSID: EE:BB:DD:88:00:CC, RSSI: -47 dBm, channel: 36, band: 5GHz, encrpytion: WPA2 Connect to which network? (0 for automatic selection) 2 Module setup done WiFi is disconnected WiFi disconnected for 5000 milliseconds or more Connecting [WiFi.begin(COROBRUN-2, *******, 0, E6:BB:DD:88:99:BB)] WiFi connected to COROBRUN-2, BSSID: E6:BB:DD:88:99:BB RSSI: -44 dBm, channel 157, band 5 GHz IP address: 192.168.22.209 WiFi connected to COROBRUN-2, BSSID: E6:BB:DD:88:99:BB WiFi connected to COROBRUN-2, BSSID: E6:BB:DD:88:99:BB ...

This error message
E (4260) wifi:can not get wifi protocol under WiFi band mode WIFI_BAND_MODE_AUTO, please use esp_wifi_get_protocols instead
will be encountered if the ESP32 Arduino core is version 3.3.5 or older. But even with that error the sketch works.

As can be seen, if more than one network can be found, the user is given the possibility to choose to which network to connect. In the Arduino IDE, enter the one digit index of the desired network in the message box at the top of the serial monitor and then strike the Enter key to send the index to the XIAO. Yes it's a bug. If there are 10 or more networks sharing the specified SSID, only one of the first nine can be chosen. Track that the XIAO connected to the correct network by looking at the BSSID.

When there's the initial scan finds only one network, then the user is not given a choice, the system will connect to that network if possible. The sketch has no problem with when a SoC does not handle 5 GHz band although the output is not quite clear that there is no scan of the 5 GHz band. Here is the output to the serial monitor when using a XIAO ESP32C3.

Project: Wi-Fi Connect / Disconnect Board: Seeed XIAO ESP32C3 STA MAC: A0:B0:C0:01:02:03 Antenna: V1.2 FPC WiFi Band Mode: 2.4 GHz only Searching for networks named COROBRUN-2 Scanning both the 2.4 and 5 GHz WiFi bands Found 1 networks named COROBRUN-2 1: BSSID: EE:BB:DD:88:99:AA, RSSI: -40 dBm, channel: 5, band: 2.4GHz, encrpytion: WPA2 Module setup done WiFi is disconnected WiFi disconnected for 5000 milliseconds or more Connecting [WiFi.begin(COROBRUN-2, *******, 0, EE:BB:DD:88:99:AA)] WiFi connected to COROBRUN-2, BSSID: EE:BB:DD:88:99:AA RSSI: -40 dBm, channel 5, band 2.4GHz IP address: 192.168.22.210 WiFi connected to COROBRUN-2, BSSID: EE:BB:DD:88:99:AA ...

This sketch is clearly a work in progress. In particular the disconnect part is not yet implemented.

Wi-Fi Throughput toc

I was asked to measure the throughput of the Wi-Fi peripheral. That was something that I wanted to do for other devices but never got around to doing. So I am grateful for the impetus that finally got me to do something about this. As usual, I took the easy way out. I don't mean that I asked AI to write the code. Instead I asked a search engine to find an article on the subject and it came up with a good and recent (22 Dec. 2025) article by Tutoduino.fr entitled Analyze your ESP32 WiFi performance. Part 2 Wi-Fi throughput is most pertinent for us.

The article is quite clear and should be read, but here is the gist of the test. First a 20 MiB (20,971,520 bytes) file is created on a desktop on the same network as the ESP32 to test. Then a Python http server which can deliver the file is started. The firmware uploaded to the ESP32 creates an HTTP client that downloads the file. It does nothing with the downloaded data which would slow the process down. We are interested in the download speed only. After the download is completed, the sketch uses the time the download took to calculate the number of Mbits (1,000,000 bits) were transferred per seconds.

Tutoduino reports a speed of 7.29 Mbits/s with an ESP32-C3. I obtained similar results. Here is a table summarizing my first tests with the sketch.

BoardAntennaBandThroughput (MBits)
XIAO ESP32C5External FPC5 GHz18.64
XIAO ESP32C5External FPC2.4 GHz13.98
XIAO ESP32C6Onboard ceramic2.4 GHz8.83
XIAO ESP32C3External FPC2.4 GHz6.99
LOLIN32 LiteOnboard PCB2.4 GHz6.71

The rates are not accurate. They tend to underestimate the actual transmission rate, since there is more than the 20 MiB that was downloaded from the web server. Each IP packet contains a header and then the TCP packet which contains the IP packet also adds a header and both these headers are transmitted along with the data. Furthermore, TCP is a complex protocol with handshakes and acknowledgements designed to recover from transmission errors. All that overhead is not counted in our test. Finally, Tutoduino states that the throughput is appreciably higher when the code is developed in the ESP-IDF which is not surprising.

With that said, the results are valid from a comparative point of view. It is striking that the XIAO ESP32C5 using the slower 2.4 GHz band is twice as fast as the Lolin32 Lite which has a first generation ESP32 soc on board. The comparison would have been fairer if a similar board fitted with an FPC antenna had been tested.

The tests were done in optimal conditions given our wireless network. The development boards were only 4 metres from the Wi-Fi access point with no real obstacle. Those are the circumstances where the 5 GHz band is expected to be faster than the 2.4 GHz and that is what is observed. At the same time, optimal conditions is not where the antenna will make much difference. I did try replacing the external FPC antenna with a rod antenna but it didn't make much of a difference as expected.

More tests are clearly warranted.

Zigbee toc

Support for Zigbee in the ESP32 Arduino core continues to grow at a fast pace. The Zigbee examples directory now contains about 30 files covering all sorts of sensors and actuators. I have yet to look at most of these. Indeed, only the same two sketches tested with the XIAO ESP32C6 a few months ago are reviewed here. There is basically no change in the Zigbee_On_Off_Light sketch. A background task has been added in the Zigbee_On_Off_Switch firmware which polls regularly the state of the light which presumably makes it more responsive.

When compiling the sketches that are found in the 07_zigbee directory of my GitHub repository, it is necessary to set some options in the Tools menu correctly. The Readme.md in the subdirectory for each sketch display a screen capture of the appropriate settings which are not the same for both.

As with before with the XIAO ESP32C6, the XIAO ESP32C5 running Zigbee_On_Off_Light can join a Zigbee2MQTT controlled Zigbee network. Joining (or pairing, both terms seem to be used interchangeably) any new device can be daunting at first, but after a few success pairing will become much easier... trust me. Here are a few recommendations to make things easier.

  1. Before uploading the firmware to the XIAO enable the Erase All Flash Before Sketch Upload option in the Arduino IDE Tools menu. The rationale is to ensure that the XIAO is not attempting to reconnect to a coordinator used before. Well, that's how I think is going on when there are pairing problems. Of course the other Zigbee related options in the Tools menu must be correctly defined.
  2. Enable joining (pairing) in the Zigbee2MQTT web interface. When testing a new device, I make it a rule to pair with the coordinator of our Zigbee network, not with any router. The following figure should make it clear how to go about this.
    enable pairing in z2m. Notifications will start popping up in the Zigbee2MQTT web interface.
  3. Place the XIAO very near the coordinator, just a few millimetres or even have the antennas kissing.
  4. Reset the XIAO, because when it restarts it goes into pairing mode. If all goes well, more notifications about the newly found device and about how the interview with it is proceeding should be appearing.

If everything went according to plan, and the interview was successful, the XIAO will show up at the bottom of the list of devices in the Zigbee2MQTT interface.

When there is a problem, which is pretty much the norm with a new device, there are a couple of things that may help. First I unplug a nearby router. It's an IKEA Zigbee bulb only two metres away and closer to my desktop machine than the coordinator. There's a possibility that the XIAO "talked" to it just after uploading the sketch and that might interfere. Secondly, I remove any device in that may have been created in the attempt to pair the XIAO.

Click on the trash can beside the undesired device and then check the Force remove option before clicking on the Delete button in the pop-up dialogue.

Try multiple times to pair the device. Be patient. As encouragement, I'll boast that every no-name Zigbee device purchased from questionable sources in far away places which offer no support at all has joined our Zigbee network thanks to Zigbee2MQTT. The unsupported device above the XIAO is a "Smart Sensor, Model C3007" according to the label on the box. It should be a PIR sensor according to some reports but it shows up as a ZG-102ZM which is a vibration and contact switch. Even IKEA zigbee devices have been paired although they tend to be even more difficult to work with initially. If problems persist, look at the FAQ Why does my device not ... pair? by Zigbee2MQTT.

While Zigbee2MQTT reports that the XIAO is paired, it is classed as unsupported. It was relatively easy to add an external converter to change that.

It would be too long to explain how that was done here (see Support new devices and External converters). However, it does not really matter. Both the XIAO and the vibration sensor show up in my home automation system based on Domoticz and work as expected.

I expect to add two more example sketches in the Zigbee section. Until then, I'll end by admitting that I can't say if pairing with the Zigbee2Tasmota bridge is possible; I "broke" my bridge trying to update it.

Bluetooth LE toc

Like most ESP32 SoC, the ESP32-C5 has Bluetooth® Low Energy (BLE) capabilities.

Scanning for Bluetooth LE Devices toc

As a first step, let's run a slightly modified version of the BLE scan sketch found in the Getting Started Guide. There seems to be an error near the end of the loop routine where a pointer is probably released a second time. That delete foundDevices; //Release pointer statement was not in the original Arduino example sketch by Evandro Copercini. The implementation of String BLEAdvertisedDevice::toString() in .../.arduino15/packages/esp32/hardware/esp32/3.3.5/libraries/BLE/src/BLEAdvertisedDevice.cpp handles most elements that may be found in BLE device advertisements except for serviceDataUUID. Consequently, a little bit of code was added to the MyAdvertisedDeviceCallbacks code to show that information when available. Don't misconstrue this, I don't know what I am doing, but it was interesting to see that when Bluetooth is activated on my relatively new Android phone ( it shows up during the scan and it does have as serviceDataUUID.

Here is the output of the modified sketch.

Bluetooth LE Scan Board: Seeed XIAO ESP32C5 Antenna: A-01 FPC Setup completed, starting main loop. Starting scan... Advertised Device: Name: , Address: 12:13:14:15:16:21, rssi: -72, serviceData: J#UZKY2�a�!�TN�&�p����Q", serviceDataUUID: 0000fef3-0000-1000-8000-00805f9b34fb Advertised Device: Name: level: 84.4 % horiz, Address: cc:bb:88:00:55:11, appearance: 1347, manufacturer data: b1034f544f54454c450200f9200000385906180100ff00000000, rssi: -93 Scan finished. Number of devices found: 2 Scan completed, clearing results... Starting scan... Advertised Device: Name: , Address: 12:13:14:15:16:21, rssi: -82, serviceData: J#UZKY2�a�!�TN�&�p����Q", serviceDataUUID: 0000fef3-0000-1000-8000-00805f9b34fb Scan finished. Number of devices found: 1 Scan completed, clearing results... Starting scan... Advertised Device: Name: , Address: 12:13:14:15:16:21, rssi: -86, serviceData: J#UZKY2�a�!�TN�&�p����Q", serviceDataUUID: 0000fef3-0000-1000-8000-00805f9b34fb Advertised Device: Name: 302610.1|0|-75, Address: cc:bb:88:00:55:11, appearance: 1347, manufacturer data: b1034f544f333238318600c5011018210362060304b0130205, rssi: -94 Scan finished. Number of devices found: 2 Scan completed, clearing results...

I can't identify the second device with a rather low rssi. Perhaps it is the set top box which has a Bluetooth remote. Even in the interest of science, I was not willing to turn off that device which is finicky and takes for ever to come back on line. If my shenanigans were to interrupt or prevent the taping of a programme by someone else in the household, it could be costly for me.

All that matters is that the scan was successful. However I must point out two things. When I first ran the sketch, the XIAO was caught in a boot loop, resetting almost immediately after the setup() was completed. Most times there was no hint as to the cause of the problem even if Core Debug Level was set to Warn in the Tools menu. However occasionally, an error message was visible.

Bluetooth LE Scan Board: Seeed XIAO ESP32C5 Antenna: A-01 FPC Bluetooth LE Scan Board: Seeed XIAO ESP32C5 Antenna: A-01 FPC E BOD: Brownout detector was triggered ESP-ROM:esp32c5-eco2-20250121 Build:Jan 21 2025 rst:0x3 (RTC_SW_HPSYS),boot:0x18 (SPI_FAST_FLASH_BOOT) SPI mode:DIO, clock div:1 load:0x408556b0,len:0x12c0 ...

It finally dawned on me that the culprit was the USB hub to which the XIAO was connected. The problem disappeared on connecting the XIA to another powered hub capable of powering the XIA when it uses its 2.4 GHz radio. This sketch requires more energy than when doing nothing with peaks up to 330 mWatts (60 mA at 5.06 or 5.07 volts). That is an important lesson: even Bluetooth is relatively power hungry.

The other warning that might be useful to others is that it was necessary to reboot the XIAO after uploading the sketch otherwise the scan appears to be working but the data shown is all wrong.

Implementing a Bluetooth Server toc

Moving on to setting up the XIAO as a BLE server using a slightly modified version of the BLE Server Program in the Getting Started Guide. The source code for the modified version is available in the GitHub repository. After uploading the firmware to the XIAO, I used nRF Connect for Mobile from Nordic Semiconductor ASA on an Android telephone to read and write the characteristic.

The XIAO_ESP32C5_Server is found when a scan is performed. Click on its Connect button. Click on the CLIENT tab and then on its Unkown service which has the UUID specified in server.ino : #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b". Then nRF Connect displays the unknown characteristic #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8". Unknown here means that the service and its characteristic are not predefined and registered values. But the program knows that the characteristic can be read and written. Click on the down arrow to read the characteristic.

The characteristic read is an array of bytes whose value is shown in hexadecimal and as ASCII characters. The phrase "Hello World from XIAO" is, of course, the value that was given to the characteristic in the sketch. Click on the up arrow of the characteristic to write a new value. Select the value type to be Text and enter a new string. I chose "Hi!". Click on the SEND button. The next screen shows that the value has indeed changed to "Hi!". While this was going on, the sketch displayed the following in the serial monitor.

Project: Bluetooth LE Server Board: Seeed XIAO ESP32C5 BLE MAC: D0:C0:B0:03:02:03 Antenna: A-01 FPC Data read by Client: Hello World from XIAO Data received from Client: Hi!

If one does not have access to nRF Connect on a smart phone, then the equivalent can be done from a terminal in Linux with a machine that has a Bluetooth LE enabled card.

michel@trig:~$ sudo gatttool -I [ ][LE]> connect D0:C0:B0:03:02:03 Attempting to connect to D0:C0:B0:03:02:03 Connection successful [D0:C0:B0:03:02:03][LE]> primary attr handle: 0x0001, end grp handle: 0x0005 uuid: 00001800-0000-1000-8000-00805f9b34fb attr handle: 0x0006, end grp handle: 0x000d uuid: 00001801-0000-1000-8000-00805f9b34fb attr handle: 0x000e, end grp handle: 0xffff uuid: 4fafc201-1fb5-459e-8fcc-c5c9c331914b [D0:C0:B0:03:02:03][LE]> char-desc 0x0005 handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb handle: 0x0006, uuid: 00002800-0000-1000-8000-00805f9b34fb handle: 0x0007, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x0008, uuid: 00002a05-0000-1000-8000-00805f9b34fb handle: 0x0009, uuid: 00002902-0000-1000-8000-00805f9b34fb handle: 0x000a, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x000b, uuid: 00002b3a-0000-1000-8000-00805f9b34fb handle: 0x000c, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x000d, uuid: 00002b29-0000-1000-8000-00805f9b34fb handle: 0x000e, uuid: 00002800-0000-1000-8000-00805f9b34fb handle: 0x000f, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x0010, uuid: beb5483e-36e1-4688-b7f5-ea07361b26a8 [D0:C0:B0:03:02:03][LE]> char-desc 0x000d handle: 0x000d, uuid: 00002b29-0000-1000-8000-00805f9b34fb handle: 0x000e, uuid: 00002800-0000-1000-8000-00805f9b34fb handle: 0x000f, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x0010, uuid: beb5483e-36e1-4688-b7f5-ea07361b26a8 [D0:C0:B0:03:02:03][LE]> char-desc 0xffff Error: Discover descriptors failed: No attribute found within the given range [D0:C0:B0:03:02:03][LE]> char-read-hnd 0x0010 Characteristic value/descriptor: 48 65 6c 6c 6f 20 57 6f 72 6c 64 20 66 72 6f 6d 20 58 49 41 4f

Of course, that value is "Hello world from XIAO" in hexadecimal. So the characteristic can be read. Let's write send "Hi!" which in hex is 48 69 21 to the server. Indeed it will be done twice, the second time waiting for confirmation.

[D0:C0:B0:03:02:03][LE]> char-write-cmd 0x0010 486921 [D0:C0:B0:03:02:03][LE]> char-write-req 0x0010 486921 Characteristic value was written successfully [D0:C0:B0:03:02:03][LE]> char-read-hnd 0x0010 Characteristic value/descriptor: 48 69 21

That last read confirms that the "Hello world from XIAO" value was replaced with "Hi!". This is what the firmware displayed in the serial monitor.

Project: Bluetooth LE Server Board: Seeed XIAO ESP32C5 BLE MAC: D0:C0:B0:03:02:03 Antenna: A-01 FPC Data read by Client: Hello World from XIAO Data received from Client: Hi! Data received from Client: Hi! Data read by Client: Hi!

The idea to use a Linux desktop comes from an article by Tony DiCola entitled Reverse Engineering a Bluetooth Low Energy Light Bulb written in 2015-03-01 and updated in 2024-03-08.

No matter which method is used, it has been confirmed that the XIAO ESP32C5 can be a Bluetooth server.

Adding a Bluetooth Client toc

A BLE Client Program from the Getting Started Guide running on the XIAO ESP32C5 can obtain read and write the data from the BLE server just as the nRF Connect application in Android and the gatttool utility in Linux. Here is the serial monitor output of the client XIAO.

Starting BLE Client... Scanning for Server... Target Server not found, will retry later. Server not connected, rescanning... Scanning for Server... Found target Server! Name: XIAO_ESP32C6_Server, Address: 54:55:56:57:58:56 Connecting to 54:55:56:57:58:56 Connected to Server successfully! Service found. Characteristic found. Ready to communicate! Read value from Server: Hello World from XIAO Sent: Hello from Client @ 96s Read value from Server: Hello from Client @ 96s Sent: Hello from Client @ 101s Read value from Server: Hello from Client @ 101s Sent: Hello from Client @ 106s Read value from Server: Hello from Client @ 106s Sent: Hello from Client @ 112s

At the same time, the BLE server running on an XIAO ESP32C6 was displaying corresponding messages on its serial monitor.

Project: Bluetooth LE Server Board: Seeed XIAO ESP32C6 BLE MAC: 54:55:56:57:58:56 Antenna: INTERNAL CERAMIC Data read by Client: Hello World from XIAO Data received from Client: Hello from Client @ 96s Data read by Client: Hello from Client @ 96s Data received from Client: Hello from Client @ 101s Data read by Client: Hello from Client @ 101s Data received from Client: Hello from Client @ 106s Data read by Client: Hello from Client @ 106s Data received from Client: Hello from Client @ 112s ...

Thread and Matter toc

Justice can't be done to these topics here. Mostly because I know next to nothing about these technologies and our home automation system does not support Thread and Matter. Having said this, two XIAO ESP32C5 communicated with each other using Thread. The first, running the < example sketch, was able to find the other XIAO ESP32-C5 running the File/Examples/OpenThread/CLI/SimpleNode.ino sketch. This is the output for the simple node which, presumably, is broadcasting its existence.

Role: Detached RLOC16: 0xfffe Network Name: OpenThread-ESP Channel: 15 PAN ID: 0x1234 Extended PAN ID: dead00beef00cafe Network Key: 00112233445566778899aabbccddeeff Thread Node State: Detached Thread Node State: Detached Thread Node State: Leader Thread Node State: Leader ...

And this is what the scan node displayed as it found the other board.

This sketch will continuously scan the Thread Local Network and all devices IEEE 802.15.4 compatible Scanning for nearby IEEE 802.15.4 devices: | PAN | MAC Address | Ch | dBm | LQI | +------+------------------+----+-----+-----+ Scanning MLE Discover: Scanning for nearby IEEE 802.15.4 devices: | Network Name | Extended PAN | PAN | MAC Address | Ch | dBm | LQI | +------------------+------------------+------+------------------+----+-----+-----+ ... Scanning MLE Discover: | PAN | MAC Address | Ch | dBm | LQI | +------+------------------+----+-----+-----+ | 1234 | c6ff5f60cceb9cb0 | 15 | -11 | 10 | Scanning for nearby IEEE 802.15.4 devices: | Network Name | Extended PAN | PAN | MAC Address | Ch | dBm | LQI | +------------------+------------------+------+------------------+----+-----+-----+ | OpenThread-ESP | dead00beef00cafe | 1234 | c6ff5f60cceb9cb0 | 15 | -16 | 10 | ...

The only thing that was necessary to get these sketches to work was to enable PSRAM in the Arduino IDE Tools menu in both cases.

While leaving the Thread scanner running, the File/Examples/Matter/MatterOnOffLight.ino sketch was uploaded to the XIAO previously running the simple Thread node. Again PSRAM had to be enabled.

ESP-ROM:esp32c5-eco2-20250121 Build:Jan 21 2025 rst:0x3 (RTC_SW_HPSYS),boot:0x18 (SPI_FAST_FLASH_BOOT) SPI mode:DIO, clock div:1 load:0x408556b0,len:0x12c0 load:0x4084bba0,len:0xc9c load:0x4084e5a0,len:0x3188 entry 0x4084bba0 > E (2546) chip[DL]: Long dispatch time: 1119 ms, for event type 2 E (2557) chip[DIS]: Failed to remove advertised services: 3 E (2557) chip[DIS]: Failed to advertise commissionable node: 3 E (2558) chip[DIS]: Failed to finalize service update: 3 Matter Node is not commissioned yet. Initiate the device discovery in your Matter environment. Commission it to your Matter hub with the manual pairing code or QR code Manual pairing code: 34970112332 QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT:Y.K9042C00KA0648G00 Matter Node not commissioned yet. Waiting for commissioning. Matter Node not commissioned yet. Waiting for commissioning. Matter Node not commissioned yet. Waiting for commissioning. > E (2539) chip[DL]: Long dispatch time: 1112 ms, for event type 2 E (2550) chip[DIS]: Failed to remove advertised services: 3 E (2550) chip[DIS]: Failed to advertise commissionable node: 3 E (2551) chip[DIS]: Failed to finalize service update: 3 ... Matter Node not commissioned yet. Waiting for commissioning. Matter Node not commissioned yet. Waiting for commissioning. ...

After a minute or so of complaining about not being commissioned, the Matter on/off node was discovered by the Thread scanner. As stated before, I do not have a Thread network and Matter hub, so it was surprising to receive an e-mail from Alexa daughter of a female warrior.

Cher Michel Un appareil de votre réseau a signalé à votre appareil Echo qu’il est compatible avec Alexa ou qu’Alexa est intégré. En connectant votre appareil à Alexa, vous pouvez activer des fonctionnalités utiles comme le contrôle vocal et l’automatisation avec des routines. Pour voir lesquels de vos appareils domotiques sont disponibles pour être connectés à Alexa, vous pouvez cliquer sur le lien ci-dessous. Voir mes appareils disponibles Ou ouvrez la dernière version de l’application Amazon Alexa, appuyez sur l’icône « + » dans le coin supérieur droit, puis sélectionnez Appareil. Les appareils qui peuvent être connectés apparaîtront sous APPAREILS DISPONIBLES. ...

Loose translation: Alexa is reporting that a compatible device has signalled its presence and that it could be controlled by Alexa voice commands and automation routines once connected and activated. The only Alexa device operating is a Echo Dot purchased in 2020. I didn't even think it had a IEEE 802.15.4 radio; the Echo Dot description in my purchase history contains no mention of Zigbee, Thread or Matter. Let's not go down this rabbit hole. I just installed Amazon Alexa app on an Android phone, and followed the instructions. The short of it is that using the 4th gen Echo Dot, Alexa recognized the On/Off Matter node it could not connect to it. Finally, I looked at the manual, the README file accompanying the sketch and found this rather prominent table and notes?

SoC Wi-Fi Thread BLE
Commissioning
LED Status
ESP32 Optional Fully supported
ESP32-S2 Optional Fully supported
ESP32-S3 Optional Fully supported
ESP32-C3 Optional Fully supported
ESP32-C5 Optional Supported (Thread only)
ESP32-C6 Optional Fully supported
ESP32-H2 Optional Supported (Thread only)

Note on Commissioning:

It does appear that an ESP based Matter device can operate either on a Wi-Fi network or a Thread network. Running the sketch on XIAO ESP32C6 connected to Wi-Fi could be provisioned and Alexa was then able to control its LED.

Conclusions toc

The XIAO ESP32C5 is quite similar to the XIAO ESP32C6. I think either represent a relatively big step up from the XIAO ESP32C3. However, which one should be chosen? The obvious overused answer is to say that it depends on what's needed. The fact is that the tradeoffs are relatively subtle and more tests are needed to distinguish them. But what I really need is much more experience with them in practical projects. The conclusion may be available in a future version of this blog.

<-First Look at the Seeed Studio XIAO ESP32C6
<-First Look at the Seeed Studio XIAO ESP32C3