2022-03-26
md
Three, Nay, Four Hardware Serial Ports on a SAM D21 XIAO
<-Seeeduino XIAO Serial Communication Interfaces (SERCOM)
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO

Unfortunately, my first attempts at modifying the mode of a serial communication (SERCOM) interface of the Seeeduino XIAO worked. Unfortunately because I became a victim of that little success. Thinking that I knew everything important about SAM D21/D51 serial communication interfaces, I boldly wrote a post about it in May of 2020. In the last few days I had to update that post to add statements which boil down to "I should not have created a second I²C port" and "it's really more complicated than explained here". Let's be clear, there's nothing that should never have been published in that first post. Indeed, if you know nothing about the SERCOM interfaces of the SAM D21/D51 families of microcontrollers, then I humbly suggest that you read it before pressing on. However if all you want is a recipe for getting 3 or even 4 hardware serial ports on a XIAO and don't care about the technical details then, by all means, jump to the last two sections.

Without wanting to nitpick, let's be a bit more careful about serial hardware nomenclature. The documentation says that the SAM D21/D51 SERCOM modules can be configured in universal synchronous and asynchronous receiver-transmitter (USART) mode. However it is unlikely that a serial port will ever be used in synchronous mode; I have never knowingly done that and I wouldn't even know where to begin using a USART. In practice, USART will refer to universal asynchronous receiver-transmitter (UART) from now on. This post shows how to set up SERCOM modules on the XIAO as UARTs which are accessed with instances of the Uart class called Serialx where x = 1, 2, 3 or 4.

Table of contents

  1. Three UARTs on a XIAO
  2. Take Two: Different Pin Assignment
  3. Order Matters
  4. Libraries for the Extra Serial Ports
  5. Four UARTs on a XIAO
  6. The Full Enchilada: Four Pin UART
  7. The Take Away
  8. The Give Away

Three UARTs on a XIAO toc

The good news is that it is perfectly "legal" to set any SERCOM interface to USART mode as well as all supported SERCOM pad to pin assignment (as long as the TX on pad 0 or pad 2 restriction is respected). There are no restrictions à la I²C that we saw in the previous topic. Since Serial1 is created by default only two hardware UARTs need to added to get to the proposed count of hardware serial ports on the XIAO. Here is the wiring for the example sketch.

TX to RX round robin connections

As can be seen, instead of connecting the TX and RX pins of each serial port together as done during loop tests, each serial port TX pin is feed to the next serial port's RX pin in round-robin fashion. Not shown is the USB connection to the desktop. Accordingly, in addition to the three hardware Serialx (x=1, 2, 3) ports shown, there is the fourth USBSerial interface which is used to download the firmware and power the XIAO.

The only other reference about this type of thing that I could find was by kio denshi: XIAO Serial Extension 2 from March 2021. The site is in Japanese, but with the help of one of the many translation tools on the Web it was easy to follow what the author did. As it happens the UART names and the wiring are different. While that does not really matter, it does serve as a warning that there is no apparent universal convention on this matter or, at least, there's none known to me.

The loop function begins by echoing any character received by any one of the three UARTs to the USBSerial (a.k.a Serial) interface so that it will show up in the IDE serial monitor. Those received characters come from the UARTs themselves because at regular, but different intervals, each UART transmits a simple message, its name and a number that always increases. For Serial1 that number is the number of seconds that the loop has been running, but for Serial2 and Serial3 it is the number of interrupts it has handled. Here is an example of the output in the PlatformIO serial monitor, but the same will be seen in the serial monitor of the Arduino IDE.

