2024-11-09
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.

That introduction was written in June. It is now early November and the major change to the project is that it is now PlatformIO compatible thanks to pioarduino a fork of the Platformio Espressif 32 development platform for PlatformIO by Jason2866. It is based on version 3.x of the Espressif Arduino core which supports ESP32-C6 SoC and other newer devices.

Table of Content

  1. Disclosure
  2. A Big Brother to the XIAO ESP32C3
  3. First Connection
  4. Initially for the Arduino IDE Only
  5. Now Available in PlatformIO Also
    1. Using the Stable Branch of pioarduino
    2. Which Branch and Fork?
    3. About the New Board Definition
  6. Pin Numbers and Names
  7. Blinky, Blinky
  8. Internal vs External Antenna
  9. The Wi-Fi Blackhole - An Unexpected Quirk
  10. Connection Time vs Transmit Power
  11. Web Controlled LED
  12. Bluetooth Controlled LED
  13. Oh... the Zigbee
  14. Deep Sleep
  15. 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.

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. All symbolic names associated with the edge pads are the same as they conform to the XIAO form factor. However, the I/O pin assigned to each peripheral is totally different. There is a lesson in that: 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.

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. The most obvious visual difference is the antenna. The new board has an on-board ceramic antenna for wireless connections, but there is a U.FL coaxial connector for an optional external antenna. The boot and reset buttons have been moved from the bottom of the board to the top, which is not significant. However they are now much smaller, domed metal, tactile switches which are noticeably harder to activate.

There are significant differences between the boards that are not visible. 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.

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@M7:~$ 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:44:33:22:11 [62903.285417] cdc_acm 3-14:1.0: ttyACM0: USB ACM device ...

Use the CtrlC keyboard combination to close that application. Or if the board is already connected to the computer, try the following command michel@M7:~$ dmesg | grep -E 'usb|cdc_acm' | tail -n 7. Alternatively, list, using the long format, all serial devices.

michel@M7:~$ ls -l /dev/serial/by-id | grep -i espressif lrwxrwxrwx 1 root root 13 nov 8 17:50 usb-Espressif_USB_JTAG_serial_debug_unit_54:32:44:33:22:11-if00 -> ../../ttyACM0

A copious amount of information can be obtained with the sudo lsusb -v -d 303a:1001 where the device ID was obtained with the . 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@M7:~$ sudo lsusb -v -d 303a:1001 | grep iSerial iSerial 3 54:32:44:33:22:11

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

After this long introduction, it's finally time to look at the serial output from the XIAO ESP32C6 which arrived with preloaded firmware. While in the past cu (actually opencu) was my preferred terminal emulation program, I now use picocom because it has a simple mapping feature to take care of the end of line encoding.

michel@M7:~$ picocom -q --imap lfcrlf /dev/ttyACM0 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 ... Ctrl A Ctrl X michel@M7:~$

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.


Initially, Arduino IDE Only toc

Initially, only the Arduino IDE (available here) supported the newer ESP32-C6 SoC. Here is how to configure the IDE.

  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.
    Additional Boards Manager URLs window
    There can be more than one board manager as can be seen.
  2. Install the newest version of the esp32 by Espressif Systems in the BOARDS MANAGER.
    Install Board Manager
    If there are too many board managers, enter esp32 in the search box at the top to shrink the list. Version 3.0.2 or newer is needed; as of Nov. 6 version 3.0.7 is available.
  3. Select the XIAO_ESP32C6 board and its port
    Plug in the board to a USB port on the computer.
    Click on the board combo box on the action bar where the IDE is already showing some board. A drop-down list of possible boards will be displayed. The first choice is almost correct here, but the actual board is not defined properly. So click on Select other board and port....
    Start typing xiao or esp32c6 in the search box. The XIAO_ESP32C6 should show up in the list. Presumably the correct serial port will be shown on the right. select both these elements and click on OK.

The Arduino IDE does have some annoying quirks. The Copy-paste data from the serial monitor problem has been around for some time. Copying the serial output is much easier with picocom. Be careful, picocom will not work if the serial monitor of the Arduino IDE has control of the serial port connected to the XIAO.

Now Available in PlatformIO Also toc

The Seeed Studio Wiki has a tutorial on using the Xiao ESP32-C6 on PlatformIO. However the approach describe below may be easier for some.

The ESP32 platform (platform-espressif32 version 6.9.0 dated September 26, 2024) from PlatformIO is based on version 2.0.17 of the Arduino core for the ESP32 by Espressif which does not support the ESP32-C6. Initially, I thought it was just a matter of time before platform-espressif32 would be migrated to version 3.0. Given that PIO is developed in Ukraine which faces unbearable hardship, patience seemed to be in order.

While I only found out about it in the last week, Jason2866 announced the creation of the pioarduino fork of platform-espressif32 on July 16th. He wrote [since] it is now clear that there will be no official support for Arduino core 3.0.x from Platformio team, [...] i decided to fork the needed repo(s) and build a community version to support core 3.0.x (starting with core 3.0.3). I can't pretend to understand the responsibilities and actions of the Espressif and PlatformIO developers in this matter. Consequently, I will not comment on the situation. However, PIO is my preferred development environment for embedded devices (even if version 2.x of the Arduino IDE was a major improvement) so I am most grateful for Jason2866's work which made it possible to add PlatformIO support to the xiao_esp32c6_sketches project on GitHub on November 5th.

Let's be clear; I am still using PlatformIO in VSCode (actually, in VSCodium, but that's a more complicated story). What has changed is the "integration" of the Arduino core into PlatformIO. I am no longer using platform-espressif32 maintained by the PlatformIO team, instead I am using the pioarduino fork by Jason2866. In practice all that's required is to change one line in the platformio.ini configuration file of projects that use an ESP32. Where the configuration file contained the following line

platform = espressif

now it contains this line

platform = https://github.com/pioarduino/platform-espressif32.git#develop

As a complete example, here is the configuration file for the 01_pin_names project.

[platformio] ; Make the Arduino IDE happy (.INO file must be in a directory of the same name) src_dir = pin_names [env:seeed_xiao_esp32c6] platform = https://github.com/pioarduino/platform-espressif32.git#develop board = seeed_xiao_esp32c6 framework = arduino monitor_speed = 460800 ;upload_port = /dev/ttyACM0 ;monitor_port = /dev/ttyACM0

Basically that's it, don't bother trying to install the platform, just compile the project. PlatformIO will take care of downloading the development branch of the pioarduino platform and will install everything. Because of that, the first compilation will take longer. Later compilations will be much faster because the platform will not need to be installed again. Indeed once the first project is compiled, one can check which platforms are installed in the system.

That seems simple enough right? Yes, but there are some complications.

Using the Stable Branch of pioarduino toc

My instinct is to avoid development branches if at all possible. Thankfully, the GitHub documentation has the following instructions.

Stable version espressif Arduino 3.0.7 and IDF 5.1.4+ [env:stable] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip board = ... ...

The stable branch of the platform was used all the examples sketches except for the first one. Currently there is a problem because the XIAO ESP32C6 board definition was not available when the stable version was released. So it was necessary to add the board definition locally and add an entry in the platformio.ini configuration file with the path to the board definition.

[platformio] ; Make the Arduino IDE happy (.INO file must be in a directory of the same name) src_dir = blink_pulse_led boards_dir = ../boards [env:seeed_xiao_esp32c6] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip board = seeed_xiao_esp32c6 framework = arduino monitor_speed = 460800 ;upload_port = /dev/ttyACM0 ;monitor_port = /dev/ttyACM0

The following tree shows where the board definition file was added and explains why the project relative board_dir path was up one level.

├── boards │   └── seeed_xiao_esp32c6.json ├── 01_pin_names │   ├── pin_names │   │   ├── main.cpp │   │   └── pin_names.ino │   └── platformio.ini ├── 02_blink_pulse_led │   ├── blink_pulse_led │   │   ├── blink_pulse_led.ino │   │   └── main.cpp │   └── platformio.ini ├── 03_scan_wifi ...

The board definition file and the boards_dir entry in platformio.ini will probably not be needed in the next release of the stable version. And this brings us to the next point.

Which Branch and Fork? toc

After adding support for PlatformIO and compiling every example sketch to be presented below with PIO, I ended up with three Espressif 32 platforms.

That made sense because the development branch of the pioarduino fork was used in the first sketch and then the stable branch was used in all the other sketches. The original espressif32 platform from the PlatformIO was still present. Notice how it is version 6.7.0, which is not the most recent. I clicked on the Updates button near the top of the window, and the IDE reported that no platform needed updating. That was a bit perplexing since version 6.9.0 is available. Next I compiled a PIO project for an ESP32 (first generation) which had platform: espressif32 in its configuration file. Looking carefully at the output during compilation it transpired that pioarduino/platform-espressif32.git#develop was used. Since platform: espressif32 is a generic specification which PIO interprets as meaning using the latest version of the platform, it is understandable that it used the develop branch of the pioarduino fork. After all the PIO documentation is quite clear on this.

Example of using a Espressif 32 development platform: [env:latest_version] ; not recommended as it does not ensure that ; - builds are repeatable ; - all developers who checkout the project wil build against the same platform version platform = platformio/espressif32 Note We highly recommend pinning the platform to a version. See Version Requirements for details.

Clearly, I should never have used the ambiguous platform: espressif32 configuration. In my defence, it never caused problems in the past. Now, more care is needed. And here is one reason.

26 commits behind pio/platform-espressif32:develop

The PlatformIO team continues to develop the "official" platform-espressif32 integration and the pioarduino fork is 26 commits behind (2024-11-09). It may be that among these, there are some corrections or additions that are needed in a project. In those cases, it may be necessary to go back to the official platform. However, the point is moot for ESP32C6 based boards since the official platform does not support them at all. I hasten to add that this is in no way a criticism of the pioarduino fork. For months now Jason2866 has been doing yeoman service almost entirely on his own with his up-to-date version of platform-espressif32. He would probably appreciate some help in keeping up with the official repository.

There's work ahead for me: going through all my ESP32 projects and testing them against his pioarduino platform to see if there's a problem or not and pinning the platform to the appropriate version as recommended.

About the New Board Definition toc

The first attempt to build one of the projects in PlatformIO with the pioarduino fork failed because the latter did not contain a definition for the Seeed XIAO ESP32C6. The official platform-espressif32 boards directory did not contain a json definition file for any ESP32-C6 board as far as I could see. So I compared the XIAO_ESP32C3 and XIAO_ESP32C6 definitions in the boards.txt in the espressif/arduino-esp32 core. Since the only significant difference seemed to be about the JTAG, I decided to use the seeed_xiao_esp32c3.json" as a starting point. I substituted "C6" for "C3", and got rid of the "ldscrpt": "esp32c6_out.ld" key-value pair because I could not find the file. I then fixed up the definition by comparing it with two definitions for ESP32C6 based boards (adafruit_feather_esp32c6.json and sparkfun_esp32c6_thing_plus.json) found in the pioarduino boards directory. I stuck the resultant seeed_xiao_esp32c6.json definition in the boards directory of the clone on my computer and then found that every example sketch would compile and run. "Brilliant" I thought. That was not a comment on my prowess but a compliment to both the PlatformIO team and Jason2866. My contribution is fa cut-and-paste job more on the level of a parakeet repeating random phrases that are not understood.

So I promptly forked pioarduino, added the board definition and did a pull request (PR) essentially asking that Jason2866 include seeed_xiao_esp32c6 in the boards directory. Then panic set in when I got the following notification about six failed integration tests.

failed builds

Quite an average, 100% as a matter of fact, because in my first ever PR, I managed to make the very mistake that I was attempting to correct. Luckily, the reviewer corrected that before committing the change. In this second case, I was surprised that Jason2866 merged the change a few hours later, I was expecting to be told that I had not done my homework... which to be honest was the truth as I could not figure out how to run any tests. As for the failed runs, none seem to have anything to do with the board definition.

The real reason for this nostalgic recounting of the events of the last week is that I want to apologize to Lucas Katayama. He had already proposed a seeed_xiao_esp32c6.json file on October 4th. However that file is languishing as an open PR in the original platformio/platform-espressif32. The fact that the PR remains open explains why it was not in the repository. I had not thought to search open pull requests when I found nothing in the boards directory. It is unfortunate that other useful contributions to the official ESP32 platform may be lost because their PR will never be merged unless there's a change of heart and the PlatformIO team moves on to the current Espressif Arduino core. Many may not be aware of Jason2866's fork, like Luca and I back in October. Be that as it may, I want to make it clear that I should have credited Lucas who was, as far as I know, the first to create a board definition file for the Seeed XIAO 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 pin 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