I recently came across same small footprint SAMD21 boards that I've found to be very useful for tinkering. I've used the SAMD21 processors with a number of Arduino boards (Nano 33 IoT, MKR WAN 1300, MKR WiFi 1010) and I've been happy with their capability. Quite a step up from the AVR parts. What impressed me with these new boards is their small size and low cost. Almost as cheap as buying the SAMD21 chip by itself. I've been wanting a small low cost board that I could incorporate into smaller standalone projects that don't require radio communication capability. Also one that I wouldn't be too concerned about damaging. Another plus are the castellated pads which allows using it as a component on an expansion board.
The first board that I found and have been using is the Seeeduino Xiao https://www.seeedstudio.com/Seeeduino-XIAO-Arduino-Microcontroller-SAMD21-Cortex-M0+-p-4426.html?utm_source=blog&utm_med…
Seeeduino XIAO
Microcontroller ATSAMD21G18, 48MHz 32-Bit ARM Cortex M0+ 48 pins 6 Sercom 38 IO 14 ADC
USB Type C connector for power and data
Operating Voltage 3.3V
Digital I/O Pins 11
PWM Pins 10
Analog I/O Pins 11
Flash Memory 256KB
SRAM 32KB
Size 23.5mm x 17.5mm
Price $4.90
And recently I found that Adafruit had made the QT Py, a variant of the Xiao, that adds some other features but has the same footprint and pinout https://www.adafruit.com/product/4600
Adafruit QT Py
Same size, form-factor, and pin-out as Seeed Xiao
Primary differences
ATSAMD21E18 processor which has a smaller package with only 32 pins so only 4 Sercom 26 IO 10 ADC
Built in RGB NeoPixel LED
PWM Pins 9
Analog I/O Pins 9
I2C port with STEMMA QT plug-n-play connector
Optional SOIC-8 SPI Flash chip on bottom
Reset button for starting your project code over or entering bootloader mode
Price $6.00
So, you can see that Adafruit traded off some I/O pins to get space for extra features. I like the added reset button because the SAMD21 is easy to lock up and with the Xiao you need to short the reset pads to get into bootloader mode.
The NeoPixel is nice but the feature I really like is the ability to add additional flash memory to the bottom of the PCB. Adafruit has a 2MB Flash chip for $1.25 http://www.adafruit.com/product/4763. I think that they added that capability to be able to run larger CircuitPython programs.
Here's a picture of the two boards, the Xiao is on top and the QT Py on the bottom. You can see that I've added the flash chip on the QT Py. The Xiao has 4 single LEDs adjacent to the USB-C connector - 1 user, 1 power, 2 for Tx and Rx of the UART.
Seeed Studio has a couple of handy breakout boards for the Xiao. I have the Grove shields https://www.seeedstudio.com/Grove-Shield-for-Seeeduino-XIAO-p-4621.html which are handy for prototyping with sensors and I'll be able to use them with the QT Py too.
RPC Testing
I've wanted to play around with using external microcontroller capability with machine vision cameras via UART. I haven't used RPC (Remote Procedure Call) protocol much and I thought that it might be fun to prototype it using a couple of Xiao boards.
Here's a quick demo - the Xiaos are mounted on the Grove shields which I use to connect their UARTs and added a pushbutton, an LED and a potentiometer for testing. The long cables between the boards are the UART connection with the Rx/Tx crossover. The board on the top is the master, the one on the bottom the slave. The master has a pushbutton that that controls a green LED on the slave and the slave sends the value from an ADC connected potentiometer to the master which is displayed on its serial monitor.
Here's the code for the master and slave. The code uses the openmvrpc library and is built from openmv example code.
xiao_to_xiao_communication_as_the_controller_device.ino
// Remote Control - As The Controller Device // // This script configures your Xiao to remotely control another Xiao. // #include <openmvrpc.h> // The RPC library above provides mutliple classes for controlling an Arduino over // CAN, I2C, SPI, or Serial (UART). //initialize and declare variables const int ledPin = 13; //led attached to this pin const int buttonPin = 2; // the number of the pushbutton pin uint8_t buttonState = LOW; //this variable tracks the state of the button, low if not pressed, high if pressed // We need to define a scratch buffer for holding messages. The maximum amount of data // you may pass in any on direction is limited to the size of this buffer. openmv::rpc_scratch_buffer<256> scratch_buffer; // All RPC objects share this buffer. /////////////////////////////////////////////////////////////// // Choose the interface you wish to control a Xiao over. /////////////////////////////////////////////////////////////// // Uncomment the below line to setup your Xiao for controlling over a hardware UART. // openmv::rpc_hardware_serial_uart_master interface(115200); void setup() { // initialize the pushbutton pin as an input: pinMode(buttonPin, INPUT); // Startup the RPC interface and a debug channel. interface.begin(); Serial.begin(115200); } ////////////////////////////////////////////////////////////// // Call Back Handlers ////////////////////////////////////////////////////////////// // This example shows reading a Digital I/O pin remotely. // void digital_read_example() { uint8_t state; if (interface.call_no_args(F("digital_read"), &state, sizeof(state))) { Serial.print(F("Remote Digital I/O State: ")); Serial.println(state); } } // This example shows reading an Analog I/O pin remotely. // void analog_read_example() { uint16_t state; if (interface.call_no_args(F("analog_read"), &state, sizeof(state))) { Serial.print(F("Remote Analog I/O State: ")); Serial.println(state); } } // This example shows writing a Digital I/O pin remotely. // void digital_write_example(uint8_t state) { // static uint8_t state = 1; if (interface.call(F("digital_write"), &state, sizeof(state))) { //state = !state; // flip state for next time // Serial.print(F("Local Button State: ")); // Serial.println(state); } } // This example shows writing an Analog I/O pin remotely. // void analog_write_example() { static uint8_t state = 0; if (interface.call(F("digital_write"), &state, sizeof(state))) { state = state + 1; // counts from 0 to 255 then rolls over } } void serial_print_example() { char *str = "Hello World!"; interface.call(F("serial_print"), str, strlen(str)); } void loop() { // digital_read_example(); analog_read_example(); buttonState = digitalRead(buttonPin); digitalWrite(ledPin, !buttonState); digital_write_example(buttonState); // analog_write_example(); serial_print_example(); }
xiao_to_xiao_communication_as_the_remote_device.ino
// Remote Control - As The Remote Device // // This script configures your Xiao to be remotely controlled by another Xiao. // #include <openmvrpc.h> // The RPC library above provides mutliple classes for being cotnrolled over // CAN, I2C, SPI, or Serial (UART). const int ledPin = 2; //led attached to this pin // We need to define a scratch buffer for holding messages. The maximum amount of data // you may pass in any on direction is limited to the size of this buffer. openmv::rpc_scratch_buffer<256> scratch_buffer; // All RPC objects share this buffer. // The interface library executes call backs on this device which have to be registered // before they can be called. To avoid dyanmic memory allocation we have to create a buffer // with the maximum number of call backs we plan to support to hold the registrations. // // Note that callback registrations only work on the rpc interface that registered them. openmv::rpc_callback_buffer<8> callback_buffer; // All RPC objects share this buffer. /////////////////////////////////////////////////////////////// // Choose the interface you wish to be controlled over. /////////////////////////////////////////////////////////////// // Uncomment the below line to setup for be controlled over a hardware UART. // openmv::rpc_hardware_serial_uart_slave interface(115200); ////////////////////////////////////////////////////////////// // Call Backs ////////////////////////////////////////////////////////////// size_t digital_read_example(void *out_data) { // Get what we want to return into a variable. uint8_t state = digitalRead(4); // Move that variable into a transmit buffer. memcpy(out_data, &state, sizeof(state)); // Return how much we will send. return sizeof(state); } size_t analog_read_example(void *out_data) { // Get what we want to return into a variable. uint16_t state = analogRead(A0); // Move that variable into a transmit buffer. memcpy(out_data, &state, sizeof(state)); // Return how much we will send. return sizeof(state); } void digital_write_example(void *in_data, size_t in_data_len) { // Create the primitive or complex data type on the stack. uint8_t state; // Check that we received the amount of data we expected. if (in_data_len != sizeof(state)) return; // Copy what we received into our data type container. memcpy(&state, in_data, sizeof(state)); // Serial.print(F("Remote Button State: ")); // Serial.println(state); // Use it now. digitalWrite(ledPin, state); // if (state > 0){ // digitalWrite(ledPin, HIGH); //LED off // } else { // digitalWrite(ledPin, LOW); //LED on // } } void analog_write_example(void *in_data, size_t in_data_len) { // Create the primitive or complex data type on the stack. uint8_t state; // Check that we received the amount of data we expected. if (in_data_len != sizeof(state)) return; // Copy what we received into our data type container. memcpy(&state, in_data, sizeof(state)); // Use it now. analogWrite(A1, state); } void serial_print_example(void *in_data, size_t in_data_len) { // Create the string on the stack (extra byte for the null terminator). char buff[in_data_len + 1]; memset(buff, 0, in_data_len + 1); // Copy what we received into our data type container. memcpy(buff, in_data, in_data_len); // Use it now. Serial.println(buff); } void setup() { // initialize the LED pin as an output: pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); // register callbacks interface.register_callback(F("digital_read"), digital_read_example); interface.register_callback(F("analog_read"), analog_read_example); interface.register_callback(F("digital_write"), digital_write_example); interface.register_callback(F("analog_write"), analog_write_example); interface.register_callback(F("serial_print"), serial_print_example); // Startup the RPC interface and a debug channel. interface.begin(); Serial.begin(115200); } // Once all call backs have been registered we can start // processing remote events. void loop() { interface.loop(); }
I'm looking forward to using the QT Py. I'll probably try that out with CircuitPython since I've added the extra memory .