--- More details at https://bit.ly/pio-monitor-filters --- Miniterm on /dev/ttyACM0 9600,8,N,1 --- --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- Startup delay: 0 xiao_3usarts_v1 --------------- Setting up Serial1 Setting up Serial2 Setting up Serial3 Setup completed, starting loop Writing runcount 1 to Serial1 Serial1: 1 Writing serviceCount2 11 to Serial2 Serial2: 11 Writing serviceCount3 12 to Serial3 Serial3: 12 Writing runcount 2 to Serial1 Serial1: 2 Writing serviceCount2 33 to Serial2 Serial2: 33 Writing serviceCount3 35 to Serial3 Serial3: 35 Writing runcount 3 to Serial1 Serial1: 3 Writing serviceCount2 55 to Serial2 Serial2: 55 Writing runcount 4 to Serial1 Serial1: 4 Writing serviceCount3 58 to Serial3 Serial3: 58 Writing serviceCount2 77 to Serial2 Serial2: 77 Writing runcount 5 to Serial1 Serial1: 5 Writing serviceCount3 81 to Serial3 Serial3: 81 Writing runcount 6 to Serial1 Serial1: 6 Writing serviceCount2 110 to Serial2 Serial2: 110 Writing runcount 7 to Serial1 Serial1: 7 Writing serviceCount3 105 to Serial3 Serial3: 105 Writing serviceCount2 133 to Serial2 Serial2: 133 Writing runcount 8 to Serial1 Serial1: 8 Writing serviceCount2 156 to Serial2 Serial2: 156 Writing runcount 9 to Serial1 Serial1: 9 Writing serviceCount3 143 to Serial3 Serial3: 143 --- exit --- Terminal will be reused by tasks, press any key to close it.

The code for the sketch is not complicated but it is long for a demonstration sketch because there's much repetition.

#include <Arduino.h> // Needed for PlatformIO #include "wiring_private.h" // for pinPeripheral() function #define USART_BAUD 115200 // Baud for USARTs // Serial2 on sercom0 (with alternate pad assignment) // TX pin A10 and ALT-SERCOM0 PAD 2 // RX pin A9 and ALT-SERCOM0 PAD 1 Uart Serial2(&sercom0, A9, A10, SERCOM_RX_PAD_1, UART_TX_PAD_2); volatile int serviceCount2 = 0; extern "C" { void SERCOM0_Handler(void) { serviceCount2++; // to check this handler is used Serial2.IrqHandler(); } } // Serial3 on sercom2 (with alternate pad assignment) // TX pin A4 and ALT-SERCOM2 PAD 0 // RX pin A5 and ALT-SERCOM2 PAD 1 Uart Serial3(&sercom2, A5, A4, SERCOM_RX_PAD_1, UART_TX_PAD_0); volatile int serviceCount3 = 0; extern "C" { void SERCOM2_Handler(void) { serviceCount3++; // to check this handler is used Serial3.IrqHandler(); } } void setup() { // Wait up to 10 seconds for Serial (= USBSerial) port to come up. // Usual wait is 0.5 second. unsigned long startserial = millis(); while (!Serial && (millis() - startserial < 10000)) ; // Serial is now up. Serial.println("8 second initial delay"); // Should be enough time to start the serial // monitor or to upload new firmware for (int i=8; i>-1; i--) { Serial.printf("\rStartup delay: %d", i); delay(1000); } // Greeting as we start Serial.println("\n\nxiao_3usarts_v1"); Serial.println("---------------"); // Serial1 Serial.println("Setting up Serial1"); Serial1.begin(USART_BAUD); // Serial2 Serial.println("Setting up Serial2"); pinPeripheral(A10, PIO_SERCOM_ALT); pinPeripheral(A9, PIO_SERCOM_ALT); Serial2.begin(USART_BAUD); // Serial3 Serial.println("Setting up Serial3"); pinPeripheral(A3, PIO_SERCOM_ALT); pinPeripheral(A2, PIO_SERCOM_ALT); Serial3.begin(USART_BAUD); Serial.println("Setup completed, starting loop"); Serial.flush(); } // delays between successive messages transmitted on each Serial device #define SERIAL1_MESSAGE_INTERVAL 1000 #define SERIAL2_MESSAGE_INTERVAL 1300 #define SERIAL3_MESSAGE_INTERVAL 1600 // timers for these delays unsigned long serial1Timer = millis(); unsigned long serial2Timer = serial1Timer; unsigned long serial3Timer = serial1Timer; int runcount = 0; void loop(){ // Serial1 // // Transmit every byte received from Serial1 to Serial = USBSerial while (Serial1.available()) { Serial.write(Serial1.read()); Serial.flush(); } if (millis() - serial1Timer >= SERIAL1_MESSAGE_INTERVAL) { runcount++; Serial.printf("\nWriting runcount %d to Serial1\n", runcount); Serial.flush(); Serial1.printf("Serial1: %d\n", runcount); Serial1.flush(); serial1Timer = millis(); } // Serial2 // // Transmit every byte received from Serial2 to Serial = USBSerial while (Serial2.available()) { Serial.write(Serial2.read()); Serial.flush(); } if (millis() - serial2Timer >= SERIAL2_MESSAGE_INTERVAL) { Serial.printf("\nWriting serviceCount2 %d to Serial2\n", serviceCount2); Serial.flush(); Serial2.printf("Serial2: %d\n", serviceCount2); Serial2.flush(); serial2Timer = millis(); } // Serial3 // // Transmit every byte received from Serial3 to Serial = USBSerial while (Serial3.available()) { Serial.write(Serial3.read()); Serial.flush(); } if (millis() - serial3Timer >= SERIAL3_MESSAGE_INTERVAL) { Serial.printf("\nWriting serviceCount3 %d to Serial3\n", serviceCount3); Serial.flush(); Serial3.printf("Serial3: %d\n", serviceCount3); Serial3.flush(); serial3Timer = millis(); } }

Warning: do not copy and paste this code, it contains an error as explained in the next section.

The implementation in the loop function is a mess. There's a lot of flushing of the Serial transmit buffer because I am trying to avoid having the USART's interrupt handlers interfering with each other. I could have done better, but I don't care. The important bit here is setting up the USARTs and most of that is done at the very beginning. There is no need to create Serial1, that's done in the Arduino core. Look at the end of variant.cpp (in the ../framework-arduino-samd-seeed/variants/XIAO_m0 directory) to see how Serial1 is created. Basically, the same thing is done here for Serial2 and Serial3. Each of those objects is an instance of the Uart and it is necessray to set up the interrupt handler of the interface. The difference is of course the choice of the SERCOM interface and the assignment of its pads and choice of GPIO pins. That was covered in the previous post but here is the pertinent table for this purpose.

Seeedino XIAO
SERCOM
module
SERCOM PadsSerial Interface
0123
SERCOM0A4A5A2A3
ALT-SERCOM0A1A9A10A8 SPI  Serial2
ALT-SERCOM2A4A5A2A3 I²C  Serial3
ALT-SERCOM4A6A7Serial1

There is another part of the configuration of the two additional serial interfaces. The mode of the GPIO pins used by the serial interfaces has to be specified. That's the role of the four pinPeripheral(Axx, PIO_SERCOM_ALT) function calls in the setup function of the sketch. The pinPeripheral() function is really an extended pinMode function. Indeed one could make A10 an input GPIO pin with a pullup resistor with the call pinPeripheral(A10, PIO_INPUT_PULLUP).

That's it. Everything seems to works as desired. Of course, in actual use, one would not have the serviceCountx variables in the SERCOM interrupt handlers.

Take Two, Different Pin Assignment toc

So what's the problem? Why did I write [t]hings are not as simple as portrayed above. There is another "gotcha" when an attempt is made to assign an "unusual" pin to a SERCOM pad in the previous post? Because when I replaced

Uart Serial3(&sercom2, A5, A4, SERCOM_RX_PAD_1, UART_TX_PAD_0);

with

Uart Serial3(&sercom2, A3, A2, SERCOM_RX_PAD_3, UART_TX_PAD_2);

and adjusted the wiring accordingly, the program no longer worked. Serial3 simply did not function. I tried other combinations such as Uart Serial3(&sercom2, A3, A4, SERCOM_RX_PAD_3, UART_TX_PAD_0); but without success again. My powers of deduction led me to believe that the source of the problem laid in the implementation of the SAM D21 Arduino core, no less! There is a table in variant.cpp that maps GPIO pins of the device to the Arduino pin number. That table also contains a description of the "default" peripheral function of the pins. In the XIAO version, slots 2, 3 and 4 of the const PinDescription g_APinDescription[] table (which correspond pins A2, A3 and A4) contain the following information.

{ PORTA, 10, PIO_ANALOG, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER_ALT), ADC_Channel18, PWM0_CH2, TCC0_CH2, EXTERNAL_INT_10 }, { PORTA, 11, PIO_ANALOG, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER_ALT), ADC_Channel19, PWM0_CH3, TCC0_CH3, EXTERNAL_INT_11 }, { PORTA, 8, PIO_SERCOM_ALT, (PIN_ATTR_DIGITAL|PIN_ATTR_PWM|PIN_ATTR_TIMER_ALT), ADC_Channel16, PWM1_CH2, TCC1_CH2, EXTERNAL_INT_NMI }, // SDA: SERCOM2/PAD[0]

According to the table, the default peripheral function of pin PA11 (A3) is PIO_ANALOG while the default peripheral function of PA08 (A4) is PIO_SERCOM_ALT. The documentation says something about this.

7.1 Multiplexed Signals
Each pin is by default controlled by the PORT as a general purpose I/O and alternatively it can be assigned to one of the peripheral functions A, B, C, D, E, F, G or H. To enable a peripheral function on a pin, the Peripheral Multiplexer Enable bit in the Pin Configuration register corresponding to that pin (PINCFGn.PMUXEN, n = 0-31) in the PORT must be written to one. The selection of peripheral function A to H is done by writing to the Peripheral Multiplexing Odd and Even bits in the Peripheral Multiplexing register (PMUXn.PMUXE/O) in the PORT.

That wonderful prose immediately precedes 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. The columns in the table correspond to the peripheral functions. I concluded that it was not possible to assign SERCOM2 PAD3 to pin PA11 (A3) because its PIO type is PIO_ANALOG (peripheral function B in SAMD jargon) not PIO_SERCOM_ALT (peripheral function D). Clearly, Table 7.1 shows that any single GPIO pin can be assigned one of a variable number peripheral functions. I concluded that the authors of the SAMD D21 Arduino Core simplified things by limiting a GPIO pin be only one type of peripheral function or else being used as a general purpose input/output pin.

With that explanation for the inability to use a different SERCOM pad assignment, I ran out of ideas about fixing the problem. Just to prove that I had stumbled on a plausible explanation, I modified the g_APinDescription table, setting PIO type of PA10 and PA11 to PIO_SERCOM_ALT. The exact definition for the two pins was simply copied from the Arduino Zero variant.cpp file, so that was easy. Once that change was made, the pinPeripheral(A2, PIO_SERCOM_ALT) and pinPeripheral(A2, PIO_SERCOM_ALT) performed the desired multiplexing and the program worked.

Brilliant, I'm a genious I thought. But I was barking up the wrong tree. As the next section shows things are simpler after all.

Order Matters toc

On the theory that if the PIO_SERCOM_ALT peripheral function could not be assigned to a pin an error code should be returned, I decided to check on the result returned by the pinPeripheral() function. I was surprised to see that pinPeripheral(A2, PIO_SERCOM_ALT) returned 0. A closer look at the pinPeripheral() function revealed that it never references the PIO type of the pin defined in the g_APinDescription when setting the pin's peripheral function; it just passes on the PIO type parameter when the latter is not an input or output type of mode. That was perplexing because clearly the peripheral function value in the description table played a role, otherwise, the surgery on the table would not have worked. I wish I could report that deep thought on my part revealed what was happening. Instead a search for g_APinDescription in all the files in the ../framework-arduino-samd-seeed/variants/XIAO_m0/ directory was what finally got me going in the right direction. This showed up.

void Uart::begin(unsigned long baudrate, uint16_t config) { pinPeripheral(uc_pinRX, g_APinDescription[uc_pinRX].ulPinType); pinPeripheral(uc_pinTX, g_APinDescription[uc_pinTX].ulPinType); ...

Now I could see what was happening. In setup() the correct pin peripheral function was defined with pinPeripheral().

// Serial3 ... Serial.println("Setting up Serial3"); pinPeripheral(A3, PIO_SERCOM_ALT); pinPeripheral(A2, PIO_SERCOM_ALT); Serial3.begin(USART_BAUD); ...

Unfortunately in the subsequent call to Serial3.begin() the pin's peripheral function was reset to PIO_ANALOG. The solution was simple, change the order:

// Serial3 ... Serial.println("Setting up Serial3"); Serial3.begin(USART_BAUD); pinPeripheral(A3, PIO_SERCOM_ALT); pinPeripheral(A2, PIO_SERCOM_ALT); ...

The same thing has to be done with Serial2. Then the sketch works just as well as with the default pin assignment. Here is the sketch.

#include <Arduino.h> // Needed for PlatformIO #include "wiring_private.h" // for pinPeripheral() function #define USART_BAUD 115200 // Baud for USARTs #define ORDER_MATTERS // must be defined to call Uart.begin before calling the pinPeripheral() function // Serial2 Uart Serial2(&sercom0, A9, A10, SERCOM_RX_PAD_1, UART_TX_PAD_2); volatile int serviceCount2 = 0; extern "C" { void SERCOM0_Handler(void) { serviceCount2++; // to check this handler is used Serial2.IrqHandler(); } } // Serial3 Uart Serial3(&sercom2, A3, A2, SERCOM_RX_PAD_3, UART_TX_PAD_2); volatile int serviceCount3 = 0; extern "C" { void SERCOM2_Handler(void) { serviceCount3++; // to check this handler is used Serial3.IrqHandler(); } } void setup() { int error[4] = {0}; int errors = 0; int pin; // Wait up to 10 seconds for Serial (= USBSerial) port to come up. // Usual wait is 0.5 second. unsigned long startserial = millis(); while (!Serial && (millis() - startserial < 10000)) ; // Serial is now up. Serial.println("8 second initial delay"); // Should be enough time to start the serial // monitor or to upload new firmware for (int i=8; i>-1; i--) { Serial.printf("\rStartup delay: %d", i); delay(1000); } // Greeting as we start Serial.println("\n\nxiao_3usarts_v2"); Serial.println("---------------"); // Serial1 Serial.println("Setting up Serial1"); Serial1.begin(USART_BAUD); // Serial2 Serial.println("Setting up Serial2"); #ifdef ORDER_MATTERS Serial2.begin(USART_BAUD); error[0] = pinPeripheral(A10, PIO_SERCOM_ALT); error[1] = pinPeripheral(A9, PIO_SERCOM_ALT); #else error[0] = pinPeripheral(A10, PIO_SERCOM_ALT); error[1] = pinPeripheral(A9, PIO_SERCOM_ALT); Serial2.begin(USART_BAUD); #endif // Serial3 Serial.println("Setting up Serial3"); #ifdef ORDER_MATTERS Serial3.begin(USART_BAUD); error[2] = pinPeripheral(A2, PIO_SERCOM_ALT); error[3] = pinPeripheral(A3, PIO_SERCOM_ALT); #else error[2] = pinPeripheral(A2, PIO_SERCOM_ALT); error[3] = pinPeripheral(A3, PIO_SERCOM_ALT); Serial3.begin(USART_BAUD); #endif for (int i=0; i<4; i++) if (error[i]) errors++; if (errors) { Serial.printf("** %d error%s while setting up the serial ports **\n", errors, (errors > 1) ? "s" : ""); for (int i=0; i < 4; i++) { if (error[i]) { switch (i) { case 0: pin = A10; break; case 1: pin = A9; break; case 2: pin = A2; break; case 3: pin = A3; break; } Serial.printf(" Pin A%d could not be reassigned to PIO_SERCOM_ALT\n", pin); } } } else Serial.println("\nInitialized all serial ports without error."); Serial.println("Setup completed, starting loop"); Serial.flush(); } // delays between successive messages transmitted on each Serial device #define SERIAL1_MESSAGE_INTERVAL 1000 #define SERIAL2_MESSAGE_INTERVAL 1300 #define SERIAL3_MESSAGE_INTERVAL 1600 // timers for these delays unsigned long serial1Timer = millis(); unsigned long serial2Timer = serial1Timer; unsigned long serial3Timer = serial1Timer; int runcount = 0; void loop(){ // Serial1 // // Transmit every byte received from Serial1 to Serial = USBSerial while (Serial1.available()) { Serial.write(Serial1.read()); Serial.flush(); } if (millis() - serial1Timer >= SERIAL1_MESSAGE_INTERVAL) { runcount++; Serial.printf("\nWriting runcount %d to Serial1\n", runcount); Serial.flush(); Serial1.printf("Serial1: %d\n", runcount); Serial1.flush(); serial1Timer = millis(); } // Serial2 // // Transmit every byte received from Serial2 to Serial = USBSerial while (Serial2.available()) { Serial.write(Serial2.read()); Serial.flush(); } if (millis() - serial2Timer >= SERIAL2_MESSAGE_INTERVAL) { Serial.printf("\nWriting serviceCount2 %d to Serial2\n", serviceCount2); Serial.flush(); Serial2.printf("Serial2: %d\n", serviceCount2); Serial2.flush(); serial2Timer = millis(); } // Serial3 // // Transmit every byte received from Serial3 to Serial = USBSerial while (Serial3.available()) { Serial.write(Serial3.read()); Serial.flush(); } if (millis() - serial3Timer >= SERIAL3_MESSAGE_INTERVAL) { Serial.printf("\nWriting serviceCount3 %d to Serial3\n", serviceCount3); Serial.flush(); Serial3.printf("Serial3: %d\n", serviceCount3); Serial3.flush(); serial3Timer = millis(); } }

Remove the ORDER_MATTERS macro at the start of the file and confirm that the order in which Serialx.begin() and pinPeripheral() are called is important if non default pin assignment is used.

Libraries for the Extra Serial Ports toc

I like how Kio-Denshi handled the extra serial ports by creating a couple of libraries. On the other hand, inserting the pin and pad assignments in variant.h is not, as I have argued before, the best idea. Since I see no overriding reason to do that the pin definitions were placed in each individual Serialx.h header file. Here is the Serial2.h header file.

#pragma once #include "variant.h" #define PIN_SERIAL2_TX (10ul) // TX on A10 #define PIN_SERIAL2_RX (9ul) // RX on A9 #define PAD_SERIAL2_TX (UART_TX_PAD_2) #define PAD_SERIAL2_RX (SERCOM_RX_PAD_1) extern Uart Serial2; void SERCOM0_Handler(void);

And here is the accompanying Serial2.cpp source code.

#include "Serial2.h" Uart Serial2(&sercom0, PIN_SERIAL2_RX, PIN_SERIAL2_TX, PAD_SERIAL2_RX, PAD_SERIAL2_TX); void SERCOM0_Handler(void) { Serial2.IrqHandler(); }

A library can be created for the third USART with the Serial3.h and Serial3.cpp files that are the same mutatis mutandis.

With this approach, if one wants to use an alternate multiplexing for the second or third UART, then one would have to create yet another library. This gets cumbersome very quickly as there are 6 possible pin assignments for each of the two extra UARTS. It's true that in any one sketch there can be only one definition of a UART per SERCOM. Consequently I am not sure that one would want to define all 12 libraries (especially as I have yet to figure out a viable way to do this in the Arduino IDE). Nevertheless, I have provided an example of how this could be done.

Four UARTs on a XIAO toc

Now that I have finally cought up with everyone and know how to take care of pin peripheral function assignments on the SAM D21/D51 microcontrollers, it's time to move on to serious business. Let's up the ante and get a fourth hardware USART running on the XIAO. So how is this done? The Serial Wire Debug interface, exposed as two pads on the bottom side of the XIAO, is run on two pins that can be multiplexed to the (ALT-)SERCOM1 interface. Since I did not want to solder wires onto the SWD pads, it was a bit tricky to connect the fourth serial port.

photo of connections

The connections cobbled together with a bit of painter's tape, a small spring clamp and some Dupont wires remained in place long enough to run a test program which is just an obvious extension of the previous program. Here is its output.

Startup delay: 0 xiao_3usarts ------------ Setting up Serial1 Setting up Serial2 Setting up Serial3 Setting up Serial4 Setup completed, starting loop Writing runcount 1 to Serial1 Serial1: 1 Writing 2 to Serial2 Serial2: 2 Writing 3 to Serial3 Serial3: 3 Writing 4 to Serial4 Serial4: 4 Writing runcount 2 to Serial1 Serial1: 2 Writing 4 to Serial2 Serial2: 4 Writing 6 to Serial3 Serial3: 6 Writing 8 to Serial4 Serial4: 8 Writing runcount 3 to Serial1 Serial1: 3 Writing 6 to Serial2 Serial2: 6 Writing runcount 4 to Serial1 Serial1: 4 Writing 12 to Serial3 Serial3: 12 Writing 16 to Serial4 Serial4: 16 Writing 8 to Serial2 Serial2: 8 Writing runcount 5 to Serial1 Serial1: 5 Writing 15 to Serial3 Serial3: 15 Writing runcount 6 to Serial1 Serial1: 6 Writing 12 to Serial2 Serial2: 12 Writing 24 to Serial4 Serial4: 24 Writing runcount 7 to Serial1 Serial1: 7 Writing 21 to Serial3 Serial3: 21 Writing 14 to Serial2 Serial2: 14 Writing runcount 8 to Serial1 Serial1: 8 Writing 32 to Serial4 Serial4: 32 Writing 16 to Serial2 Serial2: 16 Writing 24 to Serial3 Serial3: 24 Writing runcount 9 to Serial1 Serial1: 9 --- exit --- Terminal will be reused by tasks, press any key to close it.

Here is the Serial4.h header file.

#pragma once #include "variant.h" #define PIN_SERIAL4_TX (17ul) // TX on SWCLK #define PIN_SERIAL4_RX (18ul) // RX on SWDIO #define PAD_SERIAL4_TX (UART_TX_PAD_2) #define PAD_SERIAL4_RX (SERCOM_RX_PAD_3) extern Uart Serial4; void SERCOM1_Handler(void);

And here is the accompanying Serial4.cpp source code.

#include "Serial2.h" Uart Serial4(&sercom1, PIN_SERIAL4_RX, PIN_SERIAL4_TX, PAD_SERIAL4_RX, PAD_SERIAL4_TX); void SERCOM1_Handler(void) { Serial4.IrqHandler(); }

Unfortunately the PA30 and PA31 pins (respectively the SWCLK and SWDIO signals) are not in the pin description table in the variant.cpp file found in the ../framework-arduino-samd-seeed/variants/XIAO_m0/ directory. So they had to be added at the end of the table in slots 17 and 18.

const PinDescription g_APinDescription[]= { ... { PORTA, 24, PIO_COM, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, // USB/DM { PORTA, 25, PIO_COM, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE } // USB/DP // 17,18 - SWD Interface - added for 4th hardware USART , { PORTA, 30, PIO_SERCOM_ALT, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, //SWCLK, TX: SERCOM1/PAD[2] { PORTA, 31, PIO_SERCOM_ALT, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE } // SWDIO, RX: SERCOM1/PAD[3] };

I wonder if anyone could have a use for this fourth serial port? First, it would disable the debug interface which could make developement of complex programs more difficult. Secondly, the change to the variant.cpp file is never a good idea since the file "belongs" to the SAMD Arduino Core developpers. In other words, these changes will be overwritten each time the board definition is updated. Finally, connecting to the SWD pads is not as straightforward as connecting to the other pins of the device. This latter problem is neatly solved with the Seeeduino XIAO expansion board whch brings out the SWD interface and adds many peripherals such as an 0.96" OLED and an real time clock. It does cost 3 times as much as the XIAO itself.

Four hardware serial interfaces; can anyone do better? The 3 GPIO pins connected to the blue and yellow LEDs on the board correspond to pads 1, 2 and 3 of SERCOM1. But even if one could modify the USB code to stop flashing these LEDs in time with USB exchanges and somehow connect to the GPIO signals, a serial hardware interface would not be gained because the Serial4 also uses SERCOM1. As for pins A0 to A3 which remain free, they cannot be used by any SERCOM module. I suppose that some bit-banged software UART could be run on these pins, but that would be cheating. Accordingly, I feel pretty confident in asserting that 4 hardware serial ports is the limit on the XIAO.

The Full Enchilada: Four Pin UART toc

Serial 2 and Serial 3 could have been set up to include the RTS and CTS hardware flow control signals. In that case there is no choice about pin assignment, the TX pin must be multiplexed on pad 0, RX on pad 1, RTX on pad 2 and CTS on pad 3 of the SERCOM.

Here is what I think this would look like.

Uart Serial2(&sercom0, A9, A1, SERCOM_RX_PAD_0, UART_TX_RTS_CTS_PAD_0_2_3, A10, A8); extern "C" { void SERCOM0_Handler(void) { Serial2.IrqHandler(); } } ... void setup(void) { ... Serial2.begin(USART_BAUD); pinPeripheral(A1, PIO_SERCOM_ALT); // needed pinPeripheral(A9, PIO_SERCOM_ALT); // not needed, but no harm done ...

I have not pursued this subject any further at this point. However those interested could look at section 26.6.3.2 Hardware Handshaking in the SAM D21/DA1 Family Complete Datasheet for more information.

The Take Away toc

Two additional hardware serial ports can easily be added to the XIAO. For each additional serial port, one of two pins can be chosen for the TX signal and one of three pins can be chosen for the RX signal.

Extra UART with TX and RX Only
SERCOMTX pinRX pin (one of)Name
sercom0A1[0]*A8[3] A9[1]A10[2]Serial2
A10[2]A1[0]*A8[3]A9[1]
sercom2A4[0] A2[2]*A3[3]*A5[1]Serial3
A2[2]*A3[3]*A4[0] A5[1]

The SERCOM pad number to assign to the I/O pin is in square brackets. The asterisk indicated that the I/O pin's function is not set to PIO_SERCOM_ALT by default and that it will be necessary to explicitely change it with the pinPeripheral() function.

The interfaces could also be set up as four pin UARTs. There is no choice in pin assignments in the case of a USART with flow control.

Extra UART with TX, RX and Flow Control
SERCOMTX pinRX pinRTS pinCTS pin
sercom0A1[0]*A9[1]A10[2]A8[3]
sercom2A4[0]A5[1]A2[2]A3[3]*

A third extra serial port can be enabled but it will be necessary to modify the variant.cpp file as explained above.

Third Extra USART with TX and RX Only
SERCOMTX pinRX pinName
sercom118[2]17[3]Serial4

If a pin with an asterisk is chosen for the TX, the RX or CTS signal then it is necessary to manually set its mode with a pinPeripheral(Axx, PIO_SERCOM_ALT) statement. That statement must appear after the Serialx.begin() statement.

The Give Away toc

The source code for the examples discussed in this post is available on GitHub at sigmdel/xiao_usarts.

<-Seeeduino XIAO Serial Communication Interfaces (SERCOM)
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO