2022-06-14
md
Overview of the SAMD21 Arm Cortex-M0+ Based Seeed Studio XIAO
I²C Light Sensor using a Seeeduino XIAO-> <-First Look at the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO
Seeeduino XIAO Serial Communication Interfaces (SERCOM)->

The Seeed Studio XIAO SAMD21 (formerly known as the Seeeduino XIAO) arrived in my mail box on March 18, 2020. About two weeks later, I published the first version of this post. After making some corrections in the following month, the post remained unchanged until the start of 2022 when I started correcting some parts of the post in view of recent developments and things learned. While adding more and more corrections I became dissatisfied with the original post and decided that a major rewrite was in order. So this is an opportunity to clarify some things and to look at some topics not covered before such as the capacitance touch inputs and the hardware watchdog.

This post is not for the neophyte that has never used the Arduino framework. If you are in that situation, welcome to the fascinating world of microcontroller enthusiasts, and let me suggest that you start with the Seeed Studio XIAO Starter Kit Courses or the original Seeeduino XIAO Free Course by Seeed Studio. Once you have worked through the course, you may want to return to this page if you are curious about some particular topics.

The author of this post is a hobbyist who has tinkered with classic Atmel AVR, STM32 and ESP32 microcontrollers and has somewhat more experience with ESP8266 based development boards. That background obviously tainted the first encounter with the SAM D21 microcontroller found on the Seeed Studio board and it explains some references to the ESP. There were mistakes in the first version of this overview, but welcomed help from readers, notably D. J. Park, made it possible to correct some of the more egregious errors. No doubt, old errors remain while new ones were introduced in this new version. Corrections and suggestions are heartily sought; there is an email link at the bottom of the page for that purpose. Questions adressed to me are answered as best a "non expert" can.

Apologies for the length of this introduction, but that is in keeping with the length of this page which is one of the longest on this site where long posts abound. Hopefully, the table of content will be of use for those looking for a particular topic.

Table of contents

  1. The XIAO Form Factor
    1. Hardware Accessories
    2. What's in a name?
  2. The SAM D21 Microcontroller
  3. Meet Some Seeed Studio XIAO SAMD21 Competitors
  4. Web Resources
  5. Development Environments
  6. Uploading/Bootloader/COM Port/Reset Problem
  7. Only a Blink Sketch, But Do Read Me
  8. A Proper setup() Function
  9. XIAO SAMD21 Input Output Pins
  10. Analogue Input and Output
    1. Analogue Input Calibration
    2. Analogue Measure of Light with an LDR
    3. Capacitance Touch Sensors
    4. Analogue Output
  11. Digital Input and Output
    1. Digital Output
    2. Pulse Width Modulation
    3. Digital Input
  12. Interrupts
  13. Timers
  14. Watchdog
  15. Serial Communication
    1. UART
    2. SPI
    3. I²C

The XIAO Form Factor toc

When SeeedStudio announced the XIAO SAMD21 in late 2019, it was actually launching the first of a family of boards that share a common form factor. Aside from a common size, all the members of the family share a common general pin assignment and a full speed (12Mbsp) USB 2.0 interface with a USB-C type connector. The first four boards in the family have a 32-bit RISC ARM Cortex-M processor from three different chip makers. The latest board is based on the Risc-V architecture. Each member of the complete family is described in the Seeed Studio XIAO Series Web page which also contains a very clear and detailed comparison table. There is another comparison table that is worth looking at.

XIAO (小 in Chinese) means small, as Paul Battley kindly informed me in a private correspondence. There is no doubt that the boards are very small (22x18 mm). Given their tiny size, the two breadboard-compatible headers along the long sides of the board contain only 7 pins each. Three of these are for ground and power (3.3 and 5 volts) while the other 11 pins are I/O pins. All boards share a common Arduino pin numbering and serial connections. By default they all offer one UART, one I²C, and one SPI channel (see the figure below). It is possible to reassign these serial connections in various ways depending on the microprocessor used.

XIAO form factor - front XIAO form factor - backt

XIAO ModelArchitecture
Seeeduino XIAOARM Cortex M0+link
Seeed XIAO RP2040Dual-core ARM Cortex M0+link
Seeed XIAO BLE nRF52840ARM Cortex M4F (M4 with FPU)link
Seeed XIAO BLE nRF52840 SenseARM Cortex M4F (M4 with FPU)link

All boards have pads on the underside. The number and layout of these is not common although the ARM based boards appear to implement the Serial Wire Debug (SWD) interface in similar fashion (not verified). Instead of the SWD interface, the Risc-V XIAO has pads for the JTAG debug interface on the underside.

Hardware Accessories toc

When purchasing the XIAO SAMD21, it is possible to add three hardware accessories:

I have not tested any of these products, so what I am about to say is based on reading the product descriptions and may not be accurate. First of all, I believe the two carrier boards were developed for the XIAO SAMD21 and I am confused about the compatibility of these with other members of the XIAO series. That's because pogo pins are used to connect to some of the pads on the underside of the XIAO, notably the SWD interface. It is unlikely that the debug interface would work on the XIAO ESP32C3 because it uses the JTAG standard instead of SWD. Even my first generation XIAO SAMD21 boards could have problems because they do not have the GND and RST pads just below the SWD pads. So before purchasing any of these accessories, make sure it will meet your expectations.

What's in a name? toc

A couple of months ago, Seeed Studio posted the following note: product names of the XIAO series might not be consistent enough, which is why we’ve changed the product names of all XIAO series products [so that] they have one unified, consistent name (source). Here is part of the table.

Original NameNew Name
Seeeduino XIAOSeeed Studio XIAO SAMD21
Seeed XIAO RP2040Seeed Studio XIAO RP2040
Seeed XIAO BLE - nRF52840Seeed Studio XIAO nRF52840
Seeed XIAO BLE Sense - nRF52840Seeed Studio XIAO nRF52840 Sense
XIAO WiFi/BLE - ESP32C3Seeed Studio XIAO ESP32C3

The logic behind the names makes sense.

This new naming scheme is a good idea, as others have been introducing their own names such as XIAO M0 for the Seeeduino XIAO XIAO SAMD21. However, there is no going back for many libraries and packages that have been available for a long time already. Programming environments continue to identify the SAMD21 XIAO as the Seeeduino XIAO for example.

There will be no other mention of the newer members of the XIAO family in this post; XIAO will refer to the SAM D21 based board from now on.

The SAM D21 Microcontroller toc

The Atmel (now Microchip) SAM D21 is by no means a new microcontroller. It was introduced in 2012 as a low power and high performance 32-bit alternative to the mostly 8 bit devices used for IoT applications at the time. The high performance is relative, the core cannot perform floating point arithmetic in hardware. The same applies to 32-bit integer division and 32-bit integer multiplication with 64 bit results. Nevertheless, the M0+ core is a significant step up from AVR type microcontrollers.

ATTINY85-20MU ATMEGA328P-MUR ATSAMD21G18A-MU
Core Size (bits) 8 32
Speed (MHz) 20 48
Flash Memory (K bytes) 8 32 256
RAM (K bytes) 0.5 2 32
I/O 6 23 38
ADC (number/bits) 4/10 8/10 14/12
DAC (number/bits) 0 1/10
Connectivity I²C, SPI, UART I²C, SPI, UART, LIN, USB
Unit price ($US) 0.95 2.08 3.00

Prices were obtained from Digikey on March 20, 2020. At just under $5.00 from Seeed Studio, the XIAO looks like a bargain. I very much doubt one could purchase the CPU, an external crystal, a voltage regulator, four LEDs, various capacitors and resistors, metal can and a USB-C connector to make a similar board any cheaper. You could try, the schematic and more technical information is available in the Hardware Development Kit.

Meet Some Seeed Studio XIAO SAMD21 Competitors toc

The Seeeduino XIAO is not the first board based on the SAM D21 microprocessor. The major players in the hobbyist world have numerous boards in production:

Arduino,
See the Zero, MKR Zero and MKR 1000.
Adafruit, and
See the METRO M0, Feather M0 Basic Proto and the many other Feather devices based on the same processor.
Sparkfun
See the Redboard Turbo, SAMD21 Dev Breakout and SAMD21 Mini Breakout.

However the most direct competitor is the Adafruit QT Py. It has the same form factor as the XIAO, uses almost the same microcontroller (ATSAMD21E18 vs the ATSAMD21G18 on the XIAO), and adds some additional features such as an onboard Stemma QT connector for the I²C bus and solder pads on the back side for an optional SPI flash chip. The SWD interface is made available as two pads on the back side of the board just like the XIAO, but their location appears to be different.

There are others. Freescale (an offshoot of Motorola but now owned by NXP which is itself an offshoot of Phillips) may have been one of the first to offer boards based on the SAM D21. PjRC and avdweb produce small boards based on the same processor. Even Seeed Studio has at least four other SAM D21 based boards on offer (such as the Seeeduino Lotus Cortex-M0+ and Seeeduino LoRaWAN).

Web Resources toc

Information about the SAMD21 microcontroller, XIAO development board and other boards of the same type is accumulating rapidly on the Internet. There is now much more than can be read by any one person. So here is a partial list of sources that I have found useful.

General Information about the ARM architecture and the SAM D21 microcontroller. These references are quite technical and probably best avoided by those first starting out.

Information by SeeedStudio The information provided by the manufacturer is voluminous in comparison to others although most of it is from third parties. The main problem that I have is that it is dispersed and hence rather difficult to find.

Given the number of boards available, there is good information on the Web about the SAM D21. Adafruit has a tutorial Adafruit Feather M0 Basic Proto, Wanna play with the ARM Cortex M0 chipset? that is useful for XIAO users, particularly the section entitled Adapting Sketches to M0 & M4. The same is true at Sparkfun: RedBoard Turbo Hookup Guide and SAMD21 Mini/Dev Breakout Hookup Guide. The avdweb site by Albert van Dalen also has very useful information although it may be a little difficult to find. I use the search facility to find something I remember having seen previously.

A search of GitHub for "SEEED XIAO" will produce some interesting projects along with chaff. The search criteria "xiao m0" will yield the repository of sketches that accompanies this post as well as other collections of examples. The DroneBot Workshop published a Meet the Seeeduino XIAO blog post along with a YouTube video just yesterday (Dec 1, 2020). The author Bill (William) is a fellow Canadian born in Montréal as I was. Back to the topic, I have a number of additional posts about the XIAO SAMD21:

The increasing availability of good information about the XIAO and the SAM D21 microcontroller and other topics is a primary motivator for this second edition of the post.

Development Environments toc

Purists might want to try to program the XIAO using the Microchip (Atmel) Studio 7 IDE. There is copious documentation on the Microchip site although most appears to be based on the in-house development boards. This IDE is complex and I suspect that the learning curve is rather steep. There is another catch, at least for me, Studio 7 only runs in Windows.

Most readers of this post will probably choose to work with the XIAO in the Arduino framework. The original Arduino core for the SAM D21/D51 microcontrollers is available on GitHub. However it does not contain the board definition for the XIAO. Instead a fork by Seeed Studio must be used. When first introduced, only the Arduino Software (i.e. the Arduino integrated development environment or Arduino IDE) supported the XIAO, but it did not take long before PlatformIO supported the board as well.

There is another framework that can be used to program the XIAO in PlatformIO. Called Zephyr, an "OS [that] is based on a small-footprint kernel designed for use on resource-constrained and embedded systems: from simple embedded environmental sensors and LED wearables to sophisticated embedded controllers, smart watches, and IoT wireless applications." It does not appear to be as user-friendly as the Arduino framework, but that is just an uninformed judgment because I have absolutely no experience with Zephyr. Have a look at the source of its "blinky" sample program to get a little taste.

The programming language in the previous development frameworks is C/C++. Some, especially those that write programs for the Raspberry Pi, may prefer Python. There is a SAM D21 implementation of MicroPython which is largely compatible with Python 3. It is not entirely clear to me how well the XIAO is supported when looking at a recent entry in the forum. Adafruit has created CircuitPython which is a fork of MicroPython that tries to make the language easier to learn. Unfortunately, Adafruit only supports its own boards but it looks as if the XIAO is supported. Read Installing Circuit Python on Seeeduino Xiao. Andy Warburton makes it look simple to set up and use. MicroPyhton and CircuitPython bring to mind the ESP8266 NodeMCU firmware development framework, only Python is used instead of Lua. I love Python, it takes me back to my first and only computer course which I quickly dropped. I just hated off-by-one column errors in punched cards. Yes, I am that old.

Unfortunately, I do not have any experience with any frameworks other than the Arduino core. Consequently, the sample code, sketches, or projects (the last two terms borrowed from the Arduino patois and PlatformIO jargon respectively) that follow must be compiled with the SAM D21/D51 Arduino core from Seeed Studio which is the only one that has the XIAO board definition. As far as possible, I have provided these examples in a format that can be used within the Arduino IDE or PlatformIO. In all there are 20 projects which can be obtained from the xiao_m0_sketches repository on GitHub.

For those that start digging into the source code of the SAMD microcontroller Arduino core, here is a hierarchy of the principal versions. It’s only a very small sample of the 644 forks on GitHub at this point.

    arduino/ArduinoCore-samd (Release 1.8.13  2022/02/23)
    │
    ├── adafruit/ArduinoCore-samd (Release 1.7.9 2022/02/05)
    │     │
    │     ├── LynnL4/ArduinoCore-samd (-)
    │     │     │
    │     │     ├── Seeed-Studio/ArduinoCore-samd (Release 1.8.3 2022/05/24)

Currently, the Arduino IDE uses version 1.8.3 of the Seeed Studio Arduino Core. PlatformIO uses version 8.1.0 of Atmel SAM Arduino framework which is based on version 1.8.1 of the Seeed Studio Arduino core. Discrepancies experienced when compiling a project in these two IDEs could be because PlatformIO is typically based on an older core.

Except for the lag in updates in the PlatformIO IDE, it usually does not matter which IDE is used, it's mostly a matter of personal preference. Neophytes should probably begin with the Arduino IDE. It must be added that the now officially released Arduino IDE 2.0 may prove to be all that is needed by many. See the Arduino IDE 2 Tutorials. For the time being, I will continue to use PlatformIO on VSCodium because it seems that the Arduino IDE does not yet implement a project-based configuration file. That is quite useful for those that play with many different boards.

Uploading/Bootloader/COM Port/Reset Problem toc

Those who, like me, worked mostly with ESP8266 based boards such as the nodeMCU and the Lolin D1 mini will have a few teething problems on starting to use the XIAO or other SAM D21/D51 boards. To be honest, uploading a new sketch to the XIAO is usually just as simple as with the D1 mini: plug the board to the computer with a USB cable, and click on the upload icon of the IDE. But sometimes, there is a problem. The sidebar below contains links to a few of the many reports of difficulties encountered when uploading to the XIAO.

From the SeeedStudio forum.

This is not a XIAO problem per se, here is a link about to the Adafruit Feather M0 and ItsyBitsy's, another SAM D21 board, with many questions along the same line.

The avdweb site also contains a discussion of the problem

One of the answers in the Adafruit FAQ provides a hint as to the cause of the problem.

UNO-type Arduinos have a separate serial port chip (aka "FTDI chip" or "Prolific PL2303" etc etc) which handles all serial port capability separately than the main chip. This way if the main chip fails, you can always use the COM port.

M0 and 32u4-based Arduinos do not have a seperate (sic) chip, instead the main processor performs this task for you. It allows for a lower cost, higher power setup...but requires a little more effort since you will need to 'kick' into the bootloader manually once in a while.

The first image shows a typical ESP8266 development board using a USB to 3.3 volts TTY converter (here the CH340G, but it is often the CP1202) which sits between the USB connector and the hardware serial port of the microcontroller. As the next image shows there is no USB-TTY converter on the XIAO, the USB data signals from the USB-C socket are connected directly to two SAM D21 microcontroller I/O pins (PA25 and PA24). The USB signals are processed by a software USB stack which in effect replaces the hardware serial-USB converter. Consequently each time the XIAO is reset, any USB connection between the device and a computer will be lost which explains why the COM port (in Windows or device in Linux and MAC OS) disappears. A programming error could "trash" the USB stack or to put the microcontroller in some sort of tight loop so that the USB connection is lost. That should never happen with the ESP8266 board since the USB connection is between the computer and the CH370G and its own "hardware USB stack" which is independent of the ESP and will stay connected to the desktop even when the microcontroller is crashing.

Microcontrollers run in one of two modes: normal mode in which the loaded firmware is run and bootloader mode which makes it possible to change the firmware. On the ESP8266, the mode depends on the state of input/output pins when it boots. When an ESP is in bootloader mode, it expects to receive the new firmware over its hardware serial port on pins TX0 and RX0. These operations are quite different with the SAM D21. When the chip is powered up or when it is rebooted by grounding and then releasing the reset signal, the chip operates in normal mode and executes whatever firmware has already been uploaded. To put the SAM D21 in bootloader mode, the reset signal has to be asserted twice in quick succession. The microcontroller then appears as a USB storage device named Arduino in my file manager as can be seen below.

Arduino mass storage device in the file manager

The firmware is the file named CURRENT.UF2. Uploading a new firmware amounts to replacing that file with a new version. I will not go into details about that file because I know nothing about it, but hardy souls can consult the definitive document about the USB Flashing Format (UF2).

By the way, the date of the files is meaningless. It can be changed, but it will revert to the default 2018-12-24 20:00:00 on the next reboot. The size of CURRENT.UF2 does change as a function of the size of the firmware.

XIAO RST pad There is no reset button on the XIAO, only a couple of pads beside the USB connector. To mimic closing a button, the RST pad must be shorted to ground. This could be done with metal tweezers or a male-to-male Dupont wire or any other conductor. Some use shorted 0.1" header pins. It is not the most practical arrangement and if the board is in awkward position it is easy to short RST to ground twice when wanting to do it only once to reset the board. Luckily, the yellow (orange/amber) LED labelled L on the XIAO reveals its current mode.

Grounding of RST padModeYellow LED
OnceNormal
(boots from firmware in flash memory)
Flashes on once and then remains off unless the firmware turns it on later
TwiceBootloader
(board appears as a USB storage device)
Continously pulses (cycles between dimming and brightening phases)

The IDE (Arduino or PlatformIO) shields us very well from the details of uploading new firmware to the microcontroller. But if put in verbose mode, we can see what is going on behind the veil. Here is part of the output in the Arduino IDE.

Redémarrage forcé en cours en 1200bps ouvert/fermé sur le port /dev/ttyACM0 PORTS {/dev/ttyACM0, } / {/dev/ttyACM0, } => {} ... Uploading using selected port: /dev/ttyACM0 /home/michel/.local/bin/arduino-1.8.19/portable/packages/Seeeduino/tools/bossac/1.7.0-arduino3/bossac -i -d --port=ttyACM0 -U true -i -e -w -v /tmp/arduino_build_576183/startup.ino.bin -R

and the similar sequence of commands in PlatformIO.

Auto-detected: /dev/ttyACM0 Forcing reset using 1200bps open/close on port /dev/ttyACM0 Waiting for the new upload port... bossac --info --debug --port "ttyACM0" --write --verify --reset --erase -U true .pio/build/seeed_xiao/firmware.bin

Note that the -R (or --reset) option means that once the firmware is uploaded, bossac will reset the board which will then start to execute the new firmware. There is no need to "power cycle" the XIAO after uploading new firmware à la ESP8266.

BOSSA created by ShumaTech is a flash programming utility for [the] SAM family of flash-based ARM microcontrollers [by Microchip (formerly Atmel) such as the SAM D21]. The motivation behind BOSSA is to create a simple, easy-to-use, open source utility to replace Atmel's SAM-BA software. BOSSA is an acronym for Basic Open Source SAM-BA Application. The bossac utility is a command-line version of the program. Version 1.9.1 was released in August 2018 but both IDEs use version 1.7.0 that is two years older. I just had to try using the utility. The -h command line parameter displays the other command line parameters from which one learns that the -i (--info) command line parameter will identify the SAM device. However I had no luck.

michel@hp:~/.platformio/packages/tool-bossac$ ./bossac -i -p /dev/ttyACM0 No device found on /dev/ttyACM0

But it did work after putting the XIAO in bootloader mode. Once the yellow LED was pulsing the following information was displayed using the utility.

michel@hp:~/.platformio/packages/tool-bossac$ ./bossac -i Atmel SMART device 0x10010005 found Device : ATSAMD21G18A Chip ID : 10010005 Version : v1.1 [Arduino:XYZ] Nov 27 2019 16:35:59 Address : 8192 Pages : 3968 Page Size : 64 bytes Total Size : 248KB Planes : 1 Lock Regions : 16 Locked : none Security : false Boot Flash : true BOD : true BOR : true Arduino : FAST_CHIP_ERASE Arduino : FAST_MULTI_PAGE_WRITE Arduino : CAN_CHECKSUM_MEMORY_BUFFER

Clearly bossac can only work with a SAM device that is in bootloader mode. The Forcing reset using 1200bps open/close on port /dev/ttyACM0 reveals how the IDEs put the chip in that mode. Just opening the Arduino IDE serial monitor with a 1200 baud does indeed put the XIAO in bootloader mode.

The good news it that bossac will usually manage to upload new firmware to the XIAO which has been put in bootloader mode with the double grounding of the RST button. Be patient, because the IDE will go through its Forcing reset... routine before calling on bossac to package and upload the new binary file. The next two sections are long-winded discussions of other aspects of the SAM D21 microcontroller that have an impact on the ease of uploading the firmware.

Only a Blink Sketch, But Do Read Me toc

The traditional "Blink" sketch must be just about the simplest program that does something visible: flashing a light-emitting diode (LED) on and off. The XIAO has four LEDs, three of which are connected to the microcontroller pins. Note that it is necessary to write a 0 (LOW) to the pin to turn the corresponding LED on, This is contrary to the typical Arduino convention. Here is a simple sketch that flashes the yellow user LED.

#include <Arduino.h> // Use an asymmetric delay to confirm that the LEDs are active low. // Change the delay values to confirm that a new version of the firmware has been uploaded #define SHORT_DELAY 25 // 1/40 second on #define LONG_DELAY 1000 // 1 second off void setup() { pinMode(LED_BUILTIN, OUTPUT); // yellow LED that will be flashed } void loop() { digitalWrite(LED_BUILTIN, LOW); // turn a LED on delay(SHORT_DELAY); // wait digitalWrite(LED_BUILTIN, HIGH); // turn the LED off delay(LONG_DELAY); // wait } // repeat the loop

Upload the sketch to the XIAO following the instructions of the IDE used. In both Arduino and PlatformiIO, this amounts to connecting the XIAO to the desktop or portable computer with a USB cable, ensuring that the IDE knows which communication port to use and then clicking on the appropriate upload icon in the IDE. This is explained in great detail in the Lesson 1 of the Seeeduino XIAO in Action book. My sketch is almost an exact copy of blink. ino found in the lesson which, in turn, looks to be a copy of the classic Arduino example. However do note how the ON and OFF times differ in length which helps confirm that the LED is turned on when the I/O pin is set to LOW = 0 volts contrary to what the lesson says. While I am nitpicking, the serial port name of the XIAO on my Linux system is /dev/ttyACM0. Both IDEs seem to spot the correct port so that should not be a problem.

Those two little errors are not why I strongly recommended reading this section. Let's modify the sketch slightly by adding a loop counter, n, which will be incremented each time the loop() is executed. And let's add two print statements that write out the state of the user LED to the serial monitor. Because this is done over the USB connection, the USB activity LEDs would be distracting, so the I/O pins connected to these two blue LEDs are put into INPUT mode which, in effect, turns the LEDs off.

#include <Arduino.h> // Use an asymmetric delay to confirm that the LEDs are active low. // Change the delay values to confirm that a new version of the firmware has been uploaded #define SHORT_DELAY 25 // 1/40 second on #define LONG_DELAY 1000 // 1 second off void setup() { pinMode(LED_BUILTIN, OUTPUT); // yellow LED that will be flashed pinMode(PIN_LED_TXL, INPUT); // turn off the USB activity pinMode(PIN_LED_RXL, INPUT); // LEDS to better see the yellow LED } int n = 0; void loop() { n++; // increment the loop counter digitalWrite(LED_BUILTIN, LOW); // turn a LED on Serial.printf("%d: ON... ", n); // say so on the serial monitor delay(SHORT_DELAY); // wait digitalWrite(LED_BUILTIN, HIGH); // turn the LED off Serial.println("off."); // say it is off and terminate the line delay(LONG_DELAY); // wait } // repeat the loop
Source: 01_blink in xiao_m0_sketches on GitHub.

Here is part of the output as printed on the serial monitor:

10: ON... off. 11: ON... off. 12: ON... off. 13: ON... off. 14: ON... off. 15: ON... off. 16: ON... off. 17: ON... off. 18: ON... off.

That result is startling if, like me, your microcontroller experience was limited to classic Arduino and ESP8266 boards. How can the Serial object be used when the serial port was not initialized with a Serial.begin(9600) statement? The answer is that Serial in the SAM D21 Arduino framework is not an instance of a Uart class controlling a hardware universal asynchronous receiver-transmitter. Instead Serial is an alias for USBSerial which is a pseudo-serial instance wrapping a USB communications class device (USB CDC class, also called the universal serial bus class). Of course! we already documented that in the previous section: the USB data signals from the USB connector are connected directly to two I/O pins of the SAM D21 microcontroller.

As an experiment reboot the XIAO while it is running the sketch and look at the output in the serial monitor. Do not disconnect the USB cable, just reset the board by shorting the RST pad, making sure to short RST only once.

38: ON... off. 39: ON... off. 40: ON... off. 41: ON... off. 3: ON... off. 4: ON... off. 5: ON... off. 6: ON... off. 7: ON... off.

This is best done in the Arduino IDE because the monitor will automatically reconnect as soon as possible while, in my experience, most often it is necessary to manually reconect to the XIAO in PlatformIO.It's obvious that the board was rebooted after running for about 41 seconds. Why did it take a couple of seconds before the messages from the board started to show up in the serial monitor and where did the first two messages go? The answer has to do with the universal serial bus (USB).

