2020-05-04
md
Seeeduino XIAO Serial Communication Interfaces (SERCOM)
<-I²C Light Sensor using a Seeeduino XIAO
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO

There are some quite good explanations on how to add a serial communication interface on a SAM D21 based board. I will link three from "big players" in the IoT world: Arduino: Adding more Serial Interfaces to SAMD microcontrollers, Sparkfun: Adding More SERCOM Ports for SAMD Boards and Adafruit: Using ATSAMD21 SERCOM for more SPI, I2C and Serial ports. They are certainly worth reading, but they are not germane to the Seeeduino XIAO. I will explain why and how to add a supplementary SPI, I²C or USART port on the XIAO if it is possible to forego one of the other predefined ports.

Table of contents

  1. SAMD 21G Serial Communication Interfaces
  2. Seeeduino XIAO SERCOM
  3. Two I²C Ports on the Seeeduino XIAO
  4. Modifying the XIAO variant Files
  5. Using the Modified XIAO variant in PlatformIO
  6. Using the Modified XIAO variant in the Arduino IDE
  7. A Word to the Wise

SAMD 21G Serial Communication Interfaces toc

There are six serial communication interfaces (SERCOMx, x=0,...,5) on the SAM D21G, each of which can handle any one of four protocols: classic serial communication (full-duplex USART or half-duplex single wire), I²C (master or slave), SPI or Lin (slave). I had to look up LIN (Local Interconnect Network) which turned out to be a serial communication protocol used in vehicles. The XIAO firmware creates three serial ports: USART, I²C and SPI with three of the interfaces. The fourth is used for the USB connection, the fifth for the two-pin serial wire debug (SWD) interface on the back side of the board. As far as I can tell SERCOM5 is not physically connected.

Not only can the protocol of a serial communication interface be changed, but the connections of each interface can be multiplexed onto different sets of pins. Each interface has four "pads" or connections associated with a primary set of four pins and with a second, alternate, set of pins. Actually, SERCOM 4 and 5 have three complete sets of alternate pad assignments and SERCOM3 has a partial second alternate assignment. The table below presents part of the data in Table 7-1. PORT Function Multiplexing for SAM D21 A/B.CD Variant Devices and SAM DA1 A/B Variant Devices.

InterfacePads
01230'1'2'3'0"1"2"3"
SERCOM0PA08PA09PA10PA11
ALT-SERCOM0PA04PA05PA06PA07
SERCOM1PA16PA17PA18PA19
ALT-SERCOM1PA00PA01PA30PA31
SERCOM2PA12PA13PA14PA15
ALT-SERCOM2PA08PA09PA10PA11
SERCOM3PA22PA23PA24PA25
ALT-SERCOM3PA16PA17PA18PA19PA20PA21
SERCOM4PB12PB13PB14PB15
ALT-SERCOM4PA12PA13PA14PA15PA12PA13PA14PA15PB08PB09PB10PB11
SERCOM5PB16PB17PA20PA21
ALT-SERCOM5PA22PA23PA24PA25PB02PB04PB22PB13PB30PB31PB00PB01

Seeeduino XIAO SERCOM toc

Of course the flexibility is much reduced with the Seeeduino XIAO which brings out only 11 pins. Here is the relevant information from the previous table, using the Arduino pin labels.

InterfacePadsDefault
Protocol
0123
SERCOM0A4A5A2A3
ALT-SERCOM0A1A9A10A8SPI
ALT-SERCOM2A4A5A2A3I²C
ALT-SERCOM4A6A7USART

Be careful when interpreting the data in the table. SERCOM0 and ALT-SERCOM0 refer to the same serial interface; the ALT is just an indication that an alternate set of pad connections is used. Had Seeed Studio decided to use SERCOM0 with its primary pad assignment for the I²C bus, then it would not have been possible to have a SPI bus. There is no possibility of adding a serial interface on the XIAO. There are three, or more precisely two and a half, available serial interfaces on the pins of the device, SERCOM0, SERCOM2 and SERCOM4 so that all one can do is to modify the communication protocol of the interfaces if more than one SPI, I²C or USART channel is needed. While that means that there are 3×3×2 = 18 possibilities (SERCOM4 cannot be used for as a full-duplex SPI), there is not much to be gained by inverting say the SPI and I²C bus. Indeed whenever possible, the pin assignment for the I²C bus should not be changed from the default assignment because of the electrical characteristics of the pins. The table below summarizes the 9 remaining possibilities.

ProtocolsSERCOM0SERCOM2SERCOM4
3×USARTUSARTUSARTUSART
3×I²CI²CI²CI²C
2×SPI + (1×USART or 1×I²C)SPISPIUSART
SPISPII²C
2×I²C + (1×USART or 1×SPI)I²CI²CUSART
SPII²CI²C
2×USART + (1×SPI or 1×I²C)USARTI²CUSART
SPIUSARTUSART
1×SPI + 1×I²C + 1×USARTSPII²CUSART

It might be possible to set up a half SPI interface on SERCOM4 with just a SCLK and MISO or MOSI signal; I have not investigated that possibility.

Two I²C Ports on the Seeeduino XIAO toc

The following example shows how to create two hardware I²C channels on a XIAO. The "standard" I²C pins A4 and A5 (SDA and SCL respectively) are used to drive an I²C OLED display. The second I²C port will be used to connect to a Raspberry Pi as a slave device. Since the default I²C interface is on SERCOM2, the second port can be on SERCOM0 (SDA on A1, SCL on A9) sacrificing the SPI interface or on SERCOM4 (SDA on A6, SCL on A7) sacrificing the USART interface. The two circuits are shown below. Note how the XIAO and display are powered from the Pi 5 volt output.

Here is a sketch that replaces the SPI protocol on SERCOM0 or the USART channel on SERCOM4 with a second I²C instance.

/* * two_hdw_i2c   */ #include <Arduino.h>            // Needed for PlatformIO #include <Wire.h>               // I2C library #include "wiring_private.h"     // for pinPeripheral() function     #include <U8g2lib.h>            // I2C OLED display #define BAUD          115200    // Baud for serial port (/dev/ttyACM0 on Linux) #define DATA_DELAY       667    // Delay (ms) between output value updates #define REFRESH_DELAY   2000    // Delay (ms) between dispaly updates #define I2C_SLAVE_ADDRESS  4    // arbitrary choice in range (0x03 to 0x77) different from other devices on bus #define USE_SERCOM0             // Comment out to set up I2C on SERCOM4 which, by default, *** WILL NOT WORK *** #ifdef USE_SERCOM0 #define ALT_SERCOM         sercom0 #define ALT_SERCOM_SDA     1        // SERCOM0, PAD0 #define ALT_SERCOM_SCL     9        // SERCOM0, PAD1 #else #define ALT_SERCOM         sercom4 #define ALT_SERCOM_SDA     6       // SERCOM4, PAD0 #define ALT_SERCOM_SCL     7       // SERCOM4, PAD1 #endif // Using hardware I2C bus with noname 128x64 0.96" OLED display, this will use Wire object and sercom2 U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE); volatile int serviceCount = 0; TwoWire myWire(&ALT_SERCOM, ALT_SERCOM_SDA, ALT_SERCOM_SCL); extern "C" {  #ifdef USE_SERCOM0  void SERCOM0_Handler(void) {  #else  void SERCOM4_Handler(void) {  #endif      serviceCount++;          // to check this handler is used    myWire.onService();  } } int dataValue; // request data block handler void requestEvent(void) {  int level = dataValue;              myWire.write((level >> 8) & 0xFF);      // send higher 8 bits of data  myWire.write(level & 0xFF);             // send lower 8 bits of data   } void setup() {  // Start serial port, waiting up to 20 seconds  Serial.begin(BAUD);  unsigned long startserial = millis();  while (!Serial && (millis() - startserial < 20000)) delay(10);  // start second Wire object to respond to the Raspberry Pi  myWire.begin(I2C_SLAVE_ADDRESS);  pinPeripheral(ALT_SERCOM_SDA, PIO_SERCOM_ALT);  pinPeripheral(ALT_SERCOM_SCL, PIO_SERCOM_ALT);  myWire.onRequest(requestEvent);    // setup the OLED display and first Wire object  u8x8.begin();  u8x8.setPowerSave(0);  u8x8.setFont(u8x8_font_chroma48medium8_r);    // End of setup  Serial.println("Test of two hardware I2C buses");  Serial.printf("\n1st hardware I2C bus: SDA: A%d, SCL: A%d\n", SDA, SCL);  Serial.println("using sercom2 in master mode <--> LCD Display\n");    Serial.printf("\n2nd hardware I2C bus: SDA: A%d, SCL: A%d\n", ALT_SERCOM_SDA, ALT_SERCOM_SCL);  Serial.print("using ");  #ifdef USESERCOM0  Serial.print("sercom0");  #else  Serial.print("sercom4");  #endif  Serial.println(" in slave mode <--> Raspberry Pi\n");  Serial.println("\nWaiting for data requests"); } int runcount = 0; char buf[200]; unsigned long dataReadTime = 0;  // LDR read timer unsigned long refreshTime = 0;  // LCD refreh timer void loop(){  if (millis() - dataReadTime >= DATA_DELAY) {    // generate random data as if reading a sensor of some type    if (runcount == 0)      dataValue = random(4096);    runcount++;    if (runcount > 12) runcount = 0;  // give myself time to compare the data on the display and on the Raspberry Pi    // restart the timer    dataReadTime = millis();  }  if (millis() - refreshTime >= REFRESH_DELAY) {       // update the display    sprintf(buf, "data value: %d", dataValue);    u8x8.clearLine(0);    u8x8.drawString(0,0, buf);    Serial.printf("dataValue: %d, service count: %d\n", dataValue, serviceCount);    // restart the timer    refreshTime = millis();  } }

This sketch can be downloaded by clicking on this link: two_hdw_i2c.ino. A Python script such as light_sensor.py described in I²C Light Sensor using a Seeeduino XIAO On the Raspberry Pi will display the data generated by the sketch.

Note how easy it was to set up the second I²C instance in lieu of the SPI instance on SERCOM0:

  1. Create a TwoWire instance specifying the serial communication interface and the pad 0 and pad 1 connections: TwoWire myWire(sercom0, A1, A9).
  2. Set the TwoWire onService method as the SERCOM0 interrupt handler.
  3. Start the TwoWire object in slave mode specifying the I²C address: myWire.begin(4);.
  4. Assign the physical pins to the serial interface: pinPeripheral(A1, PIO_SERCOM_ALT); pinPeripheral(A2, PIO_SERCOM_ALT);.

Unfortunately, when the very same thing is attempted, except for adding the extra I²C instance on SERCOM4, there is a linking problem.

Linking everything together... ... /tmp/arduino_build_733537/core/variant.cpp.o: In function `SERCOM4_Handler': variant.cpp:(.text.SERCOM4_Handler+0x0): multiple definition of `SERCOM4_Handler' /tmp/arduino_build_733537/sketch/two_hdw_i2c.ino.cpp.o:two_hdw_i2c.ino.cpp:(.text.SERCOM4_Handler+0x0): first defined here...

Indeed we do find the following assignment in .../packages/Seeeduino/hardware/samd/1.7.0/variants/XIAO_m0/variant.cpp.

Uart Serial1( &sercom4, PIN_SERIAL1_RX, PIN_SERIAL1_TX, PAD_SERIAL1_RX, PAD_SERIAL1_TX ) ; void SERCOM4_Handler() {  Serial1.IrqHandler(); }

If those few lines are removed from the variant.cpp file, then the sketch will compile and run correctly as long as the OLED SDA and SCL pins are now connected to pins A6 and A7 of the XIAO. The lesson learned from this experiment is that it was necessary to do some "surgery" to the variant.cpp file to prevent the creation of the Serial1 object and assignment of its interrupt handler.

Modifying the XIAO variant Files toc

Wholesale removal of the Serial1 Uart from the variant files is not the best way to go. What if the USART is needed in another project? Setting up 9 different variant.h and variant.cpp files to take care of all the possible permutations of serial protocols on the XIAO is not practical. Then I thought of using macros to select one of the nine possibilities outlined above, but that's just as awkward. Instead, I decided to add a single macro NO_USART_INTERFACE to excise the Serial1 object when another type of serial interface is needed on SERCOM4. As shown in the above sketch, it is always possible to modify the serial instance of SERCOM0 and SERCOM2 if needed.

Here is the last part of the variant.h file with the preprocessor macro appearing twice to avoid moving the couple of lines defining the SERIAL_PORT_HARDWARE and SERIAL_PORT_HARDWARE_OPEN macros to the first appearance of NO_USART_INTERFACE.

// Serial ports // ------------ #ifdef __cplusplus #include "SERCOM.h" // Instances of SERCOM extern SERCOM sercom0; extern SERCOM sercom1; extern SERCOM sercom2; extern SERCOM sercom3; extern SERCOM sercom4; extern SERCOM sercom5; #ifndef NO_USART_INTERFACE #include "Uart.h" // Serial1 extern Uart Serial1; #define PIN_SERIAL1_TX (6ul) #define PIN_SERIAL1_RX (7ul) #define PAD_SERIAL1_TX (UART_TX_PAD_0) #define PAD_SERIAL1_RX (SERCOM_RX_PAD_1) #endif // not NO_USART_INTERFACE #endif // __cplusplus // These serial port names are intended to allow libraries and architecture-neutral // sketches to automatically default to the correct port name for a particular type // of use. For example, a GPS module would normally connect to SERIAL_PORT_HARDWARE_OPEN, // the first hardware serial port whose RX/TX pins are not dedicated to another use. // // SERIAL_PORT_MONITOR Port which normally prints to the Arduino Serial Monitor // // SERIAL_PORT_USBVIRTUAL Port which is USB virtual serial // // SERIAL_PORT_LINUXBRIDGE Port which connects to a Linux system via Bridge library // // SERIAL_PORT_HARDWARE Hardware serial port, physical RX & TX pins. // // SERIAL_PORT_HARDWARE_OPEN Hardware serial ports which are open for use. Their RX & TX // pins are NOT connected to anything by default. #define SERIAL_PORT_USBVIRTUAL SerialUSB #define SERIAL_PORT_MONITOR SerialUSB #ifndef NO_USART_INTERFACE #define SERIAL_PORT_HARDWARE Serial1 #define SERIAL_PORT_HARDWARE_OPEN Serial1 #endif // Alias Serial to SerialUSB #define Serial SerialUSB

The changes to variant.cpp is straightforward.

#ifndef NO_USART_INTERFACE Uart Serial1( &sercom4, PIN_SERIAL1_RX, PIN_SERIAL1_TX, PAD_SERIAL1_RX, PAD_SERIAL1_TX ) ; void SERCOM4_Handler() { Serial1.IrqHandler(); } #endif

Of course, the NO_USART_INTERFACE macro is not defined in these files because it needs to be defined only when needed.

Using the Modified XIAO variant Files in PlatformIO toc

In PlatformIO (see Adding the Seeeduino XIAO in PlatformIO), those changes to the two variant files in the <.platformio>/packages/framework-arduino-samd/variants/XIAO_m0/ directory are just about the only thing needed. The two_hdw_i2c sketch will compile and run in PlatformIO using SERCOM4 (remove or comment out the USE_SERCOM0 macro in the sketch) with the following platformio.ini configuration file.

[env:seeeduino_xiao] platform = atmelsam board = seeeduino_xiao framework = arduino monitor_speed = 115200 build_flags = -DNO_USART_INTERFACE

The modified variant.h and variant.cpp files are available in the xiao__variants.zip archive.

Using the Modified XIAO variant Files in the Arduino IDE toc

It is a bit more complicated to use the modified variant file in the Arduino IDE. The simplest is to add a platform.local.txt file in the .../arduino-1.8.10/portable/packages/Seeeduino/hardware/samd/1.7.2/ directory alongside the platform.txt file to override a number of directives in the later file.

# platform.local.txt # # Override compiler.cpp.extra_flags in boards.txt. # # For more info: # https://arduino.github.io/arduino-cli/platform-specification/ compiler.cpp.extra_flags=-DNO_USART_INTERFACE

This works, but it is not very convenient because this configuration file is in force at the board level. Something equivalent to the platformio.ini file which is in effect at the sketch level only would be preferable. The best I could come up with was adding an option in the Tools menu for the XIAO board.

Setting SERCOM4 to "None" instead of "USART" means that the NO_USART_INTERFACE macro will be defined. Adding this menu option requires changes to the boards.txt file.

  1. The new menu entry was added at the start of the boards.txt. The part to the right of the equal sign is the option text displayed in the Tools IDE menu.

    menu.sercom4=SERCOM4
  2. The possible option values were added at the end of the Seeed XIAO M0 (SAMD21) definition.

    seeed_XIAO_m0.menu.sercom4.include=USART seeed_XIAO_m0.menu.sercom4.include.sercom4_flag= seeed_XIAO_m0.menu.sercom4.exclude=None seeed_XIAO_m0.menu.sercom4.exclude.sercom4_flag=-DNO_USART_INTERFACE

    The first and third lines define the two values, "USART" and "None", which appear in the SERCOM4 submenu. The second and fourth line take care of assigning a value to sercom4_flag depending on which choice is made. That value will either be an empty string or the compiler directive -DNO_USART_INTERFACE.

  3. The sercom4_flag variable is added to the seeed_XIAO_m0.build.extra_flags= line which is found about 15 lines above the added menu choices.
    seeed_XIAO_m0.build.extra_flags= -DARDUINO_SAMD_ZERO -D__SAMD21__ -D__SAMD21G18A__ -DARM_MATH_CM0PLUS -DSEEED_XIAO_M0 {build.usb_flags} {sercom4_flag}

These changes are better than manually editing the platform.local.txt file but definitely not the best solution. And that's because the IDE does not save the menu choices for each sketch, but only for the last sketch edited in the IDE. It would be a good policy to add a comment at the start of each XIAO sketch about the correct setting for the new menu choice in XIAO sketches.

The modified variant.h, variant.cpp and boards.txt are available in the xiao_boards_variants.zip archive. Remember that the last file is needed for the Arduino IDE only. If you are using PlatformIO exclusively only the variant files need to be updated.

A Word to the Wise toc

I'll end with one warning. All modified files will be overwritten when the board definition is updated. How do I know? Since starting this post, the Seeed SAMD Boards by Seeed Studio has been updated from version 1.7.0 to 1.7.1 to 1.7.2. Enough said.

<-I²C Light Sensor using a Seeeduino XIAO
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO