2024-08-10
md
First Look at the Seeed Studio XIAO ESP32C6
<-First Look at the Seeed Studio XIAO ESP32C3

This is an account of a first look at the XIAO ESP32C6, the eighth and latest member of the XIAO family of tiny development boards from Seeed Studio. I had been looking forward to using the board's Zigbee capabilities in a practical project probably in conjunction with the low current drain of the board when the microcontroller is in deep sleep. However, it must be reported that at this point I have not been able to attain my goals with respect to Zigbee. But there's every reason to believe that it will be possible in the not too distant future. The situation is very fluid as Espressif is rapidly issuing new releases of the ESP-IDF (11 since the beginning of the year) and there have been 3 versions of the ESP32 Arduino core released since the first version of this post.

It is all fun and exciting and for that reason, this remains a work in progress... There may be more to come, even if its only corrections.

Table of Content

  1. Disclosure
  2. Arduino IDE Only... For Now
  3. First Connection
  4. A Big Brother to the XIAO ESP32C3
  5. Pin Numbers and Names
  6. Blinky, Blinky
  7. Internal vs External Antenna
  8. The Wi-Fi Blackhole - An Unexpected Quirk
  9. Connection Time vs Transmit Power
  10. Web Controlled LED
  11. Bluetooth Controlled LED
  12. Oh... the Zigbee
  13. Deep Sleep
  14. Resources

Disclosure toc

Clearly Seeed Studio does not count on me to promote their products to millions or thousands of potential buyers. For one thing this site and its associated GitHub account have a limited readership. Secondly, the friendly staff at Seeed sent an email saying they would be sending some samples almost at the same time the XIAO ESP32C6 was released to the public. I am most grateful for this gesture from Seeed which wasn't necessary by any means. I would have purchased some boards even if they hadn't reached out to me once again. If it's not clear, I really like the XIAO series, and even before the gift boards arrived, I purchased more ESP32C6s. Now I have six boards and can afford to fry one or two while performing stupid experiments with them.

Actually, no XIAO has ever been harmed in this household. They have all survived so far.

Just to dot the i's and cross the t's, Seeed Studio supplied boards free of charge but otherwise did not provide any other payment. Furthermore, Seeed did not request that I write this post, or anything else for that matter, and it does not have any editorial control.

Arduino IDE Only... For Now toc

The current platform-espressif32 (version 6.7.0 dated May 14, 2024) for Espressif 32 bit devices use in PlatformIO (PIO) is based on the rather old ESP32 Arduino core version 2.0.16. Consequently it does not support the XIAO ESP32C6. Indeed, PIO will not support the ESP32-C6 until such time as it moves to version 3.0 (or better) of the ESP32 Arduino core. See the last comment in the Add board support for Seeed XIAO ESP32C6 issue in the platform-espressif32 repository that confirms this situation. For that reason, I have used the latest version (2.3.2) of the Arduino IDE to write sketches to investigate the new XIAO. There is no real doubt that, some time in the future, PIO will move on to the new ESP32 Arduino core and will support the ESP32-C6 and the XIAO ESP32C6 explicitly. In preparation for that situation, the example sketches are set up in a way that will make it easier to add PIO compatibility. In the meantime, all sketches presented in this post will compile in the Arduino IDE provide the following configuration is done.

  1. Add https://espressif.github.io/arduino-esp32/package_esp32_index.json in the Additional Boards Manager URLS in the Preferences window in the IDE.
  2. Install platform esp32 by Espressif version 3.0.2 or newer with the Boards Manager. Hopefully, version 3.0.3 released on July 17 did not introduce incompatibilities.
  3. Select the XIAO_ESP32C6 board

The Arduino IDE does have some annoying quirks. The Copy-paste data from the serial monitor problem has been around for some time. Typically I use the venerable cu terminal program to work with serial ports, but picocom is a better to grab the serial output. Here is the command line for anyone wishing to do the same.

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

Of course, it could be necessary to adjust the device name (or COM port in Windows). The mapping from lf (linefeed) to crlf (carriage return, linefeed) is necessary otherwise, new lines will not start in the first column. Be careful, picocom will not work if the serial monitor of the Arduino IDE has control of the serial port connected to the XIAO.

First Connection toc

When a XIAO is connected to a USB port on a Windows machine, the distinctive billy boop sound should be heard as the device is recognized by the operating system. The board will then be assigned a COM port such as COM6.

In Linux one can listen to bus messages as the operating system recognizes the device. Start the dmesg command line utility with the -w flag and then plug in the XIAO ESP32C6.

michel@hp:~$ dmesg -w .... [62903.132022] usb 3-14: new full-speed USB device number 19 using xhci_hcd [62903.281388] usb 3-14: New USB device found, idVendor=303a, idProduct=1001, bcdDevice= 1.02 [62903.281404] usb 3-14: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [62903.281410] usb 3-14: Product: USB JTAG/serial debug unit [62903.281414] usb 3-14: Manufacturer: Espressif [62903.281418] usb 3-14: SerialNumber: 54:32:04:11:AF:CC [62903.285417] cdc_acm 3-14:1.0: ttyACM0: USB ACM device ...

Use the CtrlC keyboard combination to close that application. One could instead hunt for the device once it is plugged in.

michel@hp:~$ lsusb ... Bus 003 Device 020: ID 303a:1001 Espressif USB JTAG/serial debug unit michel@hp:~$ ls -l /dev/serial/by-id total 0 lrwxrwxrwx 1 root root 13 mai 5 13:10 usb-Espressif_USB_JTAG_serial_debug_unit_54:32:04:11:AF:CC-if00 -> ../../ttyACM0

More information can be obtained with the sudo lsusb -v -d 303a:1001. Much of it is not that useful for me, but it does display the serial number which I believe is the MAC address of the device.

michel@hp:~$ sudo lsusb -v -d 303a:1001 | grep iSerial iSerial 3 54:32:04:11:AF:CC

That unique identifier could be very useful when trying to set up udev rules for specific devices that are connected to a USB port.

The XIAO arrived with preloaded firmware, so when it was connected to a desktop computer with a USB cable, its red LED turned on and its yellow LED flashed at least for a while. The following messages were sent to the serial port.

Boot number: 1 E (1279) gptimer: gptimer_start(345): timer is not enabled yet Set up done WiFi Test start Use OnBoard ANT WiFi Test PASS ANT Test PASS Use Plug ANT WiFi Test PASS ANT Test FALL -72 -72 -74 ...

Clearly, the device is performing some wireless radio tests, first with the onboard antenna, with success, and then with the external antenna which failed without surprise since none was connected. I gingerly connected a PCB antenna to this new board and looked at the serial output again. Be careful when doing this, the U.FL coaxial connector is delicate and will probably not survive very many connections (the female connector on the antenna is rated for only 30 reconnections, see Hirose U.FL).

Boot number: 1 E (1272) gptimer: gptimer_start(345): timer is not enabled yet Set up done WiFi Test start Use OnBoard ANT WiFi Test PASS ANT Test PASS Use Plug ANT WiFi Test PASS ANT Test PASS BLE Test Start Use OnBoard ANT BLE Test FALL BLE Test Start Use OnBoard ANT BLE Test FALL BLE Test Start Use OnBoard ANT BLE Test FALL

This time, the WiFi tests with the onboard and external antennas passed, and a Bluetooth test failed. The board did not seem to advertise itself as a Bluetooth device, so I had no idea what could be done at this point to see if the BLE test could be made to succeed. In any case, it does not matter because the test firmware will soon be replaced. In the meantime, we have confirmation that the XIAO is alive.

Let's conclude this section with two remarks.


A Big Brother to the XIAO ESP32C3 toc

Being a member of the XIAO family, the ESP32C6 conforms to the XIAO 18x22 mm form factor shown on the right. There are 14 pads along the edges of the board, three of which are dedicated to power and ground connections and the remaining 11 are general-purpose input/output (I/O) pins. Four of these pins have no predefined functions, while the remaining seven are used to provide three serial peripherals: UART, I²C and SPI.

The ESP32-C6 microcontroller is the bigger sibling of the ESP32-C3, so I expected that the XIAO ESP32C6 would be very similar to the XIAO ESP32C3. That is not quite the case. Looking at the basic pin allocation of the two boards one can see differences. For one thing, the XIAO ESP32C6 has one less analog input pin which explains why A3 is not defined. Otherwise all symbolic names of the pins are the same, which, of course, corresponds to the XIAO form factor. However, the I/O pin assigned to each peripheral is totally different.

Note to self:
Always use the symbolic Arduino pin names when writing a sketch for a XIAO board. That way it will be simpler to port a sketch to another board in the XIAO series.

XIAO ESP32C6 - front XIAO ESP32C6 - back

Three other I/O pads are of interest to the user.

The I/O pins on the underside of the XIAO do not have Arduino names even if the XIAO ESP32C6 Pin List appears to assign such names to three of them.

There are significant differences between the boards that are not visible when just looking at the common configuration of all XIAO boards. The ESP32-C6 has a second core which is RISC-V just as the main core. However, it would be a mistake to think of the ESP32-C6 as a dual-core microcontroller in the same vein as the "classic" ESP32 and the Raspberry Pi RP-2040. The ESP32-C6 cores are very asymmetric with the main core running at 160 MHz and the second core running at a much slower 20 MHz. The second core called the ULP (presumably standing for ultra-low power) plays an important role in the nominal 15 µA current drain in deep sleep mode, where the single RISC-V core ESC32-C3 can only go down to 44 µA.

The asymmetry between the two RISC-V cores of the ESP32-C6 is not limited to their frequency. As I understand it, the ULP has access to only I/O pins 0 to 7 of the ESP32-C6. On the XIAO ESP32C6, this means that 7 of the 8 ULP I/O pins are available.

I/O pinULP pinULP peripheral
0 LP_GPIO0
1 LP_GPIO1
2 LP_GPIO2
4 LP_GPIO4LP_UART_RXD
5 LP_GPIO5LP_UART_TXD
6 LP_GPIO6LP_I2C_SDA
7 LP_GPIO7LP_I2C_SCL

Again, these symbolic names for the ULP pin and peripheral were taken from the XIAO ESP32C6 Pin List in the Seeed Studio getting started guide, but as far as I can make out, these are not defined in any files in the ESP32 Arduino core. For me at least, this does not matter very much because there is no easy way to write code for the ULP. One either has to write assembly code for the core (example: SmoothBlink_ULP_Code.ino) or use the ESP-IDF. This may change in the future; see Add support for Low Power core of the ESP32C6.

Pin Numbers and Names toc

This first sketch prints out the values defined in the variant pin definition file pins_arduino.h of the Arduino Espressif32 package found in the following directory: ~/.arduino15/packages/esp32/hardware/esp32/3.0.1/variants/XIAO_ESP32C6/. The source is sufficiently boring to not be printed here, but it can be found in the associated GitHub repository. Here is the serial output from the sketch.

XIAO ESP32C6 I/O Pin Names and Numbers The symbolic name and corresponding I/O number of the 11 digital pins D0 = 0 D1 = 1 D2 = 2 D3 = 21 D4 = 22 D5 = 23 D6 = 16 D7 = 17 D8 = 19 D9 = 20 D10 = 18 The symbolic name and corresponding I/O number of the 4 analogue pins A0 = 0 A1 = 1 A2 = 2 Note: Ax = Dx for x = 0, 1 and 2 The symbolic name and corresponding I/O number of the 7 serial pins TX = 16 [UART] (=D6) RX = 17 [UART] (=D7) SDA = 22 [I2C] (=D4) SCL = 23 [I2C] (=D5) SS = 21 [SPI] (=D3) MOSI = 18 [SPI] (=D10) MISO = 20 [SPI] (=D9) SCK = 19 [SPI] (=D8) Onboard yellow LED LED_BUILTIN = 15 BUILTIN_LED = 15 // backward compatibility Antenna WIFI_ENABLE = 3 (RF switch power enable I/O) WIFI_ANT_CONFIG = 14 (RF switch select control I/O) Other macros USB_VID = 0x2886 USB_PID = 0x0048 USB_MANUFACTURER = "Seeed Studio" USB_PROUCT = "XIAO ESP32-C6" USB_SERIAL = "" Board macro ARDUINO_XIAO_ESP32C6 defined

The Antenna I/O pin consts were added in version 3.0.4 of the ESP32 core (see pull request 10066 Enabled the onboard ceramic antenna by default when creating a new project with XIAO_ESP32C6) released on August 2, 2024. If the sketch is compiled in an older core, then the output will be different.

... Antenna RF switch I/O not defined in ESP32 3.0.3 RF switch power enable I/O = 3 (= WIFI_ENABLE in ESP32 core 3.0.4 or later) RF switch select control I/O = 14 (= WIFI_ANT_CONFIG in ESP32 core 3.0.4 or later ...

Some definitions and omissions are somewhat perplexing. The pin definition header file does not contain symbolic names for the pins on the back side of the board. One could have expected to find the following definitions in pins_arduino.h.

A4 = 4 A5 = 5 A6 = 6 MTMS = 4 MTDI = 5 MTCK = 6 MTDO = 7 BOOT = 9

Because these missing definitions can always be added when needed that omission is not very important. Also I do not know the purpose of the empty USB_SERIAL string macro but it's probably harmless enough. The USB_VID:USB_PID values do not match the USB ID of the boards I have tested so far which was 303A:1001.

Prior to the 3.0.2 version of the core, the ARDUINO_XIAO_ESP32C6 macro was not defined properly (see Issue 9929 where, amazingly I managed to repeat the error while supposedly pointing it out. Thanks to SuGlider for correcting the correction in the commit).

Blinky, Blinky toc

it's time to run a blink sketch, the microcontroller equivalent of the "Hello world!" program. The Seeed Studio Wiki already contains a blink sketch in Getting Started with Seeed Studio XIAO ESP32c6, but I have modified it slightly do that it blinks, it pulses and it responds to button presses. Let's start with the blink. First the I/O pin has to be defined and its mode needs to be set in the setup function.

static uint8_t ledPin = LED_BUILTIN; // yellow LED cathode connected to digital pin static uint8_t ledOn = LOW; // the LED anode is connected to 3.3V via 1.5K resistor void setup() { Serial.begin(); delay(1000); // 1 second delay should be sufficient for USB-CDC Serial.println("Starting setup"); Serial.println("Setting up LED output I/O pin"); // declaring LED pin as output pinMode(ledPin, OUTPUT); // turn it off digitalWrite(ledPin, 1-ledOn); ...

A static variable, ledPin, is used in the sketch instead of the predefined LED_BUILTIN variable. That way, the sketch could easily be adapted should we want to attach a LED to any one of the I/O pins on the XIAO. I know that the yellow LED will be turned on when I/O pin 15 is brought low because of the way the LED is connected as shown at the end of the XIAO ESP32C6 schematic. The asymmetric on and off times when the LED is blinking will confirm this. Here is the bit of code that takes care of blinking the LED.

#define ON_TIME 80 #define OFF_TIME 840 unsigned long beattime = 0; int beatcount = 0; int beatdelay = 0; /* beatcount 0 = turn on for short delay 1 = turn off for short delay 2 = turn on for short delay 3 = turn off for long delay */ void heartbeat(void) { if (millis() - beattime > beatdelay) { if ((beatcount & 1) == 1) // if beatcount odd turn LED off digitalWrite(ledPin, 1-ledOn); else // if beatcount event turn LED on digitalWrite(ledPin, ledOn); if (beatcount >= 3) { beatdelay = OFF_TIME; beatcount = 0; } else { beatdelay = ON_TIME; beatcount++; } beattime = millis(); } }

To understand this, remember that this routine is repeatedly called from the main loop() function. The LED is blinked in a heartbeat fashion: two short ON periods separated by a longer OFF period. The beatcount variable is used as a state variable. When beatcount is equal to 0 the LED is turned on for the first short on period. When beatcount is 1 the LED is turned off for a short time. When beatcount is 2 the LED is turned off for the second short period and finally when beatcount is 3 the LED is turned off for a long period. Short and long periods are defined with the ON_TIME and OFF_TIME macros and the variable beattime is used to store the time obtained from the millis() function whenever a period has elapsed at which point the value of beatcount is incremented. Finally, when beatcount is about to be set to 4, it is reset to 0 to continue the cycle indefinitely.

Well that's easy enough, so I had to complicate things and use the PWM (pulse width modulation) capabilities of the chip to make the LED repeatedly ramp up in intensity until it reaches a maximum and then decrease in intensity until off. It's a visually pleasing pulsating effect if the parameters have appropriate values.

#define DELTA 10 #define DELAY 50 unsigned long delaytime = 0; int delta = DELTA; int fade = 0; void pulse(void) { if (millis() - delaytime > DELAY) { fade += delta; if (fade <= 0) { fade = 0; delta = DELTA; } else if (fade >= 255) { fade = 255; delta = - DELTA; } analogWrite(ledPin, fade); delaytime = millis(); } }

When an I/O pin is outputting a PWM signal what it is actually doing is sending a square wave. In other words, the current flow through the pin is repeatedly turned on and off at a usually fairly high frequency so that, if an LED is connected to the signal, the eye cannot discern the on and off periods. Because of the persistence of vision, the visual effect is that the LED seems more or less bright depending on the proportion of on time to the total on and off time. That proportion is called the duty cycle. Instead of a percentage, the duty cycle is set by a value between 0 (no on time) to 255 (no off time). So a duty cycle of 85 corresponds to a 33% duty cycle; the LED is on only a third of the time and will not be very bright. In the above code, the DELTA macro sets by how much the duty cycle will be increased or decreased in each step, while DELAY is the time (ms) spent in each step. The variable fade is used to store the current duty cycle. The value of the variable delta will be added to fade at each step. To get the ramping up or down effect, the variable delta will be either equal to DELTA or equal to -DELTA. Again, pulse will be called repeatedly by the loop(). It uses the delaytime to calculate if DELAY milliseconds have elapsed. When that's the case, it moves on the next step. And that as simple as adding delta to fade and then testing if the maximum has been reached or the minimum value has been reached and changing the sign of delta at those points. The Arduino function analogWrite() takes care of setting up all the timers needed to create a PWM output.

In a previous incarnation of this sketch, the loop() alternated between calling heartbeat() or pulse() for 10 seconds. This time around, I set the period to 20 seconds and added the possibility to switch from one to the other with a button press. The boot button is used for that purpose even if it’s rather tiny and fiddly to use. The transition between blinking and pulsing is a bit fancier as the switchAction() function that takes care of switching between patterns, actually turns the LED off and resets the pattern about to be started to its initial state in the process. It also outputs a message to the console stating why the pattern changed. When writing this function, I got bitten by a little gotcha even though it was solved in the older version. Once an I/O pin has been put into PWM mode with a analogWrite() function, it remains in that mode even when after a digitalWrite(). To return the pin to digital read/write mode a call to pinMode(ledPin, OUTPUT) is required.

static uint8_t buttonPin = 9; // Boot button uint8_t buttonState = 0; // It should be high when read by setup() uint8_t buttonActive = 0; // void setup() { ... Serial.println("Setting up Boot button input I/O pin"); // declaring boot button pin as input pinMode(buttonPin, INPUT); // externally pulled high with 10K resistor buttonState = digitalRead(buttonPin); Serial.printf("Boot button default value: %d\n", buttonState); buttonActive = !buttonState;

In the setup() function the I/O pin is set in INPUT mode. I checked the schematic and verified that the pinned is pulled high by a 10K ohms resistor so that there was no need to enable an internal pull up resistor. It was not really necessary to check the schematic, I/O pin 9 is part of the so-called strapping pins and it must be high when the microcontroller is booted to enable normal operation. Again, just in case another I/O pin was to be used with a switch, the variable buttonState will be used to hold the current binary (HIGH or LOW) value read on the I/O pin and buttonActive will be set to the opposite of the initial value of the pin on the assumption that it is in its off (unpressed) position when the microcontroller boots. So in our case, the button will be deemed pressed when its value is equal to buttonActive , in other words, when digitalRead(buttonPin) return LOW

Finally, some necessary but not necessarily elegant switch debouncing is done within the loop() function itself (thank you Mackenzie King for that idiom). That avoids bringing in my button library.

Internal vs External Antenna toc

Version 3.0.4 of the ESP32 Arduino core undertook and hopefully finalized the handling of the antennas on the XIAO ESP32C6. By default, the RF (radio frequency) single pole double throw switch is enabled and the internal antenna is selected. The board does not detect if an external antenna is connected, so it must be selected explicitly. This would usually be done in the setup() function of a sketch with the following line of code.

digitalWrite(WIFI_ANT_CONFIG, HIGH); // LOW for internal antenna (default)

As we have seen, WIFI_ANT_CONFIG defined in pins_arduino.h, is the I/O pad connected to the switch control line to select the antenna. If writing code that might be run on other ESP32 boards, I would suggest using the following code or something equivalent.

#if defined(ARDUINO_XIAO_ESP32C6) // The onboard ceramic antenna is used by default. // Uncomment the following macro to use a connected external antenna. //#define USE_EXTERNAL_ANTENNA #else #undef USE_EXTERNAL_ANTENNA // just making sure #endif void setup() ... #if defined(USE_EXTERNAL_ANTENNA) digitalWrite(WIFI_ANT_CONFIG, HIGH); // LOW = internal antenna (default) #endif ...

The RF switch is not enabled (i.e., not powered) by default in the hardware but it was only in version 3.0.4 of the ESP32 core that the switch was enabled in software with the addition of a .../variants/XIAO_ESP32C6/variant.cpp file containing an initVariant() function (see the Enabled the onboard ceramic antenna by default when creating a new pr… commit for details.)

Recent history and rant

Even when it is not powered, there is enough leakage through the RF switch for the wireless signal to get to the microcontroller so that Wi-Fi, Bluetooth and Zigbee communication is possible if not optimal. That caused considerable confusion until version 3.0.4 enabled the switch by default in software. I must thank msujino who has explored the subject much more thoroughly than me and presented his results in the Seeed Studio forum topic XIAO_ESP32C6 Switching between builtin and external antenna. Thanks also to Alfonso Acosta (fons on the Seeed forum) for drawing my attention to the problem and for providing a link to that valuable discussion on the forum.

The justification for the addition of variant.cpp can be found in the Cincinnatu pull request

Due to the hardware design of XIAO_ESP32C6, the WIFI antenna will not be turned on by default during use. Some of our users may not know how to turn on the WIFI by themselves, therefore, so I have submitted this PR.

Blaming clueless users was disingenuous as the only documentation at the time was a hint in the Wiki.

TIP
There's an IO port 14 used to select between using the built-in antenna or an external antenna. If port 14 is at a low level, it uses the built-in antenna; if it's at a high level, it uses the external antenna. The default is low level. If you want to set it high, you can refer the code below.
void setup() { pinMode(14, OUTPUT); digitalWrite(14, HIGH);//use external antenna }

The suggested operation would not give the best result and there was no hint to that an RF switch had to be enabled. I would argue that the current TIP is almost as bad as the previous one.

TIP
GPIO14 is used to select between using the built-in antenna or an external antenna. Before that, you need to set GPIO3 low level to turn on this function. If GPIO14 is set low level, it uses the built-in antenna; if it set to high level, it uses the external antenna. Default is low level. If you want to set it high, you can refer the code below.
void setup() { pinMode(3, OUTPUT); digitalWrite(3, LOW);//turn on this function delay(100); pinMode(14, OUTPUT); digitalWrite(14, HIGH);//use external antenna }

It uses "magic numbers" instead of the WIFI_ENABLE and WIFI_ANT_CONFIG variables defined in the pins_arduino.h file and it unnecessarily reproduces part of the initVariant() code.

Perhaps the following is clearer.

TIP
The built-in ceramic antenna is enabled by default. To use a connected external antenna, add the following line of code, usually in the setup() function.
digitalWrite(WIFI_ANT_CONFIG, LOW); //use external antenna

For the sake of completeness another hint could be added, assuming that the premise is true.

TIP
Power consumption can be reduced by disabling the RF switch that selects the antenna; see the code below.
void setup() { pinMode(WIFI_ENABLE, INPUT); //disable the RF switch }

Be aware that the performance of the antenna will be degraded as a consequence.

The initial versions of all the sketches presented below that used wireless communication were flawed because they were based on version 3.0.2 of the ESP32 Arduino core and I was blissfully ignorant of the problem with the RF switch since all the devices were within a meter or two of each other. As of August 9, I believe that I have corrected that mistake in all the sketches. Since I wanted to maintain backward compatibility with version 3.0.2 of the ESP32 core, here is how I dealt with initializing the RF switch and selecting the internal or an external antenna.

#if defined(ARDUINO_XIAO_ESP32C6) // An onboard ceramic antenna is used by default, but an external antenna can be used instead // in which case uncomment the following macro definition. //#define USE_EXTERNAL_ANTENNA #else #undef USE_EXTERNAL_ANTENNA // just making sure #endif void setup() { ... #if defined(ARDUINO_XIAO_ESP32C6) #if (ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 0, 4)) // reproduce initVariant() from ESP32 v3.0.4 uint8_t WIFI_ENABLE = 3; uint8_t WIFI_ANT_CONFIG = 14; // enable the RF switch pinMode(WIFI_ENABLE, OUTPUT); digitalWrite(WIFI_ENABLE, LOW); // select the internal antenna pinMode(WIFI_ANT_CONFIG, OUTPUT); digitalWrite(WIFI_ANT_CONFIG, LOW); #endif // same code for ESP32 v3.0.2 and up #if defined(USE_EXTERNAL_ANTENNA) digitalWrite(WIFI_ANT_CONFIG, HIGH); #endif #endif ...

Essentially, the setup code contains the equivalent of the version 3.0.4 initVariant() function when an ESP32 Arduino core older than 3.0.4 is used. Frankly, I have not found a good reason to complicate things this way and I would suggest that everyone use the newest version of the ESP32 Arduino core.

So now we are in a position to truly verify the difference between the on-board ceramic antenna and an external rod antenna.

The RF switch is enabled and using the internal antenna. 4 networks found Nr | SSID | RSSI | CH | Encryption 1 | SIGMDELNET | -43 | 1 | WPA2 2 | netlan | -45 | 6 | WPA2 3 | netiot | -47 | 6 | WPA2 4 | OpenWrt | -84 | 1 | WPA2 The RF switch is enabled and using an external antenna. 7 networks found Nr | SSID | RSSI | CH | Encryption 1 | SIGMDELNET | -33 | 1 | WPA2 2 | netlan | -40 | 6 | WPA2 3 | netiot | -42 | 6 | WPA2 4 | OpenWrt | -81 | 1 | WPA2 5 | playteck | -88 | 6 | WPA2 6 | BELL975 | -90 | 11 | WPA2 7 | 1633 Guests | -94 | 11 | WPA2

When an RSSI [received signal stength indicator] value is represented in a negative form (e.g. -100), the closer the value is to 0, the stronger the received signal has been (Source). That makes sense as the RRSI values for each access point is further away from 0 the further it is from the XIAO. On that basis, the rod antenna captures measurably more of the electro-magnetic energy produced by the access point radios. And "that's a good thing" as Martha would say. Just how symmetric these antennas are when comparing their transmit and receive behaviour is not clear, but I think it's pretty safe to assume that the rod antenna will prove better when transmitting a signal than the ceramic antenna. I don't know anything about RF circuits, impedance matching, VSWR and all that so don't take my word for it, instead look at graph produced by msfujino. While it is not the purpose of the study done by msfujino, I think it additionally shows that the external antenna transmit more power than the internal antenna everything else being equal.

The source code is in the 03_scan_wifi directory of the xiao_esp32c6_sketches repository. The code has an additional macro #define DISABLE_RF_SWITCH to test the consequences of not powering the antenna selection switch. If you are intrigued by the counter-intuitive results that will obtain if the macro is defined, then you may want to look at the sketch in the 12_xiao32c6_antenna directory or just the raw data from running that sketch.

The Wi-Fi Blackhole - An Unexpected Quirk toc

I think an "unexpected quirk" is a superfluous, useless and inessential redundancy. Be that at it may, I ran into a problem using the wifi_tx_power to plot connect times against the transmit power of the WiFi radio. It would systematically show that the connection to the IP address was 0.0.0.0 when the connection to the Wi-Fi network was established. Adding a delay would fix that problem, but I was curious about it, so I wrote a pared down connection sketch to get an idea of the time delay involved. Here is a sample of results obtained with one of XIAO ESP32C6 using its internal antenna.

Attempting to connect to the Wi-Fi network WiFi is connected with IP address: 192.168.50.139 Time to connect: 456 ms Time to valid IP local address: 1239 ms Difference: 783 ms Attempting to connect to the Wi-Fi network of this change, WiFi is connected with IP address: 192.168.50.139 Time to connect: 409 ms Time to valid IP local address: 458 ms Difference: 49 ms Attempting to connect to the Wi-Fi network WiFi is connected with IP address: 192.168.50.139 Time to connect: 456 ms Time to valid IP local address: 518 ms Difference: 62 ms Attempting to connect to the Wi-Fi network WiFi is connected with IP address: 192.168.50.139 Time to connect: 406 ms Time to valid IP local address: 465 ms Difference: 59 ms

It can take anywhere from 49 to 783 milliseconds between when the Wi-Fi connection is said to be made and when a valid non 0.0.0.0 IP address is registered. The table below summarizes the similar results obtained with other ESP32 modules.

BoardModuledifferences
XIAO_ESP32C6ESP32-C6456, 783, 49, 63, 59...
XIAO_ESP32C3ESP32-C3652, 557, 572, 559, 560...
CRAP_SUPER_MINIESP32-C3827, 561, 558, 575, 638, 697...
LOLIN_S2_MINIESP32-S250, 643, 55, 650, 46...
LOLIN32_LITEEP3255, 68, 50, 54, 53, 560...
LILYGO_T_DISPLAYESP3253, 68, 77, 568, 38, 51...

CRAP_SUPER_MINI is actually one of the marginal Super Mini ESP32-C3 cards described elsewhere. It was necessary to set the transmit power to a lower value to get it to connect to the Wi-Fi network.

Clearly this delay has nothing to do with the ESP32-C6, it occurs in all ESP modules that I could test. Why have I never observed that before? Perhaps I just added a delay without thinking about it. Perhaps I am doing something wrong and a kind soul will put me straight.

Source code for the sketch is in the GitHub repository.

Connection Time vs Transmit Power toc

The table shows times needed to connect to a Wi-Fi network in milliseconds as a function of the radio TX power setting. The tests were run only twice on a XIAO ESP32C6, the first time using the internal antenna and the second time using the external FPC 1.2 antenna supplied with the XIAO ESP32C3 (something which should not have been done according to the Seeed KK Engineer Blog article XIAO Antenna.

PowerInternalexternalRelative diff
WIFI_POWER_default44039311.96 %
WIFI_POWER_19_5dBm4303968.59 %
WIFI_POWER_19dBm394437-9.84 %
WIFI_POWER_18_5dBm391430-9.07 %
WIFI_POWER_17dBm429432-0.69 %
WIFI_POWER_15dBm4293958.61 %
WIFI_POWER_13dBm3923881.03 %
WIFI_POWER_11dBm4003931.78 %
WIFI_POWER_8_5dBm396427-7.26 %
WIFI_POWER_7dBm4334320.23 %
WIFI_POWER_5dBm43238911.05 %
WIFI_POWER_2dBm3933891.03 %

Those tests were run in version 3.0.2 of the ESP32 Arduino core without enabling the RF switch, so it was necessary to rerun them. Because of reported connection issues (Wifi connection on xiao esp32c6 takes >10 minutes), I took care to test in a more representative environment. So instead of having the XIAO about one metre from the Wi-Fi router, another Wi-Fi access point was set up as far away as practical from the XIAO. It is the OpenWrt network in the previous test. The wireless signal had to travel about 15 metres in one direction and go through two walls. The RF switch was enabled and in this test a generic 2.4Ghz rod external antenna and a PCB antenna (tuned to 2.4158Ghz or 2.4153GHz I can't quite make out the hand-writing) were used instead of the FPC v1.2 antenna. The external antennas were oriented in two different directions. Here are the comparative results when measuring the signal strength (RSSI) from the OpenWrt Wi-Fi network and the time to connect to the latter in milliseconds.

Internal antenna vs external rod antenna – horizontal

RSSIConnection time(ms)
internalexternalvar.internalexternalvar.
WIFI_POWER_default-80-756.25 %497372-25.15 %
WIFI_POWER_19_5dBm-81-757.41 %3744068.56 %
WIFI_POWER_19dBm-85-7412.94 %3744037.75 %
WIFI_POWER_18_5dBm-86-7413.95 %413409-0.97 %
WIFI_POWER_17dBm-82-749.76 %4064070.25 %
WIFI_POWER_15dBm-81-810.00 %3733740.27 %
WIFI_POWER_13dBm-82-784.88 %378373-1.32 %
WIFI_POWER_11dBm-81-801.23 %3733730.00 %
WIFI_POWER_8_5dBm-83-803.61 %4064080.49 %
WIFI_POWER_7dBm-82-811.22 %413411-0.48 %
WIFI_POWER_5dBm-84-795.95 %377374-0.80 %
WIFI_POWER_2dBm-82-767.32 %385376-2.34 %
Average6.21 %-1.15 %

To interpret the data, consider the first line of data which corresponds to the default Wi-Fi transmit power as set when the XIAO boots up. The signal strength is greater when using the external antenna (an RSSI closer to 0 represents a stronger signal). The variation in the RSSI obtained with the two antennas is calculated as the increase in RSSI (5 = -75 - (-80)) over the value of the RSSI measured with the internal antenna which gives a 6.25% improvement. Similarly the variation in the connection time is the ratio of the time difference over the connection time (372 - 497)/497, so in this case the connection with the external antenna was an exceptional -25.15% faster.

Internal antenna vs external rod antenna – vertical

RSSIConnection time(ms)
internalexternalvar.internalexternalvar.
WIFI_POWER_default-80-765.00 %497422-15.09 %
WIFI_POWER_19_5dBm-81-766.17 %3743750.27 %
WIFI_POWER_19dBm-85-7610.59 %3743740.00 %
WIFI_POWER_18_5dBm-86-7512.79 %413374-9.44 %
WIFI_POWER_17dBm-82-793.66 %4064080.49 %
WIFI_POWER_15dBm-81-792.47 %3734089.38 %
WIFI_POWER_13dBm-82-784.88 %378374-1.06 %
WIFI_POWER_11dBm-81-774.94 %3733730.00 %
WIFI_POWER_8_5dBm-83-786.02 %406377-7.14 %
WIFI_POWER_7dBm-82-784.88 %413405-1.94 %
WIFI_POWER_5dBm-84-787.14 %3774088.22 %
WIFI_POWER_2dBm-82-767.32 %385373-3.12 %
Average6.32 %-1.62 %

Internal antenna vs external PCB antenna – horizontal

RSSIConnection time(ms)
internalexternalvar.internalexternalvar.
WIFI_POWER_default-80-782.50 %497403-18.91 %
WIFI_POWER_19_5dBm-81-792.47 %3743760.53 %
WIFI_POWER_19dBm-85-797.06 %374373-0.27 %
WIFI_POWER_18_5dBm-86-798.14 %413374-9.44 %
WIFI_POWER_17dBm-82-802.44 %406405-0.25 %
WIFI_POWER_15dBm-81-792.47 %3734109.92 %
WIFI_POWER_13dBm-82-776.10 %37844317.20 %
WIFI_POWER_11dBm-81-783.70 %3733740.27 %
WIFI_POWER_8_5dBm-83-786.02 %406374-7.88 %
WIFI_POWER_7dBm-82-793.66 %413408-1.21 %
WIFI_POWER_5dBm-84-795.95 %3774057.43 %
WIFI_POWER_2dBm-82-784.88 %385373-3.12 %
Average4.62 %-0.48 %

Internal antenna vs external PCB antenna – vertical

RSSIConnection time(ms)
internalexternalvar.internalexternalvar.
WIFI_POWER_default-80-7111.25 %4975337.24 %
WIFI_POWER_19_5dBm-81-7112.35 %3744089.09 %
WIFI_POWER_19dBm-85-7116.47 %37441611.23 %
WIFI_POWER_18_5dBm-86-7117.44 %4134160.73 %
WIFI_POWER_17dBm-82-7212.20 %406374-7.88 %
WIFI_POWER_15dBm-81-7112.35 %3734068.85 %
WIFI_POWER_13dBm-82-7212.20 %378374-1.06 %
WIFI_POWER_11dBm-81-7211.11 %3733750.54 %
WIFI_POWER_8_5dBm-83-7213.25 %406374-7.88 %
WIFI_POWER_7dBm-82-7212.20 %4134150.48 %
WIFI_POWER_5dBm-84-7214.29 %3774098.49 %
WIFI_POWER_2dBm-82-7212.20 %3854178.31 %
Average13.11 %3.18 %

The good news is that the XIAO was able to connect to the Wi-Fi network no matter the transmit power setting and no matter which antenna was used. This more or less confirms that the rather poor result with the Super Mini ESP32-C3 had more to do with the particular manufacturer's implementation of the antenna coupling circuit than with the nature of the ceramic antenna. On average the XIAO ESP32C6 needed only 1 to 2% longer to connect with its on-board antenna than with the external rod antenna. That's a negligible difference. The 6% signal strength gains when using the rod antenna is more meaningful. The PCB antenna has the potential for attaining an appreciably better wireless connection but its orientation is more critical.

Of course, the source code is available in the 05_wifi_tx_power directory.

Web Controlled LED toc

The wireless connectivity of the ESP32-C6 is very appealing for those interested in home automation. I ported a simple sketch for controlling an LED with a web page first presented in the overview of the XIAO ESP32C3. Unfortunately, the sketch would not compile in the Arduino IDE with the AsyncTCP and ESPAsyncWebServer libraries that can be installed with the Arduino library manager. Luckily, the newest version from me-no-dev on GitHub solves this problem. Minimalist copies of these libraries are installed in a private library directory. The details about that can be found in a README file of the xiao_esp32c6_sketches repository.

The only changes made to the original code were to set the TITLE at compile time using the ARDUINO_XIAO_ESP32Cx (x = 3 or 6) macros and to handle the LED since there is none on the XIAO ESP32C3 except for the charge LED. As before the XIAO ESP32C6 onboard LED is toggled on and off by clicking on a button in a Web page.

XIAO ESP32C3 Web Server Index Page

This is just a skeleton sketch which does not contain important properties of a Web interface for an IoT device. For example, there is no update of the interface should the LED state be changed from another Web client. There is no local control via a push button or switch to control the LED and so on. The series of posts beginning with A Wi-Fi Switch for Domoticz using a XIAO ESP32C3 goes into details on how these things and many more could be done. It should be possible to adapt it without too much struggle to run on the XIAO ESP32C6.

Bluetooth Controlled LED toc

Given my limited knowledge of Bluetooth, be it Low or High Energy, it was loath to use the BLE library contained in the ESP32 Arduino core. Thankfully, there are numerous examples to be found, some of which were useful for a neophyte.

Starting with konsaibakudan's simple clean, implementation, I had to complicate things and, dare I say, fix some issues. I added many print statements to follow along in the serial monitor as the BLE peripheral/server is set up. Remember, I am trying to learn this stuff. After that, I looked at the services and characteristics UUID.

First here is a quick word about characteristics and services. A characteristic is a data value transferred between Bluetooth devices. Logically linked characteristics are grouped into services which correspond to a function. A Health Thermometer service might include a temperature value characteristic, a time interval between measurement characteristic, and so on. The Bluetooth Special Interest Group has a set of predefined UUIDs for services and characteristics. There is an advantage to use such predefined UUIDs in a project when possible. Client apps such as nRF Connect from Nordic Semiconductor or LighBlue from Punch Through Design, will display the service name instead of the incomprehensible UUID. I have encountered some old examples that used reserved UUID that had nothing to do with the project and found that a bit disconcerting, so it would be better to choose carefully among the predefined UUIDs. An additional benefit of using an assigned UUID is that it can be shortened to 16 bits. So there are a few suggested predefined service and characteristic UUIDs at the top of the sketch as well as randomly generated valid custom UUIDS.

#ifdef USE_CUSTOM_UUIDS #define SERVICE_UUID "57a81fc3-3c5f-4d29-80e7-8b074e34888c" #define CHARACTERISTIC_UUID "2eeae074-8955-47f7-9470-73f85112974f" #else #define SERVICE_UUID "1815" //"00001815-0000-1000-8000-00805F9B34FB" // Automation IO Service //"1812" //"00001812-0000-1000-8000-00805F9B34FB" // Human Interface Device Service //"181c" //"0000181c-0000-1000-8000-00805F9B34FB" // User Data Service #define CHARACTERISTIC_UUID "2BE2" //"00002BE2-0000-1000-8000-00805F9B34FB" // Light Output //"2BO5" //"00002B05-0000-1000-8000-00805F9B34FB" // Power #endif

The two screen captures with nRF Connect illustrate the advantage of using valid preassigned UUIDs instead of custom UUIDs.

The custom UUID were generated with the Online GUID / UUID Generator following Mohammad Afaneh advice in How do I choose a UUID for my custom services and characteristics?. Also look at BLE UUIDs by Dan Editor. Links to a list of predefined UUID numbers can be found in Assigned Numbers by the Bluetooth SIG. Nordic also maintains a Bluetooth Numbers Database which is presumably wider in scope.

Once I had my version of ESP32LEDBluetoothcode.ino working well, I committed it to the repository before making a final change. Instead of polling the server to read any change in the LED characteristics to update the state of the LED, a callback function is defined as found in the other example sketches mentioned above.

Oh the Zigbee toc

Zigbee is a low-power, low-data-rate, and close proximity wireless ad hoc network based on the IEEE 802.15.4 standard. Four years ago a Zigbee button was added to the home automation system. Because it worked very well, we have installed additional buttons to mitigate the awkward placement of wall switches and also a few water leak detectors. Consequently, I was very much hoping that the XIAO ESP32C6 would become a platform on which I could build some Zigbee devices. So far that goal has proved elusive, but it’s early and there's still have hope.

The good news it that I did get the example sketches Zigbee_Light_Switch.ino and Zigbee_Light_Bulb.ino running on two XIAO ESP32C6 such that it was possible to turn on and off the LED of the XIAO running the bulb sketch by pressing the Boot button of the other XIAO running the switch sketch. That did require slightly modifying the bulb sketch in two respects. The first is that the original sketch assumed that an RGB LED was connected to the board whereas the XIAO has only a yellow LED available. The other modification proved a bit more tedious to track down. No matter the distance between the two devices, the Zigbee bulb would not connect to the Zigbee switch. The following debug message was continuously output as successive connection attempts failed: esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL).

As is often the case, a solution was found in a repository issue, in this case the esp-zigbee-ske repository. In a reply about the ESP32C6 having a problem pairing, xieqinan suggested that a weak link between the end device and the coordinator could be the culprit. When the Link Quality Indicator is below a threshold, the end device will not attempt to connect. Lowering that threshold could be the solution. He suggested 32 as the threshold. So following xieqinan's instructions, I implemented that fix.

#define LQI_THRESHOLD 32 static void esp_zb_task(void *pvParameters) { esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG(); esp_zb_init(&zb_nwk_cfg); esp_zb_on_off_light_cfg_t light_cfg = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG(); esp_zb_ep_list_t *esp_zb_on_off_light_ep = esp_zb_on_off_light_ep_create(HA_ESP_LIGHT_ENDPOINT, &light_cfg); esp_zb_device_register(esp_zb_on_off_light_ep); esp_zb_core_action_handler_register(zb_action_handler); esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); #if defined(LQI_THRESHOLD) esp_zb_secur_network_min_join_lqi_set(LQI_THRESHOLD); #endif ...

That worked, the XIAO could reliably pair and there was no problem controlling the LED on one XIAO remotely using the boot button of the XIAO albeit from a small distance. Actually, no problem except with fiddling with the tiny boot button. But that's OK, this is just a proof of concept.

In retrospect (August 6), the true cause for the low link quality was disabled RF switch. Once that is that's fixed, preliminary tests show that if both the Zigbee switch and Zigbee bulb sketches are run on XIAO ESP32C6, each using its internal antenna, Zigbee communication operates in a range of a metre or two. However, at the further edge of that range, the time between a button press and the toggling of the state of the LED is noticeably longer than when the two devices are near each other. It is probably an indication that the Zigbee messages need to be retransmitted. When a rod antenna is used with the XIAO running the bulb sketch, range increases to 10 metres bring no discernable degradation in the Zigbee communication. This is not saying that 10 metres is a limit, it was merely the maximum distance that could be achieved in my work space. Even if no longer needed, the LQI_THRESHOLD macro has been left in the code, just in case the problem occurs in some less than optimal circumstances.

This peer-to-peer network is not what many want. It would be truly useful if the Switch or Bulb sketch could be modified to connect to a Zigbee coordinator such as Zigbee2MQTT or Zigbee2Tasmota. That remains elusive at least in the Arduino context. Much work is being done in that direction using the esp-zigbee-sdk; see for example the Zigbee supporting Esp32 module with Arduino in the Hubitat community forum and Making a working zigbee light with an ESP32-C6. That said, the XIA0 ESP32C6 does connect to the Zigbee coordinator. This is the debug output to the serial monitor of the Zigbee_Light_Bulb sketch as it finally paired with Zigbee2MQTT.

XIAO About Tab in Z2M
[ 930][I][Zigbee_Light_Bulb.ino:130] esp_zb_app_signal_handler(): ZDO signal: ZDO Config Ready (0x17), status: ESP_FAIL [ 931][I][Zigbee_Light_Bulb.ino:98] esp_zb_app_signal_handler(): Zigbee stack initialized [ 933][I][Zigbee_Light_Bulb.ino:104] esp_zb_app_signal_handler(): Device started up in factory-reset mode [ 934][I][Zigbee_Light_Bulb.ino:106] esp_zb_app_signal_handler(): Start network formation [ 3160][I][Zigbee_Light_Bulb.ino:126] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL) [ 6387][I][Zigbee_Light_Bulb.ino:126] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL) [ 9614][I][Zigbee_Light_Bulb.ino:126] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL) [ 12841][I][Zigbee_Light_Bulb.ino:126] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL) [ 16068][I][Zigbee_Light_Bulb.ino:126] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL) [ 19960][I][Zigbee_Light_Bulb.ino:120] esp_zb_app_signal_handler(): Joined network successfully (Extended PAN ID: dd:dd:dd:dd:dd:dd:dd:dd, PAN ID: 0x1a62, Channel:11, Short Address: 0x99c8)

And here is the Zigbee2MQTT log stating that the XIAO was paired but that it is unsupported.

info 2024-07-06 19:12:19 Successfully interviewed '0x543204fffe11c5dc', device has successfully been paired warning 2024-07-06 19:12:19 Device '0x543204fffe11c5dc' with Zigbee model 'undefined' and manufacturer name 'undefined' is NOT supported, please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html

Of course, the fact that the new Zigbee device is not supported by Zigbee2MQTT is not too surprising. All the good Zigbee cluster definitions need to be added and a manufacturer name and a device model must be defined. Over a year ago, swkim01 showed how he did that in the esp-zigbee-sdk esp_HA_customized_light example. But that is already outdated as xieqinan added basic manufacturer information to examples only 3 days ago (2024-07-03). There's every reason to believe that these examples will make their way to the ESP32 Arduino core, when mere mortals will be able to play with their new ESP32 Zigbee gadget.

Unfortunately, pairing with Zigbee2Tasmota was not successful.

19:24:53.416 -> [155863][I][Zigbee_Light_Bulb.ino:138] esp_zb_app_signal_handler(): ZDO signal: ZDO Leave (0x3), status: ESP_OK 19:24:53.416 -> [155864][I][Zigbee_Light_Bulb.ino:134] esp_zb_app_signal_handler(): Network steering was not successful (status: ESP_FAIL)

It does seem that the "Tasmotized" Sonoff ZBBridge does receive the Zigbee messages from the XIAO, but basically ignores them. It is hard to be absolutely sure about this, as the bridge is in use and it receives many messages from zigbee devices about the house.

Let's end this section with a reminder that parameters need to be set manually in the Tools menu when compiling these projects. The README files for the original Light Bulb and Light Switch sketches list these. If one prefers a screen capture showing the settings then look at the Readme files for modified zigbee-bulb and zigbee-switch sketches. Be careful, the settings are not the same in all respects.


Deep Sleep toc

The ESP32-C6 has two major power saving modes (light sleep and deep sleep) which in turn can be modified in "sub sleep modes". Furthermore, there are several wake up sources which can be combined to bring the controller out of sleep mode. Truth be told, the sleep modes on the ESP32-C6 are rather complex and I will have to slog my way through the ESP32-C6 technical reference manual, the Sleep Modes API documentation and probably the Power Management API documentation and as many examples as possible to make sense of this.

The Deep sleep mode and wake-up examples in the Seeed Studio Getting started guide are simplifications of those found in the ESP32 Arduino core and unfortunately neither worked when I tried them. The consensus of those who responded to pleas for help on this topic was to remove all use of the Serial peripheral. Actually, this is not necessary as will be reported at the end of this section. Initially, I settled on a BASIC example by PJ Glasso published in April on the Seeed Forum as the basis for my example sketches without any output to the serial monitor. Instead all information was transmitted via the onboard LED.

Each time the board wakes up from deep sleep, it behaves just as if it had been restarted. In other words, it executes the setup() function. So the first thing the function does is ensure that the I/O pin attached to the LED is in output mode. Then it increments the boot count which is stored in non-volatile real time clock memory. It then slowly blinks the LED, counting out the value of the boot count variable. After a short 5 seconds delay, there is a quick sequence of flashes announcing that the microcontroller is going into deep sleep mode for a period of 15 seconds. When it wakes up the process repeats, but, of course, the boot count will be one greater than before.

// 15 seccond sleep period in microseconds #define SLEEP_PERIOD 15000000 const int ledPin = BUILTIN_LED; const int ledOn = LOW; // Counter for number restarts stored in non-volatile memory RTC_DATA_ATTR int bootCount = 0; void blink(int count=1, int ms=50) { for (int i=0; i<count; i++) { digitalWrite(ledPin, ledOn); delay(ms); digitalWrite(ledPin, 1-ledOn); delay(ms); } } void setup() { // Set builtin led pin as an output pinMode(ledPin, OUTPUT); // Increment boot count bootCount++; // Display bootcount with slow LED blinks blink(bootCount, 750); // Delay for 5 seconds delay(5000); // Quick LED blinks announcing start of deep sleep blink(5); ust what t // Configure deep sleep wake-up timer esp_sleep_enable_timer_wakeup(SLEEP_PERIOD); // Enter deep sleep esp_deep_sleep_start(); } void loop() { // This odd flashing pattern should never be seen because the // device is put in deep sleep in setup() before loop() starts. blink(4, 50); delay(125); blink(2, 100); delay(2000); }

In this example, a timer wakes the board at regular intervals. This could be the basis for a device that reads a sensor and then sends out the data in some manner such as Wi-Fi, Bluetooth, Zigbee or perhaps even via a serial interface such as I²C. Presumably, it could be a battery powered because the board would be in deep sleep mode most of the time. The more important average power consumption of such a device can only be known by testing it because the 15 µA figure is a theoretical minimum for the ESP32-C6 by itself only. Some are investigating this, msfujino in Comparison of Sleep Currents for XIAO ESP32C6, S3, and C3 for example.

Instead of data logging at regular intervals, it would be useful to put the XIAO in deep sleep mode to be wakened only when a monitored event occurs. The following example shows how that could be done with a push button that can momentarily connect an I/O pin to ground. The sketch that does this is almost identical to the previous one. The only significant difference in the code is setting up the wake source.

// In principle D0, D1 or D2 (I/O pins 0, 1, 2) can be // grounded to wake the board from deep sleep. const int wakeUpPin = 0; // On board yellow LED const int ledPin = BUILTIN_LED; const int ledOn = LOW; // Counter for number restarts stored in non-volatile memory RTC_DATA_ATTR int bootCount = 0; void blink(int count=1, int ms=50) { for (int i=0; i<count; i++) { digitalWrite(ledPin, ledOn); delay(ms); digitalWrite(ledPin, 1-ledOn); delay(ms); } } void setup() { // Set wake up pin as an input pinMode(wakeUpPin, INPUT_PULLUP); // Set builtin led pin as an output pinMode(ledPin, OUTPUT); // Increment boot count bootCount++; // Display bootcount with slow LED blinks blink(bootCount, 750); // Delay for 5 seconds delay(5000); // Configure the I/O wake-up source if (esp_sleep_enable_ext1_wakeup(1 << wakeUpPin, ESP_EXT1_WAKEUP_ANY_LOW) == ESP_OK) { // Quick LED blinks announcing start of deep sleep blink(5); // Enter deep sleep esp_deep_sleep_start(); } else { Serial.begin(); while (true) { delay(2000); Serial.println("Cannot go into deep sleep mode"); } } }

Grouding D0It was rather lucky that this sketch worked. It was also a bit unfortunate because it would not work with D1 or D2 as the wake pin nor would it work when inverting the polarity of the wake connection. I was searching for an explanation in the wrong places, until I read a note about esp_sleep_enable_ext1_wakeup in the esp_sleep.h source code.

* @note Internal pullups and pulldowns don't work when RTC peripherals are * shut down. In this case, external resistors need to be added. * Alternatively, RTC peripherals (and pullups/pulldowns) may be * kept enabled using esp_sleep_pd_config function...

Grouding D2 On reading that, I left the internal pullup in place, supplemented it with an external 47K ohms pull up resistor and tested that pin D2 could reliably wake up the board when it was in deep sleep mode. Further tests proved that D0 and D1 would work reliably and that it was possible to bring the board out of deep sleep with a high signal using any of those three pins. Of course it is necessary to change the (redundant) internal and external pullup into a pull down and to change the wake up mode from ESP_EXT1_WAKEUP_ANY_LOW to ESP_EXT1_WAKEUP_ANY_HIGH. One could also use any one of I/O pads 4, 5, 6 or 7 on the underside of the XIAO to wake the processor. Trying to use any other available I/O pin on the XIAO will cause an error since only I/O pins 0 to 7 are mapped to RTC I/O pins.

The external pull up (or down) resistor will increase the current draw. Consequently, I will look into the use of the esp_sleep_pd_config function. However at this point, I am happy to have working skeleton sketches for the deep sleep mode of the ESP32-C6.

Using Serial

The Serial peripheral can in fact be used as long as it is properly closed down just before entering deep sleep. For example, these are the last four Serial methods invoked just before deep sleep is started with esp_deep_sleep_start() in the deep_sleep_io sketch.

Serial.println("Closing Serial peripheral and going into deep sleep after LED flashes."); Serial.printf("Ground I/O pin D%d to wake from the deep sleep.\n", wakeUpPin); Serial.flush(); Serial.end();

And here is the output to the serial monitor as the XIAO was woken from deep sleep at 22:38 and then about four minutes later at 22:42.

... 22:38:05.748 -> Deep Sleep I/O Example 22:38:05.748 -> Wakeup caused by external signal using RTC_CNTL 22:38:05.748 -> Boot count: 7 22:38:21.277 -> Closing Serial peripheral and going into deep sleep after LED flashes. 22:38:21.277 -> Ground I/O pin D0 to wake from the deep sleep. 22:42:34.901 -> 22:42:34.901 -> Deep Sleep I/O Example 22:42:34.901 -> Wakeup caused by external signal using RTC_CNTL 22:42:34.901 -> Boot count: 8 ...

If the macro called USE_SERIAL is defined at the start of the sketches, the Serial peripheral will be used and serial monitor messages such as those shown above will supplement the LED flashes to display the state of the ESP32C6. By default the macro is not defined.

As for the actual current draw when the device is in deep sleep, it is in the micro-ampere region. I don't have equipment nor the know-how for making such measurements. However there is a very good discussion by msfujino on the Seeed Studio forum: Comparison of Sleep Currents for XIAO ESP32C6, S3, and C3 with the following results.

DeviceSleep current (µA)Battery life (days/years)
XIAO ESP32S311.95,602 / 15.3
XIAO ESP32C614.34,662 / 12.8
XIAO ESP32C345.11,478 / 4.1

That last column is a bit of a tongue-in-cheek addition. The time for a 2Ah battery to be discharged down to 2O% by a XIAO that is always in deep sleep mode was obtained with the online calculator: simple calculator for estimating a (LiPo) battery's life.

Again, the source code for these examples is available for download on the associated GitHub repository.

One final note while on this topic. Once the ESP32-C6 is in deep sleep mode, it is not possible to upload a new version of the firmware to the device. It will be necessary to put the board in bootloader mode.

Resources toc

Rather sparse for now, but more links may be added later.

<-First Look at the Seeed Studio XIAO ESP32C3