I am old enough to recall the arrival of USB which was seen as a quite an advancement. Here was a true plug and play (self-configuring) hot swappable interface that meant that it was no longer necessary to locate elusive manuals and fiddle with tiny dip switches or, even worse, to find the correct configuration program for a card or device being added to a personal computer. We didn't even have to shut off the computer. When a USB device (the XIAO) is connected to a USB host (the computer), an interrupt will be raised on the latter indicating to the USB stack (i.e. the USB software package on the computer) that there is a new device. The stack will begin to query the XIAO to find out what its capabilities are. This is the USB enumeration process and it is not a trivial task. Shyamal Varma wrote a primer on the subject. No matter what the details are, in the end the computer will determine that the device class of the XIAO is a USB communications device class. The D-Bus service will then create a serial device (usually /dev/ttyACM0 on my machine as stated before) connected to the USB port. When the IDE serial monitor sees that the serial device is open, it will open it and start printing the data received from /dev/ttyACM0 which are the Serial.print statements in our sketch sent by the XIAO. The USB enumeration, the creation of a device by D-Bus, opening that device by the IDE serial monitor, all this takes time; about two seconds on my system. The first two messages just ended up in the big bit bucket in the sky until the connection was made.

That process explains why it was not necessary to initialize the Serial object. The sketch does not create a UART and initialize it, USBSerial is created in the Arduino startup code that we never see that runs even before the setup() function is reached. The communication channel with the computer is created and destroyed automatically when the physical connection between the device and host is established without any manual intervention needed on our part.

Before going on to expound on what I think the setup() function should be, I must insist on the fact that I don't know what I am talking about. My little scenario makes sense to me but it is only a heuristic tool which hopefully is "close enough." There's an open invitation to anyone that feels something better should be said to send a message via the link at the bottom of the page.

A Proper setup() Function toc

The setup() function of many of my sketches contains Serial.print() statements documenting the process as variables and objects are initialized, pin modes are set and so on. It's a simple debugging aid when creating a new sketch and it can be quite helpful for those of us that do not have very expensive in-circuit emulators (ICE) or debugging probes. Clearly, some care is needed when doing that with the XIAO. If Serial.print() is used too quickly after the microcontroller boots, then the information will be lost. If the setup() function waits until a USB connection is established, then the microcontroller will never execute its program unless it is tethered to a USB host which will rarely be the case in practice. The solution is a timeout for the USB connection. Here is a sketch, startup, which provides some timing values.

#include <Arduino.h> #define STARTUP_DELAY 8 // 8 seconds unsigned long setupstart; // time to reach setup() in microseconds unsigned long startserial; // time for USBSerial to come up in microseconds unsigned long connecttimer; // time for USB connection bool serialstart; bool serialbegin; bool connected = false; // Assume no USB connection established void report(void) { Serial.println("\n--SAM D21 XIAO Startup Timing--"); Serial.printf("setup() reached in %u microseconds (%.1f milliseconds).\n", setupstart, setupstart/1000.0); Serial.printf("(Serial) = %s %u microseconds (%.1f milliseconds) later.\n", (serialstart) ? "true" : "false", startserial, startserial/1000.0); Serial.printf("(Serial) = %s after Serial.begin(3214)\n", (serialbegin) ? "true" : "false"); if (connected) Serial.printf("USB connected %u milliseconds (%.1f seconds) after USBSerial up. Baud: %d.\n", connecttimer, connecttimer/1000.0, Serial.baud()); else Serial.printf("No USB connection made after %d seconds.\n", STARTUP_DELAY); } void setup() { Serial.println("\nsetup() - 1"); // Wait up to 10 seconds for Serial (= USBSerial) port to come up. // Usual wait is 0.5 second in Debian 10 running on I7-4770 @ 3.4 GHz setupstart = micros(); // time to reach the setup() routine startserial = millis(); // use startserial as a millis() counter while (!Serial && (millis() - startserial < 10000)); serialstart = (Serial); // true if timeout not reached startserial = micros() - setupstart; // time for Serial to come up in microseconds Serial.println("\nsetup() - 2"); // will not be seen // Serial is up but the USB connection is not yet completed, // Waiting for a USB connection Serial.begin(3214); // baud is meaningless serialbegin = (Serial); // reset to false Serial.println("\nsetup() - 3"); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); // Wait for USB connection connecttimer = millis(); while (!connected && (millis() - connecttimer < STARTUP_DELAY*1000)) { connected = (Serial); } connecttimer = millis() - connecttimer; if (connected) { Serial.println("\nUSB connection made."); } digitalWrite(LED_BUILTIN, HIGH); } void loop() { Serial.println("\nloop() started"); for (int i=10; i>0; i--) { report(); Serial.printf("\nSystem reset in %d seconds\n", i); delay(1000); } // reset the microcontroller NVIC_SystemReset(); }
Source: 02_startup in xiao_m0_sketches on GitHub.

For a first test, I used the Arduino IDE, connecting the XIAO board and starting the serial monitor setting the baud at 115200, 230400, 2500000 or even 9600 if wanted. Then the sketch was compiled and uploaded. This is the output displayed on the monitor. As can be seen, I chose 115200 out of habit.

13:35:47.344 -> USB connection made. 13:35:47.344 -> 13:35:47.344 -> loop() started 13:35:47.344 -> 13:35:47.344 -> --SAM D21 XIAO Startup Timing-- 13:35:47.344 -> setup() reached in 1173 microseconds (1.2 milliseconds). 13:35:47.344 -> (Serial) = true 498835 microseconds (498.8 milliseconds) later. 13:35:47.344 -> (Serial) = false after Serial.begin(3214) 13:35:47.344 -> USB connected 531 milliseconds (0.5 seconds) after USBSerial up. Baud: 115200. 13:35:47.344 -> 13:35:47.344 -> System reset in 10 seconds

The boolean test (Serial) returns true within half a second after the setup() function is reached. However, that only means that the USB stack is up and running; there is no USB connection at this point. To test for the latter it is necessary to issue a Serial.begin() command (with any value for the baud as that is ignored in the serial-over-cdc-usb Serial_ class) before testing the Serial boolean operator. Those curious about the behaviour of the latter should take a look at the role of the _DTR field in the Serial_::begin() Serial_::operator bool() functions in .../framework-arduino-samd-seeed/cores/arduino/USB/CDC.cpp. It will become clear that Serial.beginWithoutDTR() should not be used instead of Serial.begin() when wanting to identify when a USB connection is available.

The results show that the Arduino serial monitor re-establishes a connection over USB with the XIAO in slightly more than a second after the microcontroller reboots. In platformIO, I simply cannot restart the monitor as quickly because the USB connection, which is lost after the XIAO is reset, is not restored automatically. After waiting for 8 seconds for a USB connection, the program will move on as shown below where I waited until the yellow LED was no longer on before opening the serial monitor just to prove the point.

... --SAM D21 XIAO Startup Timing-- setup() reached in 1173 microseconds (1.2 milliseconds). (Serial) = true 498831 microseconds (498.8 milliseconds) later. (Serial) = false after Serial.begin(3214) No USB connection made afer 8 seconds. System reset in 7 seconds ...

With all that, I now believe that Albert van Dalen is correct and that the best start to the setup() function is just an appropriate delay. I even created a little bootDelay library complete with optional LED flashing and a USB connection timeout parameter (8 seconds by default which seems just right in my case). It can be found in the blink2 example (Source: 03_blink2). Frankly, a simple delay(8000) at the very start of setup() would suffice. The justification for this delay is twofold. First, that delay provides enough time to open the serial monitor to view messages printed during the rest of the setup() function and the beginning cycles of the loop() function if the setup() is short. Usually Serial.print() statements are just a debugging tool and not needed during the normal execution of the microcontroller firmware. On the other hand, that 8 second initial delay could prove very useful should I manage to put the SAM D21 into a state where the USB stack is no longer responsive. If it becomes impossible to upload a corrected version, I can disconnect the USB cable to the XIAO, reconnect it and after a couple of seconds the XIAO USB stack will function correctly which leaves another 6 seconds before the errant firmware starts which should be ample time to start the upload of a corrected firmware.

Many of the example projects presented below display their results on the serial monitor (or the serial plotter in the Arduino IDE). Accordingly, a blocking test for a USB connection will often follow the initial delay. Do not do that in normal circumstances.

As a final remark in this section, note that the NVIC_SystemReset(); statement is a software reset of the SAMD 21.

XIAO SAMD21 Input Output Pins toc

The following images show more details concerning the XIAO SAMD21. In addition to the "standard" XIAO connections (11 microcontroller input/output pins on the headers and the 2 microcontroller signals connected to SWD pads on the reverse of the board), 3 SAM D21 I/O pins are connected to 3 of the 4 LEDs to the right of the USB-C connector. The fourth LED, labelled P for power, is not connected to an I/O pin. A pad to the left of the USB-C connector is connected to the RST pin of the microcontroller while the pad directly below it is connected to ground. In later revisions of the XIAO, the RST and ground connections are replicated on the reverse side of the XIAO. There is also an additional pair of pads on the reverse side of the XIAO that could be used to power the board.

XIAO - front XIAO - back

Connecting to the pads on the back side of the XIAO is not easy. The XIAO Expansion board uses four pogo pins to connect to the SWDIO, SWCLK, GND and RST pads to provide the debug connector and a reset push button (see Hardware Accessories above for more details).

Pin diagrams of the XIAO often show Dx alongside the Arduino pin name Ax. These additional variables are not defined anywhere and their addition is probably to highlight the flexibility of the microcontroller because each I/O pin can be a digital input or output or an analogue input. An Arduino name such as A3 is an unsigned 8-bit index to an entry in the pin description table named g_APinDescription[]found in the variant.cpp file. The value of the names are defined in the variant.h header file in approximately the following fashion.

static const uint8_t A0 = 0; ... static const uint8_t A9 = 9; static const uint8_t A10 = 10; static const uint8_t DAC0 = 0; ... static const uint8_t MISO = 9; static const uint8_t MOSI = 10; ..

For clarity, some pins have more than one name such as MISO = A9 = 9. The value of each variable is Ax is x (x = 0,1,...10). It could be tempting to use the literal numeric value (the Arduino pin number) instead of the Arduino pin name. That should not be done in the very unlikely event that this mapping is changes in a future version of the variant files when the SAM D21 Arduino core is updated.

The actual SAM D21 I/O pins have names such as PA02 and PB18 which represents two 8-bit values: the port (A, B, C, or D according to the WVariant.h header although only A and B are commonly encountered) and a bit number (0 - 31, also called the bit mask) within the port. Each entry in the pin description table begins with the port and bit mask of the corresponding Arduino pin. For speed, the conversion from Arduino name to the microcontroller pin identity is implemented as two macros digitalPinToPort(P) and digitalPinToBitMask(P) that are defined in the variant.h header file. The pin description table contains much more information about each I/O pin. Here is an "artistic" rendering of what is in the table.

Arduino PinSAM D21
Pin
Default
Function
DigitalAnalogueIRQ
NameNumber In OutPWM In OutQT
A00PA2DAC
A11PA4I/O
A22PA10I/O ✘(*)
A33PA11I/O ✘(*)
A44PA8I²C - SDA ✘(*)
A55PA9I²C - SCL ✘(*) ✔ (!)
A66PB8UART - TX
A77PB9UART - RX ✔ (!)
A88PA7SPI - SCK
A99PA5SPI - MISO
A1010PA6SPI - MOSI
-11PA19USB TX activity LED
-12PA18USB RX activity LED
-13PA17Free User LED
-14PA28USB Host enable
-15PA24USB Data-
-16PA25USB Data+
--PA31 (§)SWDIO (?) (?)(?)(?) (?)
--PA30 (§)SWCLCK (?) (?)(?)(?) (?)

Notes:

(*) - X drivers in an X/Y matrix of QT (capacitance touch) sensors.
(!) - pins A5 and A7 share an interrupt which cannot be attached to both at the same time.
(§) - The last two entries PA31 (SWDIO) and PA30 (SWCLK) used to implement the Serial Wire Debug Interface are not in the pin description array which explains why those entries do not have a pin number. According to avdweb, PA31 can be used safely for I/O without disturbing the debug interface, but if PA30 is used for I/O it will disable the debug interface. I have used these two pins as a fourth hardware serial port which was more of a stunt than anything else. I did not know about the avdweb "patch" needed to use PA30 for digital I/O, so did not apply it and the UART worked without it. Albert van Dalen hints that pins PA24, PA25 and PA28 should not be in the pin description table in order to minimize what he called the native USB port virus.
(?) - not tested

Three LEDs are connected to I/O pins which do not have Arduino pin names. However they all have macros that can be used as aliases to the Arduino pin number:

Arduino Pin NumberMacros
11PIN_LED3PIN_LED_TXL
12PIN_LED2PIN_LED_RXL
13PIN_LED_13PIN_LEDLED_BUILTIN

The fourth LED is a power-on signal not connected to an I/O pin.

The sources for these tables are the XIAO schematic, variant.cpp in the .../packages/Seeeduino/hardware/samd/1.7.0/variants/XIAO_m0/ directory and Table 7-1. PORT Function Multiplexing for SAM D21 A/B.CD Variant Devices and SAM DA1 A/B Variant Devices in the microcontroller datasheet.

Warning: The XIAO SAMD21 is a 3.3 volts device and applying 5 volts to an input will overload and damage the XIAO. There may be a 5-volt power connection on the board, but DO NOT use it to power say an I²C RTC clock because the latter's data connections would then be at the 5-volt level when in a HIGH state. Look at the absolute maximum ratings of VPIN in the following table.

Table 41-2. Absolute Maximum Ratings
SymbolDescriptionMin.Max.Units
VDDPower supply voltage03.8V
IVDDCurrent into a VDD pin-92 (1)mA
IGNDCurrent out of a GND pin-130 (1)mA
VPINPin voltage with respect to GND and VDDGND-0.6VVDD+0.6VV
TstorageStorage temperature-60150°C
1.Maximum source current is 46mA and maximum sink current is 65mA per cluster. A cluster is a group of GPIOs as shown in the table below. Also note that each VDD/GND pair is connected to two clusters so current consumption through the pair will be a sum of the clusters source/sink currents.

Source: Microchip, SAM D21/DA1 Family, Complete Datasheet DS40001882E, p. 1008.

Analogue Input and Output toc

Up to now, I have had basically no experience with analogue input and output because I have mostly worked with ESP8266 based devices, many of which did not even use its single analogue input pin. The SAM D21 is quite different, as almost every I/O pin can be used for digital input/output or analogue input; there is a single analogue output pin. This was a good opportunity to learn some of the details about this aspect of microcontrollers.

voltage divider schema First thing to check is that all Ax (x = 0,...10) I/O pins of the XIAO can be analogue inputs. A simple voltage divider with two 2.2K resistors was used to test each input pin with 3 voltages: 3.3 volts, 3.3/2 volts and 0 volts. A running average measure of the input voltage at the pin being tested was automatically restarted by the program whenever the input volt changed. Here is a typical output of the program.

Set the Analog to digital converter resolution to 12 bits Initialized pin A6 as an input. setup() completed, starting loop() Connected pin to 0V, resetting average Count: 10, current level: 0, average: 0.0 Count: 20, current level: 0, average: 0.0 Count: 30, current level: 0, average: 0.0 Count: 40, current level: 0, average: 0.0 Connected pin to voltage divider, resetting average Count: 10, current level: 2015, average: 2086.5 ... Count: 80, current level: 2029, average: 2027.1 Count: 90, current level: 2029, average: 2027.0 Count: 100, current level: 2020, average: 2025.6 Count: 110, current level: 2021, average: 2024.7 Connected pin to 3.3V, resetting average Count: 10, current level: 4051, average: 4053.1 ... Count: 60, current level: 4055, average: 4052.6 Count: 70, current level: 4052, average: 4052.5 Count: 80, current level: 4053, average: 4052.5 Count: 90, current level: 4052, average: 4052.5 Count: 100, current level: 4054, average: 4052.5 Count: 110, current level: 4052, average: 4052.6

Here is the source code that produced that output.

#include <Arduino.h> #include "src/bootdelay/bootdelay.h" int pin = A2; // pin to test void setup() { // Time to upload new firmware or open the COM port (or device) bootDelay("\nanalog_in\n---------", true); // The default ADC resolution is 10 bits set in // .../packages/framework-arduino-samd-seeed/cores/arduino/wiring_analog.c // Let's set it to 12 bits, #define ADC_RESOLUTION 12 is in the variant header file // .../packages/Seeeduino/hardware/samd/1.7.0/variants/XIAO_m0/variant.h analogReadResolution(ADC_RESOLUTION); Serial.printf("Set the Analog to digital converter resolution to %d bits\n", ADC_RESOLUTION); pinMode(pin, INPUT); Serial.printf("Initialized pin A%d as an input.\n", pin); Serial.println("setup() completed, starting loop()"); } #define HIGH_LEVEL 3900 #define LOW_LEVEL 300 #define MODE_GND 0 #define MODE_VIN 1 #define MODE_VCC 2 unsigned long sum = 0; unsigned long count = 0; int mode = MODE_VIN; void loop() { int level = analogRead(pin); if ( (level > HIGH_LEVEL) && (mode != MODE_VCC) ) { count = 0; sum = 0; mode = MODE_VCC; Serial.println("\nConnected pin to 3.3V, resetting average"); } else if ( (level < LOW_LEVEL) && (mode != MODE_GND) ) { count = 0; sum = 0; Serial.println("\nConnected pin to 0V, resetting average"); mode = MODE_GND; } else if ( (level >= LOW_LEVEL) && (level <= HIGH_LEVEL) && (mode != MODE_VIN) ) { count = 0; sum = 0; Serial.println("\nConnected pin to voltage divider, resetting average"); mode = MODE_VIN; } else { // mode unchanged, accumulate values to calculate the average sum += level; count ++; if (count % 10 == 0) { double avg = (sum*1.0)/(count*1.0); Serial.printf("Count: %d, current level: %d, average: %.1f\n", count, level, avg); } } delay(250); }
Source: 04_analog_in in xiao_m0_sketches on GitHub.

The result was nearly identical for all the XIAO pins so that I am confident that all pins can be used as 12-bit or lower analogue to digital converters. Surprisingly, there is no analog on pins A4 and A5 of the QT Py which is the Adafruit clone of the XIAO. Perhaps the slightly different version of the SAM D21 on the QT Py (ATSAMD21E vs the ATSAMD21G on the XIAO) explains why the SDA and SCL pins would not be analogue inputs on the Adafruit clone.

A 12-bit digital analogue converter translates the input voltage measured at the pin (which must be in the range 0 to Vcc which is 3.3 volts maximum) into one of 2^12 = 4096 possible values. When the input voltage is 0 volts, the value read should be 0, when the voltage is equal to Vcc the input value should be 4095. That is not quite what was returned by the sketch. With one XIA0 the average returned value on pin A7 after 22150 readings was 4053.1. There can be quite a bit of variation in the values returned when measuring Vcc as can be seen with the following histogram established after 741 measurements on A2 of another XIAO.

Note how the highest returned value was 4067 which is short of 4095, the theoretical maximum value. It is possible to calibrate the analogue input, in other words, rescale the ADC so that it will more reliably output 4095 when Vcc is applied to the pin. This is covered in the next subsection: Analogue Input Calibration.

Looking at the source code (in .../packages/framework-arduino-samd-seeed/cores/arduino/wiring_analog.c), it becomes obvious that the resolution of SAMD 21 analogue inputs can set to only one of 3 values: 8 (0 to 255), 10 (0 to 1023) and 12 bits (0 to 4095).

void analogReadResolution(int res) { _readResolution = res; if (res > 10) { ADC->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_12BIT_Val; _ADCResolution = 12; } else if (res > 8) { ADC->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_10BIT_Val; _ADCResolution = 10; } else { ADC->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_8BIT_Val; _ADCResolution = 8; } syncADC(); } uint32_t analogRead(uint32_t pin) { uint32_t valueRead = 0; ... valueRead = ADC->RESULT.reg; syncADC(); ADC->CTRLA.bit.ENABLE = 0x00; // Disable ADC syncADC(); return mapResolution(valueRead, _ADCResolution, _readResolution); }

Even if the maximum ADC resolution is 12 bits, the statement analogReadResolution(24) is perfectly legitimate. The analogRead() function will return a scaled value in the range 0 to 16777215 (=2^24-1) which only provides the illusion of greater accuracy.

Finally, note that the ADC read resolution is common for all analogue input pins; sorry if this is belabouring the obvious.

Analogue Input Calibration toc

Among the libraries installed in the SeeedStudio SAMD Arduino Core version 1.7.0 there was an example sketch named CorrectADCResponse in the SAMD_AnalogCorrection library. I modified it to be able to run it in both the Arduino IDE and PlatformIO, connected pin A1 to ground and A2 to Vcc (3.3volts) as instructed at the top of the source file and ran it. Here was the output generated by the program.

Calibrating ADC with factory values Reading GND and 3.3V ADC levels ADC(GND) = 0 ADC(3.3V) = 4064 Offset correction (@gain = 2048 (unity gain)) Offset = 0, ADC(GND) = 0 Gain correction Gain = 2048, ADC(3.3V) = 4064 Gain = 2049, ADC(3.3V) = 4065 ... Gain = 2063, ADC(3.3V) = 4093 Gain = 2064, ADC(3.3V) = 4094 Gain = 2065, ADC(3.3V) = 4094 Gain = 2066, ADC(3.3V) = 4094 Gain = 2067, ADC(3.3V) = 4095 Gain = 2068, ADC(3.3V) = 4095 Gain = 2069, ADC(3.3V) = 4095 Gain = 2070, ADC(3.3V) = 4095 Gain = 2071, ADC(3.3V) = 4095 Gain = 2072, ADC(3.3V) = 4095 ... Readings after corrections ADC(GND) = 0 ADC(3.3V) = 4095 ================== Correction values: Offset = 0 Gain = 2067 Add the next line to your sketch: analogReadCorrection(0, 2067); ==================
Source of the calibration sketch: 05_AnalogCorrection in xiao_m0_sketches on GitHub.

I then reran the analog_in sketch, setting the specified correction factor in the setup(),

Count: 710, current level: 4087, average: 4089.8 Count: 720, current level: 4088, average: 4089.8 Count: 730, current level: 4092, average: 4089.8 Count: 740, current level: 4095, average: 4089.8 Count: 750, current level: 4092, average: 4089.9 Count: 760, current level: 4091, average: 4089.8 ... Count: 5530, current level: 4090, average: 4089.9 Count: 5540, current level: 4089, average: 4089.9 Count: 5550, current level: 4089, average: 4089.9 Count: 5560, current level: 4087, average: 4089.9 ...

Quickly enough, the average settled at 4089.9 and the maximum value 4095 is rarely reached.

Unfortunately, the CorrectADCResponse was removed from the SeeedStudio fork of the core in the next version. I wasn't aware of that fact until I began rewriting this post. Unfortunately, I did not see the December 2020 forum post by krzysiekf asking where the library had gone. Luckily msfujino explained how to get the library from the Arduino Core for SAMD microcontrollers by Arduino where it is still present. That's fine for SAM D21 boards like the XIAO, but I would recommend getting the SeedStudio 1.7.0 version which also handles SAM D51. The whole core can be obtained here and then steps 3 and 4 of the procedure by msfujino can be followed. However it is probably easier to just get the library from the GitHub repository SAMD21/SAMD51 Analog Correction based on the original Seeed Studio version.

Analogue Measure of Light with an LDR toc

Here is the simple circuit used to measure the quantity of light built around a light dependant resistor (LDR) and a fixed resistor. The 5K value of the fixed resistor is just an indication, it is not critical. Besides, I do not know the characteristics of the LDR. As verified in the previous section, any of the 11 I/O pins of the XIAO could be used, I chose A3.

#include <Arduino.h> // Analog input pin #define LDR 3 void setup(){ delay(3000); Serial.println("\nStarting ldr_in\n---------------"); delay(5000); pinMode(LDR, INPUT); analogReadResolution(ADC_RESOLUTION); // #defined in ../XIAO_m0/variant.h Serial.println("setup() completed, starting loop()"); } int level; void loop(){ level = analogRead(LDR); Serial.println(level); // no text is better for the serial plotter delay(2000); }
Source: 06_ldr_in in xiao_m0_sketches on GitHub.

I took these measures in the late afternoon when the light level in the room was decreasing as the sun was getting closer to the horizon.

plotted LDR values

Values over 4000 can be obtained by bringing a COB LED quite near the LDR. The measured value can drop quickly to 0, or very near to that level, by cupping a hand tightly around the LDR and thus shutting out most of the light.

The source code contains a macro which can be used to calculate a rolling average of the input voltage on the LDR pin.

// Define to size of the FIFO queue to calculate a rolling average of LDR values. // if less than 2, no averaging is done #define LDR_FIFO_SIZE 10

Here is a graph showing how well a rolling average (in red) of the last 10 observations smooths out the raw data in blue.

plotted raw and average LDR values

This is based on artificial data. The LDR pin was left floating which produces something that looks very much like a random walk. The rolling average would probably do better with the light level measure which, unfortunately, I no longer had when adding this feature to the project. If the result is not smooth enough, then a longer FIFO buffer could be chosen. That has no impact on the performance, the calculation of the average is independent of the size of the buffer. Or a more sophisticated weighted average could be used, see testtouch sketch below.

Capacitance Touch Sensors toc

Like the ESP32, the SAM D21 has "touch pins". I was hoping that meant that the SAM D21 microcontroller had the equivalent of builtin TTP223 ICs. In other words, I thought it would be possible to connect an external electrode such as a copper disk or touch pad on a printed circuit board to one of these pins, set the mode and, voilà, there would be a clean 0/1 signal on the pin indicating when a finger was very close or in contact with the pad. Reality is always messier.

A major complication results from what Jeremy Gilbert (jgilbert20) called The PTC downfall: a big binary blob called QTouch. The Peripheral Touch Controller (PTC) subsystem is Microchip's hardware implementation of capacitive touch sensing as installed in the SAM D21 and other Microchip devices such as the ATmega328PB. The QTouch library is a royalty-free library for working with the PTC, but it is closed source. For that reason it is not included in any Arduino core. Hence the "official" QTouch library can only be used in development environments that are more accommodating, with the Microchip Studio 7 IDE being the most obvious choice. For mere hobbyists who prefer PlatformIO or the Arduino IDE, the only games in town are two reversed-engineered libraries: Jeremy Gilbert's LibrePTC and Adafruit_FreeTouch. Both libraries are available through the PlatformIO library manager but only the Adafruit library can be downloaded with the Arduino library manager. I have only used the Adafruit library and done just enough with it to check that the assignment of QT capabilities in the pin description table is correct.

Following the lead of juliusbangert asking for [h]elp with using Adafruit_FreeTouch library on SAMD21, I plotted the input from A0 and A1 set up as touch sensors in the Arduino Serial Plotter.

serial plot of the input while touching a pad

The plot is quite different from the one shown by Julius, but that could be down to differences in the sensor pad and in the version of the library. The sensor pad attached to each pin was very rudimentary, an 80 mm male-male Dupont wire arced from the pin to a free hole on the breadboard. Since the post by Julius in 2017, little information about the Adadfruit_FreeTouch has surfaced. I did find three examples on Web that use the library.

These libraries share a common "brute force" approach that I reproduced in a sketch. My only improvement was the attempt to establish the "touched" threshold in a calibration step rather than playing a guessing game.

/* * Seeeduino XIAO - testing Ax pin that can be capacitive touch inputs */ #include <Arduino.h> // needed in PlatformIO #include "Adafruit_FreeTouch.h" #define NUM_TOUCH_PINS 7 // create an instance of Adafruit_FreeTouch for each touch pin of the XIAO Adafruit_FreeTouch qt[NUM_TOUCH_PINS] = { Adafruit_FreeTouch( A0, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A1, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A6, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A7, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A8, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A9, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A10, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), }; // Maximum normal capacitance measures of each touch pin int threshold[NUM_TOUCH_PINS] { 0, 0, 0, 0, 0, 0, 0}; // Update the maximum capacitance measures during calibration void updateThreshold(int touchPin, int val) { if (val > threshold[touchPin]) threshold[touchPin] = val; } // Set the threshold at scale * maximum recorded capacitance measure void setThresholds(float scale) { for (int touchPin=0; touchPin < NUM_TOUCH_PINS; touchPin++) { threshold[touchPin] = (int) (scale*threshhold[touchPin]); } } // Measure the capacitance of each pin count times to find the // maximum value and then scale it to establish the thresh hold. void calibrateThresholds(int count, float scale) { for (int n=0; n<count; n++) { for (int touchPin=0; touchPin < NUM_TOUCH_PINS; touchPin++) { updateThreshold(touchPin, qt[touchPin].measure()); delay(5); } } setThresholds(scale); } // Return 1 if val > threshold or 0 otherwise int touched(int touchPin, int val) { if (val > threshold[touchPin]) return 1; return 0; } // flag if the setup succeeded bool allgood = true; void setup() { delay(8000); // time to upload the firmware // initialize the touch pins for (int touchPin = 0; touchPin < NUM_TOUCH_PINS; touchPin++ ) { if (! qt[touchPin].begin()) { Serial.printf("Failed to initialize Touch class on touch pin %d\n", touchPin); allgood = false; } } if (allgood) { calibrateThreshholds(200, 1.1); #ifdef PLATFORMIO Serial.println("Threshholds:"); for (int touchPin = 0; touchPin < NUM_TOUCH_PINS; touchPin++ ) { Serial.printf(" touch pin %d: %d\n", touchPin, threshold[touchPin]); } #endif } #ifdef PLATFORMIO Serial.println("setup() completed."); #endif } // continuously read the first two touch pins and print their touched value. void loop() { if (allgood) { // run test on first two touch pins. int val0 = touched(0, qt[0].measure()); delay(5); // needed! int val1 = touched(1, qt[1].measure()); #ifdef PLATFORMIO if (val0) Serial.println("Touch pin 0 touched"); if (val1) Serial.println("Touch pin 1 touched"); #else Serial.printf("%d\t%d\n", val0, val1); #endif delay(20); } else { Serial.println("Setup failed"); delay(4000); } }
Source: 07_freetouch in xiao_m0_sketches on GitHub.

As can be seen from the output

Thresholds: touch pin 0: 752 touch pin 1: 655 touch pin 2: 304 touch pin 3: 276 touch pin 4: 314 touch pin 5: 317 touch pin 6: 354 setup() completed. Touch pin 1 touched Touch pin 1 touched Touch pin 1 touched Touch pin 1 touched Touch pin 1 touched Touch pin 0 touched Touch pin 0 touched Touch pin 1 touched Touch pin 0 touched Touch pin 1 touched

The long answer by adafruit_support_mike in the Julius forum post contains information about the parameters used to construct the Adafruit_FreeTouch class. It would be valuable to experiment with these values in a serious project. His last recommendation about the use of a running average could be misunderstood. A running average smooths out the fluctuations in the measured capacitance, but clearly from the plots, it is their greater amplitude that marks a touch. So I assume that the idea was to identify a touch when the current capacitance measure is too far from the running average. That also works from my test.

/* * Seeeduino XIAO - testing Ax pin that can be capacitive touch inputs */ #include <Arduino.h> #include "Adafruit_FreeTouch.h" #define NUM_TOUCH_PINS 7 // A0, A1, A6 to A10 #define N 20 // Moving average parameter #define scale 0.25 // "touched" or outlier proportion // create an instance of Adafruit_FreeTouch for each touch pin of the XIAO Adafruit_FreeTouch qt[NUM_TOUCH_PINS] = { Adafruit_FreeTouch( A0, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A1, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A6, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A7, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A8, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A9, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), Adafruit_FreeTouch( A10, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE ), }; // average capacitance level int level[NUM_TOUCH_PINS] {0}; // Update the average capacitance measure. // This is actually a weighted moving average with weights of older // measures vanishing to 0 void updateLevel(int touchPin, int val) { level[touchPin] = (( N * level[touchPin] ) + val ) / ( N + 1 ); } // Return 1 if the difference between the capacitance measure and the average level is big // In other words return 1 if the new measure is an outlier with respect to the moving average int touched(int touchPin, int measure) { bool big = ( abs(measure - level[touchPin]) > (int) (scale*level[touchPin]) ); if (!big) updateLevel(touchPin, measure); // dont include outliers in the moving average calculation return big; } void calibrateLevels(int count) { int n = (count < 3*N) ? 3*N : count; for (int i=0; i<n; i++) { for (int touchpin=0; touchpin<NUM_TOUCH_PINS; touchpin++) { updateLevel(touchpin, qt[touchpin].measure()); delay(10); } } } // flag if the setup succeeded bool allgood = true; void setup() { delay(8000); // time to upload the firmware // initialize the touch pins for (int touchPin = 0; touchPin < NUM_TOUCH_PINS; touchPin++ ) { if (! qt[touchPin].begin()) { Serial.printf("Failed to initialize Touch class on touch pin %d\n", touchPin); allgood = false; } } if (allgood) { calibrateLevels(200); #ifdef PLATFORMIO Serial.println("Moving average capacitance levels:"); for (int touchPin = 0; touchPin < NUM_TOUCH_PINS; touchPin++ ) { Serial.printf(" touch pin %d: %d\n", touchPin, level[touchPin]); } #endif } #ifdef PLATFORMIO Serial.println("setup() completed.\n"); #endif } // continuously read the first two touch pins and print their touched value. void loop() { if (allgood) { // run test on first two touch pins. int val0 = touched(0, qt[0].measure()); delay(5); // needed! int val1 = touched(1, qt[1].measure()); #ifdef PLATFORMIO if (val0) Serial.println("Touch pin 0 touched"); if (val1) Serial.println("Touch pin 1 touched"); #else Serial.printf("%d\t%d\n", val0, val1); #endif delay(20); } else { Serial.println("Setup failed"); delay(4000); } }
Source: 08_testtouch in xiao_m0_sketches on GitHub.

There's no pretence that either of these sketches provides a working solution to using the touch-sensing capabilities of the XIAO in a practical application where the board is used to perform resource-intensive tasks. But this is just an overview. If someone knows of a more complex example please get in touch that I may add it here.

Analogue Output toc

There is a single "true analogue" output signal on the XIAO which is available on pin A0. The term true analogue was in quotation marks because the signal is obtained through a 10 bit digital to analogue converter (DAC) so that the output voltage can be set to one of 2^10 = 1024 values. The output voltage is given by the following formula:

  DAC output voltage  Vout = (DATA/1023)*Vref

where DATA is any value from 0 to 1023. The reference voltage, Vref, can be one of many values, but by default it is the XIAO input voltage which is nominally 3.3V. Accordingly only discrete voltages can be obtained but the resolution, which by default is 0.003226 volts or 3.226 millivolts, is probably better than I will ever need. Using a Vref of 1 volt, the step will fall to slightly less than 1 millivolt: 0.9775.

To test this analogue signal, I simply connected A0 to A2 but any other analogue input from A1 to A10 could be used although it is best to avoid A1. Here is a simple sketch that simply ramps the output analogue signal up and down in a linear fashion and reads the pin output. In essence it produces a triangular wave with code rather similar to the sketch using PWM to pulse the intensity of LEDs.

#include <Arduino.h> #include "src/SAMD_AnalogCorrection/src/SAMD_AnalogCorrection.h" // select the ADC input pin, could be any pin from A1 to A10 #define ADC_IN A2 // If the SCALE is defined, then DAC output will be scaled by 4 (2^12 = 4 * 2^10) // so that the DAC output and the ADC input plots match #define SCALE // Resolutions // #define DAC_BITS 10 // 2^10 = 1024 (hence possible output value 0 to 1023) #define ADC_BITS ADC_RESOLUTION //#define ADC_RESOLUTION 12 in .../XIAO_m0/variant.h // Connect DAC0 (A0) output to ADC_IN pin as defined above #define DAC_OUT DAC0 // DAC output pin - A0 #ifdef SCALE int scaleFactor = 1<<(ADC_BITS - DAC_BITS); #endif int step = 16; void setup() { // time for USB connection and to upload new firmware delay(3000); Serial.println("\nStarting dac2adc\n----------------"); delay(5000); //pinMode(DAC_OUT, OUTPUT); // ***** DO NOT set DAC0 = A0 mode to OUTPUT when using the DAC ***** pinMode(ADC_IN, INPUT); Serial.printf("Initialized pin A%d as an input.\n", ADC_IN); analogWriteResolution(DAC_BITS); Serial.printf("Set DAC resolution of output pin %d to %d bits.\n", DAC_OUT, DAC_BITS); analogReadResolution(ADC_BITS); Serial.printf("Set ADC resolution of input pin %d to %d bits.\n", ADC_IN, ADC_BITS); analogReadCorrection(0, 2067); Serial.println("Set analogReadCorrection, offset = 0, gain = 2067."); #ifdef SCALE Serial.printf("Output DAC values scaled up by %d to match input ADC values.\n", scaleFactor); #else Serial.println("Raw output DAC and input ADC values shown."); #endif Serial.println("setup() completed, starting loop()"); } int invalue; int outvalue = 0; void loop() { analogWrite(DAC_OUT, outvalue); delay(2); // time to settle, probably not necessary invalue = analogRead(ADC_IN); #ifdef PLATFORMIO #ifdef SCALE Serial.printf("OUT(DAC):%d\tIN(ADC):%d\n", scaleFactor*outvalue, invalue); #else Serial.printf("OUT(DAC):%d\tIN(ADC):%d\n", outvalue, invalue); #endif #else // Arduino #ifdef SCALE Serial.printf("OUT(DAC):%d\tIN(ADC):%d\tMax:4095\n", scaleFactor*outvalue, invalue); #else Serial.printf("OUT(DAC):%d\tIN(ADC):%d\tMaxDAC:1023\tMaxADC:4095\n", outvalue, invalue); #endif #endif outvalue += step; if ((outvalue < 0) || (outvalue > 1023)) { step = -step; if (outvalue < 0) outvalue = 0; else outvalue = 1023; } delay(10); }
Source: 09_dac2adc in xiao_m0_sketches on GitHub.

This is the output of the setup() function when SCALE is not defined (i.e., when #define SCALE is removed or turned into a comment).

Starting dac2adc ---------------- Initialized pin A2 as an input. Set DAC resolution of output pin 0 to 10 bits. Set ADC resolution of input pin 2 to 12 bits. Set analogReadCorrection, offset = 0, gain = 2067. Raw output DAC and input ADC values shown. setup() completed, starting loop()

The loop() function pumps out lines of output and input measures along with two reference values to the serial port.

OUT(DAC):63 IN(ADC):2783 MaxDAC:1023 MaxADC:4095 OUT(DAC):47 IN(ADC):2787 MaxDAC:1023 MaxADC:4095 OUT(DAC):31 IN(ADC):2784 MaxDAC:1023 MaxADC:4095 OUT(DAC):15 IN(ADC):2784 MaxDAC:1023 MaxADC:4095 OUT(DAC):0 IN(ADC):2785 MaxDAC:1023 MaxADC:4095 OUT(DAC):16 IN(ADC):2788 MaxDAC:1023 MaxADC:4095 OUT(DAC):32 IN(ADC):2784 MaxDAC:1023 MaxADC:4095 OUT(DAC):48 IN(ADC):2785 MaxDAC:1023 MaxADC:4095 OUT(DAC):64 IN(ADC):2783 MaxDAC:1023 MaxADC:4095 ...

The serial output of the sketch can be viewed in the IDE serial plotter (Tools/Serial Plotter).

The graph could be surprising on first look, but remember this plot is not a direct measure of the output and input voltages. The triangular blue wave shows the numeric data written to the DAC to specify the output voltage on pin 0. The red wave (or higher amplitude wave, for colour-blind readers) shows the numeric data read from the ADC on pin 8 as it converts the output voltage read on pin A1. The ADC has a 12-bit resolution, so it divides the 0 to 3.3 voltage range into 4096 steps. Writing 64 to the output DAC will result in an output voltage of 0.2064 (=3.3*64/1023). The ADC reads that voltage as 4095*0.2064/3.3 = 256.12, which is 4 times 64 (when ignoring rounding and measurement errors). The input voltage is quite close to the output voltage notwithstanding the scale factor. This can be verified by scaling either the DAC or ADC measure. When the SCALE macro is defined, the output value is scaled and the result is the following graph.

It is now obvious that the input value is (almost) equal to the output value.

The analogWriteResolution(DAC_BITS) statement is not needed. Surprisingly, if it is included, the DAC_BITS parameter has no influence at all on the program; testing with analogWriteResolution(6) resulted in the exact same plot as before. Looking at analogWriteResolution() analogWrite() (in .../packages/framework-arduino-samd-seeed/cores/arduino/wiring_analog.c), it is obvious that no constraints are placed on the value written to the DAC except that only the lower 10 bits of the value written are used.

void analogWriteResolution(int res) { _writeResolution = res; } void analogWrite(uint32_t pin, uint32_t value) { PinDescription pinDesc = g_APinDescription[pin]; uint32_t attr = pinDesc.ulPinAttribute; if ((attr & PIN_ATTR_ANALOG) == PIN_ATTR_ANALOG) { // DAC handling code if (pin == PIN_DAC0) { // Only 1 DAC on A0 (PA02) syncDAC(); DAC->DATA.reg = value & 0x3FF; // DAC on 10 bits. syncDAC(); DAC->CTRLA.bit.ENABLE = 0x01; // Enable DAC syncDAC(); return; } ... if ((attr & PIN_ATTR_PWM) == PIN_ATTR_PWM) { value = mapResolution(value, _writeResolution, 16); ... return; } ... // -- Defaults to digital write pinMode(pin, OUTPUT); value = mapResolution(value, _writeResolution, 8); if (value < 128) { digitalWrite(pin, LOW); } else { digitalWrite(pin, HIGH); } }

In other words, the _writeResolution variable that is modified with the analogWriteResolution() function is not used in analogWrite() when writing to the DAC pin (A0). It is used when writing to the other XIAO pins which are all capable of PWM. I think the take away here is do not set analogWriteResolution() in the hope of modifying the resolution of the DAC pin (A0). It will do nothing with respect to the DAC, but it will modify the behaviour of analogWrite() which sets the duty cycle of PWM outputs.

Important Note: when using pin 0 to output the DAC signal, DO NOT set the pin mode to OUTPUT. Here is what happens if pinMode(DAC_PIN, OUTPUT); statement is included the setup() function. (See analogWrite() DAC on A0.)

Remember the red truncated wave is the actual voltage on the output pin, the blue wave is the set output voltage which is manifestly not obtained at times. Note that the DAC and ADC measures have been converted to volts. The sketch, dac2dac_v.ino, that produced this plot can be found in the archive that accompanies this post.

Digital Input Output toc

All I/O pins on the XIAO can be used as digital pins, which means pins that should be in either a LOW state or HIGH state when used as inputs or outputs.

Digital Output toc

It is a simple matter to adapt the sketch blinking the XIAO yellow user LED (01_blink) to show that every pin on the XIAO can be used as a digital output pin. Here is a schematic for a perfectly good probe to use to check that each pin of the XIAO flashes on and off when using the following sketch. It is also possible to imitate an analogue output signal with a square wave by modifying the ratio of its ON time to its OFF time. This is called pulse width emulation (PWM).

Single LED probe

It will be on sale very soon on a merchandizing site for the modest cost of $ 0.99 (plus $ 32.84 shipping and handling fee, US dollars of course). However some may want to wait for the high tech version.

Two LED probe

Prototyping is complete, so a crowdfunding campaign will be launched, as soon as a patent has been obtained and the current chip shortage crisis is over.

#include <Arduino.h> // needed for PlatformIO #define DELAY 125 void setup() { // 8 second startup delay delay(1000); // letting the USB stack establish a connection Serial.println("Patience xiao!"); // will be lost if the connection is not established delay(7000); // quiet time to upload new firmware or open the COM port (or device) // initialize I/O signals connected to the LED for (int pin=0; pin<11; pin++) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); // set the pin low Serial.printf("Initialized pin A%d as output.\n", pin); } Serial.println("setup() completed, starting loop()"); } int pin = 0; void loop() { Serial.printf("Pin A%d set to HIGH", pin); digitalWrite(pin, HIGH); delay(DELAY); digitalWrite(pin, LOW); Serial.println(", now LOW."); pin = (++pin)%11; }
Source: 10_digitalOut in xiao_m0_sketches on GitHub.

Just connect "the probe" to each pin of the XIAO in turn and wait until the LED flashes on and off in tandem with the message on the serial monitor.

Pulse Width Modulation toc

Simple Arduino-style pulse width modulation is available on pins A1 to A10 and on the three I/O lines connected to the on-board LEDs.

#include <Arduino.h> void setup() { delay(1000); Serial.println("Patience xiao"); delay(7000); for (int i = 0; i<14; i++) { pinMode(i, OUTPUT); Serial.printf("Pin %s%d mode set to output\n", (i<11) ? "A" : "", i); } Serial.println("setup() completed, starting loop()"); } int setPwm(int pin, int duty) { if (duty >= 255) { duty = 255; pinMode(pin, OUTPUT); digitalWrite(pin, HIGH); } else if (duty <= 0) { duty = 0; pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } else { analogWrite(pin, duty); } //Serial.println(duty); return duty; } int pin = 1; void loop() { Serial.printf("Fading pin %d", pin); if (pin) Serial.println(); else Serial.println(" will not work"); if (pin > 10) delay(500); // minimize USB activity LED flashes else delay(100); if (pin < 11) { for (int i = 0; i < 17; i++) { setPwm(pin, i*16); delay(100); } for (int i = 15; i >= 0; i--) { setPwm(pin, i*16); delay(100); } } else { for (int i = 16; i >= 0; i--) { setPwm(pin, i*16); delay(100); } for (int i = 1; i < 17; i++) { setPwm(pin, i*16); delay(100); } } pin = (++pin)%14; }
Source: 11_pwm in xiao_m0_sketches on GitHub.

Notice the setPwm() function. In analogWrite() PWM range, lady ada highlights the difference between AVR and ARM cortex devices when setting the PWM duty cycle to 255. It turns out that the same problem exists when setting the duty cycle to 0. In both cases the output of the pin will not be a steady 0 or Vcc voltage; there will be a very short pulse. Hence the need to reset the pin mode to OUTPUT and the use of digitalWrite() instead of analogWrite() in those two edge cases.

A PWM signal is a square wave (ON/OFF). It's duty ratio (which is set with the setPwm() function in the above sketch is the proportion of ON time to the period of the signal. In other words, it is the ratio (ON time)/(ON time + OFF time). The period of the wave is 1/frequency. So the true length of time that the signal is on and off depends on the frequency of the signal. As far as I know, there is no function in the Arduino framework to set the PWM frequency. Doing that necessitates programming at the "bare metal" level, using assembly language in other words, which is beyond the scope of an overview of the XIAO and way above my pay scale in any case. I will point out that there is at least one library Arduino_SAMD21_turbo_PWM - A fast PWM library for SAMD21G-based Arduinos that points the way to do this. Unfortunately, it does not support the XIAO.

Digital Input toc

input probe: single wire The original version of this post used a momentary close push-button to test the digital input function of the I/O pins. That required using a button library, which was good because it allowed me to advertise my own library. In retrospect, it all seemed much too complicated. A Dupont wire with an optional resistor is all that is needed to verify that all the I/O pins of the XIAO can be used as digital inputs. There is no need for an external pull up or pull down restore to ensure that the microcontroller input pin is in a steady default state when the I/O pin is floating. Setting the pin mode to INPUT_PULLDOWN connects a weak (20K to 60K but typically 40 KΩ) resistor between the pin and ground. If the mode is set to INPUT_PULLUP then the weak resistor connects the pin to Vcc. If the mode is set to INPUT, then the pin "floats" meaning its state (HIGH or LOW) is susceptible to electrical noise. This can be the cause of mysterious errors.

To run the following sketch, set INPUT_MODE to one of INPUT_PULLUP or INPUT_PULLDOWN. If INPUT_PULLDOWN is chosen, one end of the wire should be connected to Vcc (3.3 V) so that when the other end of the wire contacts a pin, it will overcome the weak pull down internal resistor and toggle the state of the input pin to HIGH. If the mode is set to INPUT_PULLUP then the wire should be grounded to toggle an input pin to the LOW state.

#include <Arduino.h> #define INPUT_MODE INPUT_PULLDOWN // INPUT_PULLUP or INPUT_PULLDOWN void setup() { // Time to upload new firmware or open the COM port (or device) delay(1000); Serial.println("Patience xiao"); delay(7000); // initialize I/O signals connected to the LED for (int pin=0; pin<11; pin++) { pinMode(pin, INPUT_MODE); Serial.printf("Initialized pin A%d as input with internal %s resistor.\n", pin, (INPUT_MODE == INPUT_PULLUP) ? "pullup" : "pulldown"); } pinMode(LED_BUILTIN, OUTPUT); for (int i=0; i<8; i++) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); delay(75); } digitalWrite(LED_BUILTIN, HIGH); // turn LED off Serial.println("setup() completed, starting loop(), start probing"); } int pin = 0; int prevpin = -1; int prevlevel = -1; void loop() { int level = digitalRead(pin); if (pin == prevpin) { if (level != prevlevel) { Serial.printf("Pin A%d is %s\n", pin, (level) ? "HIGH" : "LOW"); prevlevel = level; } } else if ((INPUT_MODE == INPUT_PULLUP) && !level) { Serial.printf("Pin A%d is LOW\n", pin); prevpin = pin; prevlevel = level; } else if ((INPUT_MODE == INPUT_PULLDOWN) && level) { Serial.printf("Pin A%d is HIGH\n", pin); prevpin = pin; prevlevel = level; } pin = (++pin)%11; }
Source: 12_digitalIn in xiao_m0_sketches on GitHub.

The state of the button is continuously polled in this sketch, but it is possible to forego all this polling busy work by using an interrupt as explained in the next section.

With our simple probe, we can be sure that the signal to the input pin will be very close to 0 volts if the wire is grounded or very close to Vcc (3.3 volts on the XIAO) if the pin is connected to the 3.3 V output pin. But in practical applications, there's often no guarantee that such an unambiguous signal will be fed to a digital input pin. What is the minimum voltage level that the microcontroller will deem to be HIGH? The single true analogue output pin (A0) can be used to test this.

#include <Arduino.h> // Connect DAC output to any other XIAO pin #define DAC_PIN A0 // DAC output pin #define DIG_PIN A2 // digital input pin, could be any pin from A1 to A10 //#define PLOT // show data in plotter monitor #define DATA 0 // show raw numeric values written to DAC #define VOLTS 1 // scale the DAC to volts // select the scaling to use #define SCALE VOLT // Resolution // #define DAC_RESOLUTION 10 // 2^10 = 1024 (hence possible output value 0 to 1023) #define VREF 3.3 const float dacToVolt = VREF/((1<<DAC_RESOLUTION)-1); const float digToVolt = VREF; #ifdef PLATFORMIO #undef PLOT #endif #ifdef PLOT int step = 16; #else int step = 1; #endif void setup() { // Time to upload new firmware or open the COM port (or device) delay(1000); Serial.println("Patience xiao"); delay(7000); // time to upload new firmware pinMode(DIG_PIN, INPUT); //pinMode(DAC_PIN, OUTPUT); // ***** DO NOT set A0 mode to OUTPUT when using the DAC ***** analogWriteResolution(DAC_RESOLUTION); } int invalue; int outvalue = 0; #ifndef PLOT bool isLow = true; #endif void loop() { analogWrite(DAC_PIN, outvalue); delay(2); invalue = digitalRead(DIG_PIN); #ifdef PLOT #if (SCALE == VOLTS) Serial.printf("OUT(V):%.4f\tIN(V):%.4f\n", dacToVolt*outvalue, digToVolt*invalue); #else // (SCALE == DATA) Serial.printf("OUT(DAC):%d\tIN(ADC*1023):%d\n", outvalue, invalue*1023); #endif #else if (isLow && invalue) { Serial.printf("Transition from LOW to HIGH at %.4fV\n", dacToVolt*outvalue); isLow = false; } else if (!isLow && !invalue) { Serial.printf("Transition from HIGH to LOW at %.4fV\n", dacToVolt*outvalue); isLow = true; } #endif outvalue += step; if ((outvalue < 0) || (outvalue > 1023)) { step = -step; if (outvalue < 0) outvalue = 0; else outvalue = 1023; } delay(10); }
Source: 13_dac2digIn in xiao_m0_sketches on GitHub.

Here is the output to the Arduino plotter.

Input voltage vs digital output

And this is the output if the PLOT macro is not defined or if running in PlatformIO.

Transition from LOW to HIGH at 1.5871V Transition from HIGH to LOW at 1.2161V Transition from LOW to HIGH at 1.5903V Transition from HIGH to LOW at 1.2161V Transition from LOW to HIGH at 1.5903V Transition from HIGH to LOW at 1.2161V

While a voltage inferior to 1.22 volts is applied to A2 on the particular XIAO on which this sketch was run, a LOW (=0) will be returned by digitalRead(A2). Similarly a voltage above 1.59 volts will be read as a HIGH (=1). The in between grey zone is a manifestation of hysteresis where digitalRead() will return HIGH or LOW depending on the state of the pin before. This observed grey zone is much smaller than what could be expected given the guaranteed characteristics which are 0.99 and 1.8 volts (at VDD = 3.3 volts) as per Table 41-12. Normal I/O Pins Characteristics in SAM D21/DA1 Family, Complete Datasheet pg 1009.

ParameterConditionsMin.Max.Unit
Input low-level voltageVDD = 2.7V-3.63V-0.3 × VDDV
Input high-level voltage0.55 × VDD-

Interrupts toc

The initial look at interrupts was not very detailed.

/*  button_int.ino */ #define BUTTON  7        // Can be any I/O pin from 0 to 10 #define DEBOUNCE_TIME 10 // millisecond button debouncing time void buttonISR() {  noInterrupts();  delay(DEBOUNCE_TIME);  digitalWrite(LED_BUILTIN, digitalRead(BUTTON));  interrupts(); } void setup() {  // initialize the LED pin as an output:  pinMode(LED_BUILTIN, OUTPUT);  // initialize the pushbutton pin as an input and enable the internal pull up resistor:  pinMode(BUTTON, INPUT_PULLUP);  attachInterrupt(digitalPinToInterrupt(BUTTON), buttonISR, CHANGE);   } void loop() { // empty right now, add code as needed. }

I was happy that it worked and then went on at length about the digitalPinToInterrupt() macro. It is no longer defined twice so there's no point in repeating that discussion. However, I have now realized that the mapping between the Arduino pin number and the external interrupt attached to it is not as simple as I thought. So I modified the setup() function to shows the mapping between the Arduino pin number and its external interrupt.

DigitalPinToInterrupt(A0) = 0, External interrupt: 2 DigitalPinToInterrupt(A1) = 1, External interrupt: 4 DigitalPinToInterrupt(A2) = 2, External interrupt: 10 DigitalPinToInterrupt(A3) = 3, External interrupt: 11 DigitalPinToInterrupt(A4) = 4, External interrupt: 16 DigitalPinToInterrupt(A5) = 5, External interrupt: 9 DigitalPinToInterrupt(A6) = 6, External interrupt: 8 DigitalPinToInterrupt(A7) = 7, External interrupt: 9 DigitalPinToInterrupt(A8) = 8, External interrupt: 7 DigitalPinToInterrupt(A9) = 9, External interrupt: 5 DigitalPinToInterrupt(A10) = 10, External interrupt: 6

When two pins, A5 and A7, share the same interrupt it probably means that some restrictions apply. I decided to write a slightly more sophisticated program.

#include <Arduino.h> #define PIN_5_ONLY 0 // attach an interrupt to pin A5 but not to A7 #define PIN_7_ONLY 1 // attach an interrupt to pin A7 but not to A5 #define PIN_5_AND_7 2 // attach an interrupt to pins A5 and A7 - distinct ISRs **** Neither pin can fire the interrupt **** #define PIN_5_7_SAME_ISR 3 // attach an interrupt to pins A5 and A7 - same ISR **** Neither pin can fire the interrupt **** #define EXT_INT9_PIN PIN_5_ONLY #define DEBOUNCE_TIME 10 // millisecond button debouncing time volatile int activeint = -1; volatile bool reported = true; void SR(int pin) { noInterrupts(); delay(DEBOUNCE_TIME); digitalWrite(LED_BUILTIN, digitalRead(pin)); /* Exploiting the fact that the XIAO LEDS are active LOW. Here is the logic: if (digitalRead(BUTTON) == HIGH) { // If the buttonState is HIGH, the button is release so turn the LED off: digitalWrite(LED_BUILTIN, HIGH); } else { // If the buttonState is LOW, the button is pressed so turn LED on: digitalWrite(LED_BUILTIN, LOW); } With an acive HIGH LED could use digitalWrite(LED_BUILTIN, !digitalRead(BUTTON)); */ activeint = pin; reported = false; interrupts(); } // used for pins 0 and 1 void button0_ISR() { SR(0); } void button2_ISR() { SR(2); } void button3_ISR() { SR(3); } void button4_ISR() { SR(4); } void button5_ISR() { SR(5); } void button6_ISR() { SR(6); } void button7_ISR() { SR(7); } void button8_ISR() { SR(8); } void button9_ISR() { SR(9); } void button10_ISR() { SR(10); } void setup() { // Time to upload new firmware or open the COM port (or device) delay(1000); Serial.println("Patience xiao"); delay(7000); // time to upload new firmware // initialize the LED pin as an output: pinMode(LED_BUILTIN, OUTPUT); for (int pin=0; pin<11; pin++) { Serial.printf("DigitalPinToInterrupt(A%d) = %d, ", pin, digitalPinToInterrupt(pin)); Serial.printf(" External interrupt: %d\n", g_APinDescription[pin].ulExtInt); // initialize the pin as an input and enable the internal pull up resistor: pinMode(pin, INPUT_PULLUP); } attachInterrupt(digitalPinToInterrupt(0), button0_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(1), button0_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(2), button2_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(3), button3_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(4), button4_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(6), button6_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(8), button8_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(9), button9_ISR, CHANGE); attachInterrupt(digitalPinToInterrupt(10), button10_ISR, CHANGE); #if !(EXT_INT9_PIN == PIN_7_ONLY) attachInterrupt(digitalPinToInterrupt(5), button5_ISR, CHANGE); Serial.println("button5_ISR attached to pin 5"); #endif #if (EXT_INT9_PIN == PIN_5_7_SAME_ISR) attachInterrupt(digitalPinToInterrupt(7), button5_ISR, CHANGE); Serial.println("button5_ISR attached to pin 7"); #elif !(EXT_INT9_PIN == PIN_5_ONLY) attachInterrupt(digitalPinToInterrupt(7), button7_ISR, CHANGE); Serial.println("button7_ISR attached to pin 7"); #endif Serial.println("setup() completed, starting loop(), check each pin"); } void loop() { // empty right now, add code as needed. if ( (!reported) && (activeint >= 0) ) { if (activeint) Serial.printf("Interrupt from pin A%d\n", activeint); else Serial.println("Interrupt from pin A0 or A1"); reported = true; } }
Source: 14_interrupts in xiao_m0_sketches on GitHub.

Just connect each pin to ground to test the interrupt service routine (ISR) for that pin. While a pin is grounded, the yellow LED should be on. When the pin is first connected to ground and then when it is disconnected, a State of pin Ax changed message should appear in the serial monitor. Depending on the value of the EXT_INT9_PIN directive, the interrupt attached to pin 5 and pin 7 may or may not work. An interrupt cannot be shared among pins so only one of pin 5 or 7 can have an active interrupt. On the other hand, distinct interrupts can have a common service routine. Look at pins 0 and 1 which have the same ISR in the program.

Timers toc

We have already worked with timers albeit without it being obvious. Pulse width modulation depends on timers.

There are two timer-based blink sketches in the Arduino IDE example sketches for the Seeeduino XAIO. Try them (File/Examples/ scroll down to Examples for Seeeduino XAIO and select TimerTC3/ISRBlink and TimerTCC0/ISRBlink), they both work. I combined the two, with the choice of the timer done with a macro directive and tried to implement the asymmetric on and off behaviour of the previous blink sketches.

#include <Arduino.h> // select the timer to use #define TIMER_TC3 0 // use a 16 bit resolution timer #define TIMER_TCC0 1 // use a 24 bit resolution timer #define USE_TIMER TIMER_TC3 // set on and off times in microseconds #define ON_TIME 750000 // Values in the 750000 to 1250000 #define OFF_TIME 1250000 // range work reliably with both timers #if USE_TIMER == TIMER_TC3 #include <TimerTC3.h> #define TIMER TimerTc3 #elif USE_TIMER == TIMER_TCC0 #include <TimerTCC0.h> #define TIMER TimerTcc0 #else #ERROR "No timer defined" #endif unsigned long period[] {OFF_TIME, ON_TIME}; volatile int isLEDOn = 0; void timerIsr() { digitalWrite(LED_BUILTIN, isLEDOn); isLEDOn = 1 - isLEDOn; TIMER.setPeriod(period[isLEDOn]); } unsigned long timer; void setup() { // Time to upload new firmware delay(8000); // Do not wait for a USB connection Serial.println("\nStarting setup()."); Serial.printf("Using timer %s", #if USE_TIMER == TIMER_TC3 "TimerTc3" #else "TimerTcc0" #endif ); Serial.printf("\nOn/Off times in microseconds: %d, %d.\n", (int) period[true], (int) period[false]); Serial.printf("On/Off times in milliseconds: %d, %d.\n", (int) period[true]/1000, (int) period[false]/1000); pinMode(LED_BUILTIN, OUTPUT); // initialize yellow user LED digitalWrite(LED_BUILTIN, HIGH); // turn off pinMode(PIN_LED_TXL, INPUT); // turn off USB TX LED pinMode(PIN_LED_RXL, INPUT); // turn off USB RX LED Serial.printf("Set LED_BUILTIN (%d) to OUTPUT and turned off USB LEDs.\n", LED_BUILTIN); TIMER.initialize(OFF_TIME); TIMER.attachInterrupt(timerIsr); timer = millis(); Serial.println("setup() completed, starting loop()."); } int oldState = isLEDOn; void loop() { noInterrupts(); int state = isLEDOn; interrupts(); if (state != oldState) { Serial.printf("%.8u %s (was %s for: %u ms).\n", millis(), (state) ? "ON " : "OFF", (state) ? "off" : "on ", millis() - timer); oldState = state; timer = millis(); } }
Source: 15_timers in xiao_m0_sketches on GitHub.

See the TimerOne & TimerThree Libraries example program from PJRC for an excellent explanation of the use of the volatile type and of the need to disable interrupts in the main loop. Note how the mode of the PIN_LED_TXL pin is set to INPUT. Otherwise the blue TX LED will flash at each character written to the serial monitor which distracts from the flashing yellow LED. The output in the serial monitor confirms the timing values set.

Starting setup(). Using timer TimerTcc0 On/Off times in microseconds: 750000, 1250000. On/Off times in milliseconds: 750, 1250. Set LED_BUILTIN (13) to OUTPUT and turned off USB LEDs. setup() completed, starting loop(). 00009251 ON (was off for: 1250 ms). 00010001 OFF (was on for: 750 ms). 00011251 ON (was off for: 1250 ms). 00012001 OFF (was on for: 750 ms). 00013251 ON (was off for: 1250 ms). 00014001 OFF (was on for: 750 ms). 00015251 ON (was off for: 1250 ms). 00016001 OFF (was on for: 750 ms). 00017251 ON (was off for: 1250 ms). 00018001 OFF (was on for: 750 ms). ...

While this may look good, to be honest, I am very dissatisfied with this presentation of the SAM timers. I seem to be running into trouble when using the timers. Just changing the timer interval gives unexpected results with either timer.

Using timer TimerTc3 On/Off times in microseconds: 75000, 1250000. On/Off times in milliseconds: 75, 1250. Set LED_BUILTIN (13) to OUTPUT and turned off USB LEDs. setup() completed, starting loop(). 00009251 ON (was off for: 1250 ms). 00010451 OFF (was on for: 1200 ms). 00011701 ON (was off for: 1250 ms). 00012901 OFF (was on for: 1200 ms). 00014151 ON (was off for: 1250 ms). 00015351 OFF (was on for: 1200 ms). Using timer TimerTcc0 On/Off times in microseconds: 75000, 1250000. On/Off times in milliseconds: 75, 1250. Set LED_BUILTIN (13) to OUTPUT and turned off USB LEDs. setup() completed, starting loop(). 00009251 ON (was off for: 1250 ms). 00009551 OFF (was on for: 300 ms). 00010801 ON (was off for: 1250 ms). 00011101 OFF (was on for: 300 ms). 00012351 ON (was off for: 1250 ms). 00012651 OFF (was on for: 300 ms).

Even worse, intializing timer TimerTc3 with a 500 ms time will work, but it will crash when TimerTcc0 is used.

Using timer TimerTc3 On/Off times in microseconds: 1250000, 500000. On/Off times in milliseconds: 1250, 500. Set LED_BUILTIN (13) to OUTPUT and turned off USB LEDs. setup() completed, starting loop(). 00008501 ON (was off for: 500 ms). 00009751 OFF (was on for: 1250 ms). 00010251 ON (was off for: 500 ms). 00011501 OFF (was on for: 1250 ms). ... Using timer TimerTcc0 On/Off times in microseconds: 1250000, 500000. On/Off times in milliseconds: 1250, 500. Set LED_BUILTIN (13) to OUTPUT and turned off USB LEDs. ... XIAO frozen

The print statement after the time initialization was ignored, but the yellow LED was turned on and remained on. The communication port, /dev/ttyACM0 on my system, was still up but firmware could not be uploaded to the XIAO until it was put in bootloader mode. In short, I have no idea what happened. Unfortunately, I cannot find information about the TimerTC3 and TimerTCC0 libraries which are only present in the Seeed Studio fork of the SAMD Arduino core. There are libraries that I should look at in the future.

Again, any help would be much appreciated.

Watchdog toc

The SAMD 21 microcontroller does have a hardware watchdog, but it does not seem to be fully supported in the Arduino core. At least, I could not find a function to start it and another to reset it to prevent it from restarting the microcontroller. As often the case, others have stepped in and provided the missing bits which takes care of the awful details.

The last two libraries are obtainable with the PlatformIO manager. As for the Arduino IDE, but only the Adafruit library can be installed directly with the library manager; SAMCrashMonitor must be installed manually. As for the code by Adrian Admaski, it was a simple task to transform it into a very small library. Here are the header file samdwdt.h and implementation samdwdt.cpp.

#pragma once voidwdtFeed(void);

#include <Arduino.h> #include "samdwdt.h" #define GLCK_BASE ((uint8_t*)(0x40000C00)) #define GLCK_GENCTRL *((uint32_t*)(GLCK_BASE + 0x4)) #define GLCK_GENDIV *((uint32_t*)(GLCK_BASE + 0x8)) #define WDT_BASE ((uint8_t*)(0x40001000)) // WDT #define WDT_CONFIG *((uint8_t*)(WDT_BASE + 0x1)) // #define WDT_CTRL *((uint8_t*)(WDT_BASE + 0x0)) #define WDT_CLEAR *((uint8_t*)(WDT_BASE + 0x08)) bool wdtInitiated = false; void wdtInit() { GLCK_GENCTRL = (0x2 << 0) | (1 << 20) | (0x03 << 8) | (1 << 16); // Select generic clock 2 | Divide the clock by 2^(GENDIV.DIV+1) | Set the clock source to OSCULP32K | Enable GCLK2 GLCK_GENDIV = (0x2 << 0) | (0x3 << 8); // Select generic clock 2 | 8 seconds time-out period WDT_CONFIG |= (0x6 << 0); // 512 Hz WDT_CTRL |= (1 << 1); // Enable the watchdog wdtInitiated = true; } void wdtFeed(void) { if (wdtInitiated) WDT_CLEAR |= (0xA5); else wdtInit(); }
Source: 16_basicwdt in xiao_m0_sketches on GitHub.

Here is the serial output from running the test code.

19:27:19.589 -> Starting setup() 19:27:19.589 -> Watchdog initiated 19:27:19.589 -> setup() completed, starting loop() 19:27:26.599 -> Feeding watchdog after 7 seconds - countdown: 5 19:27:33.677 -> Feeding watchdog after 7 seconds - countdown: 4 19:27:40.687 -> Feeding watchdog after 7 seconds - countdown: 3 19:27:47.760 -> Feeding watchdog after 7 seconds - countdown: 2 19:27:54.766 -> Feeding watchdog after 7 seconds - countdown: 1 19:27:54.766 -> No longer feeding watchdog 19:27:54.865 -> 100 ms 19:27:54.964 -> 200 ms 19:27:55.064 -> 300 ms ... 19:28:02.762 -> 8000 ms 19:28:02.861 -> 8100 ms 19:28:11.792 -> 19:28:11.792 -> Starting setup() 19:28:11.792 -> Watchdog initiated 19:28:11.792 -> setup() completed, starting loop() 19:28:18.897 -> Feeding watchdog after 7 seconds - countdown: 5

Serial Communication toc

The SAMD microcontroller comes with the usual array of serial communication buses. All boards of the XIAO family provide one UART, one I²C and one SPI bus by default. In this section each of these will be examined in turn. This is only scratching the surface of the capabilities of the chip which I have investigated at length in other posts, notably Seeeduino XIAO Serial Communication Interfaces (SERCOM)The XIAO and Three, Nay, Four Hardware Serial Ports on a SAM D21 XIAO.

UART toc

For a first look at the hardware serial port (UART), let's run a simple loop back test. That means that pin A6 which is the UART transmit pin must be connected to the receive pin A7 of the same UART.

#include <Arduino.h> #define SERIAL_BAUD 115200 #define WRITE_INTERVAL 3000 // transmit every 3 seconds unsigned long writetimer; void setup() { // Time to upload new firmware delay(8000); // Wait for a USB connection Serial.begin(0); while (!Serial) delay(10); Serial.println("\nStarting setup()."); Serial1.begin(SERIAL_BAUD); while (!Serial1) delay(10); Serial.printf("Hardware serial port (Serial1) initialized with %d baud.\n", SERIAL_BAUD); Serial.println("TX pin A6 must be connected to RX pin A7."); pinMode(PIN_LED_TXL, INPUT); // turn off USB activity LED pinMode(LED_BUILTIN, OUTPUT); Serial.println("setup() completed, starting loop()."); writetimer = millis(); } int count = 0; void loop() { while (Serial1.available()) { digitalWrite(LED_BUILTIN, LOW); Serial.write(Serial1.read()); delay(2); // otherwise the flashng LED is hardly visible digitalWrite(LED_BUILTIN, HIGH); } if (millis() - writetimer > WRITE_INTERVAL) { count++; Serial.printf("\nTransmitting \"Message %d\"\n", count); Serial1.printf("Message %d\n", count); writetimer = millis(); } }
Source: 17_uart in xiao_m0_sketches on GitHub.

The result shows that the serial communication works.

Starting setup(). Hardware serial port (Serial1) initialized with 115200 baud. TX pin A6 must be connected to RX pin A7. setup() completed, starting loop(). Transmitting "Message 1" Message 1 Transmitting "Message 2" Message 2 Transmitting "Message 3" Message 3 Transmitting "Message 4" ...

It is possible to have more than one hardware serial port if the I²C or the SPI bus is not needed. See Three, Nay, Four Hardware Serial Ports on a SAM D21 XIAO.

SPI toc

In the original version of this section, the XIAO SPI bus was examined with a logic analyzer. I forgot that the SPI serial port can be tested in a loop back configuration just like the UART. My excuse is that I did not think to test the UART port at the time. As one would expect in this type of test, the serial output pin is connected to the serial input pin. More pedantically, the SPI data output pin, A10 or MOSI (for Master Out Slave In), is connected to the SPI data input pin A9 or MISO (for Master In Slave Out). There is no need to worry about the SPI clock signal and the chip select (also called the slave select) signal since these are meant for the slave device. Here is the output of the little test program.

Starting setup(). Initializing SPI port MOSI (output pin): 10 MISO (input pin): 9 SCK (clock): 8 CS (select): 3 setup() completed, starting loop(). Transmitting "Message 1" Received: "Message 1" Transmitting "Message 2" Received: "Message 2" ... letting MISO (A9) float Transmitting "Message 6" Received: " 0xf0 0x0f 0xc0 0x7f @ 0xff 0x00 0xff 0x00 " Transmitting "Message 7" Received: " 0x00 0x05 0x00 s 0x00 f 0x00 0x00 " ... connecting MISO (A9) to 3.3 volts Transmitting "Message 13" Received: " 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff " Transmitting "Message 14" Received: " 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff " ... connecting MISO (A9) to ground Transmitting "Message 21" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 " Transmitting "Message 22" Received: " 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 "

The received message was exactly what was sent which is what was expected. Then the MISO pin was disconnected from the MOSI pin and left unconnected to anything for a short while (the pin was said to "float" in the jargon). Not surprisingly garbage data was read in as each character of the output message was transmitted. Most did not correspond to printable ASCII characters and their numeric value is shown in hexadecimal notation. Then the SPI data input pin was connected to the 3.3 volt rail so that the input value of each character was 0xFF corresponding to all bits being equal to 1. Finally, the MISO was tied to ground and all received bytes were then equal to 0.

Here is the source of the SPI loop back test.

#include <SPI.h> const int CS = A3; // SS = A4 = 4 but that would mean loosing I²C port void setup (void) { // Time to upload new firmware delay(8000); // Wait for a USB connection Serial.begin(0); while (!Serial) delay(10); Serial.println("\nStarting setup()."); Serial.println("Initializing SPI port"); Serial.printf(" MOSI (output pin): %d\n", MOSI); Serial.printf(" MISO (input pin): %d\n", MISO); Serial.printf(" SCK (clock): %d\n", SCK); Serial.printf(" CS (select): %d\n", CS); pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); // disable Slave Select SPI.begin(); SPI.setClockDivider(SPI_CLOCK_DIV8); //divide the clock by 8 Serial.println("setup() completed, starting loop()."); } #define BUFFER_SIZE 32 char outBuffer[BUFFER_SIZE] = {0}; char inBuffer[BUFFER_SIZE] = {0}; int count = 0; void loop (void) { digitalWrite(CS, LOW); // enable Slave Select count++; int n = snprintf(outBuffer, BUFFER_SIZE, "Message %d", count); Serial.printf("\nTransmitting \"%s\"\n", outBuffer); for (int i = 0; i < n; i++) { inBuffer[i] = SPI.transfer(outBuffer[i]); } digitalWrite(CS, HIGH); // disable Slave Select Serial.print("Received: \""); for (int i=0; i < n; i++) { char c = inBuffer[i]; if ((31 < c) && (c < 127)) Serial.print(c); else Serial.printf(" 0x%.2x ", c); } Serial.println("\""); memset(inBuffer, 0, sizeof(inBuffer)); delay(1000); }
Source: 18_spi_test in xiao_m0_sketches on GitHub.

I²C toc

The I²C bus has a single bidirectional data signal which means that a simple loop back test cannot be performed. Instead of a test, I will present a digital clock program where the XIAO is connected to a couple of I²C devices: an 0.96" OLED display and a module with a real-time clock (RTC). The ubiquitous OLED display is a no name 128 by 64 pixels SSD1306 while the RTC is based on the DS3231. The connections between the devices are straight forward: connect all SDA pins together, all SCL pins together, ground pins together and Vcc pins together.

      XIAO     OLED      RTC & EEPROM
    -------    ----      ------------
        GND-----GND------GND
       3.3V-----VCC------VCC
    A4(PA8)-----SDA------SDA
    A5(PA9)-----SCL------SCL

Let's start with a preliminary sketch to scan the I²C bus to ensure that the connected devices can be found and to obtain their I²C address. This can be done with most versions of the i2c_scanner sketches that abound on the Web. I used a version by Thomas Feldmann with slight adjustments in the setup() function. Also #include <Arduino.h> was added so that the utility will compile and run in PlatformIO as well as in the Arduio IDE.

#include <Arduino.h> #include <Wire.h> void setup() { // Wait for the serial connection Serial.begin(9600); // baud meaningless for SAMD21 boards and while (!Serial) delay(10); // others with serial over USB CDC Wire.begin(); Serial.println("\nI2C Scanner"); }
Source: 19_i2c_scan in xiao_m0_sketches on GitHub.

This is the output in the IDE serial monitor.

Scanning... I2C device found at address 0x3C ! I2C device found at address 0x57 ! I2C device found at address 0x68 ! done

In the previous edition of this post, the U8g2 library by olikraus was used to control the display. This time around SSD1306Ascii by Bill Greiman is used. As for the real-time clock, the library used is RTC by Michael C. Miller. The code itself is a much simplified version of the DS3231_Simple example sketch.

#include <Arduino.h> #include <Wire.h> // private libraries #include "src/RTC/src/RtcDS3231.h" #include "src/SSD1306Ascii/src/SSD1306Ascii.h" #include "src/SSD1306Ascii/src/SSD1306AsciiWire.h" // -- OLED params -- // set to true if the I2C pins are above the display area // set to false if the I2C pins are below the display area #define PINS_AT_TOP false // 0X3C+SA0 - 0x3C or 0x3D #define I2C_ADDRESS 0x3C // Define proper RST_PIN if required. #define RST_PIN -1 // -- Time params -- const char* dow[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; const char* month[] = { "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; #define TIME_STR "%02d:%02d", now.Hour(), now.Minute() #define DATE_STR "%s %d, %d", month[now.Month()], now.Day(), now.Year() RtcDS3231<TwoWire> rtc(Wire); SSD1306AsciiWire oled; void setup() { delay(4000); // nothing written to serial monitor, start right away! Wire.begin(); Wire.setClock(400000L); RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__); if (!rtc.GetIsRunning()) rtc.SetIsRunning(true); if (rtc.GetDateTime() < compiled) rtc.SetDateTime(compiled); // never assume the rtc was last configured by you, so // just clear them to your needed state rtc.Enable32kHzPin(false); rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); #if RST_PIN >= 0 oled.begin(&Adafruit128x64, I2C_ADDRESS, RST_PIN); #else oled.begin(&Adafruit128x64, I2C_ADDRESS); #endif #if (PINS_AT_TOP != true) oled.displayRemap(true); #endif } // center the given string in the display void center(char* str) { oled.setCol( (oled.displayWidth() - oled.strWidth(str))/2 ); oled.print(str); } // redraw the screen once a minute, when its value changes int prevminute = -1; void loop () { // communicate with RTC over I²C RtcDateTime now = rtc.GetDateTime(); // wait for a change in the minute while (now.Minute() == prevminute) { delay(5); now = rtc.GetDateTime(); } prevminute = now.Minute(); // communicate with display over I²C oled.clear(); oled.setFont(Verdana_digits_24); char strBuffer[28]; // display time sprintf(strBuffer, TIME_STR); oled.setRow(0); center(strBuffer); oled.setFont(Verdana12); // display day of week strcpy(strBuffer, dow[now.DayOfWeek()]); oled.setRow(4); center(strBuffer); oled.println(); // display date sprintf(strBuffer, DATE_STR); center(strBuffer); delay(59000); // wait almost a minute before repeating }
Source: 20_i2c_clock in xiao_m0_sketches on GitHub.

The size of the firmware when compiled with the two platforms is not the same. Thinking that this difference could result from the use of different versions of the SAMD Arduino Core in the two IDEs, I compiled the sketch with the Arduino IDE using the older version of the core that is installed in PlatformIO. Again the Arduino version was bigger which could perhaps be attributed to the number of libraries linked into the program. Or it could have something to do with optimization flags, debugging information and so on. I should test this again with Version 2.0 of the Arduino IDE before drawing any conclusions.

This program is by no means a true digital clock. Most obviously, there is no way to set or modify the time. Nevertheless, it does show that the I²C interface of the XIAO is functional.

I²C Light Sensor using a Seeeduino XIAO-> <-First Look at the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO
Seeeduino XIAO Serial Communication Interfaces (SERCOM)->