Skip to content

13. Embedded networking and communications

This week I programmed two Seeed XIAO microcontrollers to communicate with each other using SoftwareSerial library.

Group work

For the group work we programmed two boards to communicate with each other using the same SoftwareSerial library as I would afterwards use for my own assignment.

One of the boards was the one I made and another was Aarne's. Both had the Xiao RP2040 mounted.

Harnessing the power of modularity

I was thinking if I could use the board I already make at (link previous week). I had the three power pins () pins and three free IO-pins and I was not sure if those are enough for communication. Instantly when our instructor at our local lecture told it’s perfectly ok, I realized that I could take advantage of the different XIAO microcontroller boards having the same pin layouts (which is what I am referring to with "modularity").

Xiao pin layout

Image source

Notice the 6-pins on the right side. In my board I kept the same order for the external pin connectors.

My board from previous week

So I can just:

Two boards connected

Just... wow...

Well... there is just one possible issue. While the 5V from the USB would provide power to both boards, Also the 3V3 regulators are "fighting" against each other. This might or might not be an issue.

I could not get a confident answer from our instructors or the internet. After some thought I figured that as long as the actual voltages of the 3V3 regulators stay very close to each other there should not be a problem. If one was greater than another there could be current flowing in the wrong direction which I understood many regulators might not handle so well..

At this point in time one could consider measuring the voltages from the regulators from both boards. I went ahead and just plugged it in..

Still no smoke

As far as I'm concerned there is no fire or smoke, but instead the powerlights are still on on both boards. So I could assume everything is fine (?..)

Usecase

As I want to keep things simple for this week, I will just try and communicate the pressing of the buttons to the other board which is not connected directly to the buttons.

This is more like a proof-of-concept of how easy it would be to create something more complex while keeping it modular. I am not sure what would be the really relevant application for this.

I heard the ESP32 has a poor ADC. So maybe if you needed that AND wifi/bluetooth capability, you could use the RP2040 to read some analog sensors and the ESP32C3 to connect wirelessly.

However, if you need more pins than the seven (4xGPIO) from that one side, it would be more practical to just go with the “shield” approach. That's where you stack boards vertically with pin connectors.

Ugly shield example

Though it's still hard to see an actually relevant usecase for multiple stacked microcontrollers.

Programming

So the goal is to communicate the button pressing to the other board which has a built-in NeoPixel. The communication will be done with GPIO pins (General Purpose Input/Output) using the SoftwareSerial library.

The basics of embedded programming were presented earlier in week 4. That page also presents the boards I will be using for this assignment too.

I started with the same code example which we used for the group project. Otherwise the program was made by me.

Here is the unmodified example:

#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3); // RX, TX

void setup()
{
  Serial.begin(115200);
  while (!Serial) {
  }

  Serial.println("Goodnight moon!");

  mySerial.begin(9600);
  mySerial.println("Hello, world?");
}

void loop()
{
  if (mySerial.available())
    Serial.write(mySerial.read());
  if (Serial.available())
    mySerial.write(Serial.read());
}

First problem

The ESP32C3 apparently does not support the SoftwareSerial.

esp error

Because when I compiled for the RP2040 OR the SAMD21 both worked. Luckily I had the SAMD21 at hand and could proceed by using that.

First success

I split the code in two files for the "Control" board which reads the buttons and the other "Peripheral" board which has the LED (NeoPixel). At first I modified those so that the peripheral just sends back all the received messages.

first message exchange

Below are the program codes for each.

This is for the "Control" board, where only the pins had been changed:

#include <SoftwareSerial.h>
SoftwareSerial mySerial(D9, D8); // RX, TX //The correct pins for my board

void setup()
{
  Serial.begin(115200);
  while (!Serial) {
  }

  Serial.println("Goodnight moon2222!");

  mySerial.begin(9600);
  mySerial.println("Hello, world2222?");
}

void loop()
{
  if (mySerial.available())
    Serial.write(mySerial.read());
  if (Serial.available())
    mySerial.write(Serial.read());
}

And this is for the "peripheral" board

#include <SoftwareSerial.h>
SoftwareSerial mySerial(D8, D9); // RX, TX // Notice these are opposite

void setup()
{
  Serial.begin(115200);

  Serial.println("Goodnight moon2222!");

  mySerial.begin(9600);
  mySerial.println("Hello, world2222?");
}

void loop()
{
  // For now, just make it return what is received
  if (mySerial.available())
    mySerial.write(mySerial.read());
}

Result

Again, the code is split to two files:

Which are also embedded and explaned more below in the next section.

Here is the result:

Hero shot

As an animation. Note that the color switches only once for each press of the button. It is hard to see from this gif sadly.

Animation

The funny thing is that it can also be powered from the other board's USB port.

Alternative

Control

This program reads the button presses and sends commands to the other board accordingly. The commands are singular characters or integers.

The first button turns the led on/off by the messages 'o' and 'f' correspondingly.

The second button iterates through a set number of values (integers, here 0 to 5).

software-serial_control.ino
#include <SoftwareSerial.h>

SoftwareSerial mySerial(D9, D8); // RX, TX

#define BUTTON1PIN D2
#define BUTTON2PIN D3

const uint8_t my_address = 0; // My network address
const uint8_t recipient_address = 1; // Default peripheral address
const char message_ending = '\n';

const uint8_t animation_max = 5;

bool light_on = false;
uint8_t animation_state = 0;

void setup()
{
  pinMode(BUTTON1PIN, INPUT_PULLUP);
  pinMode(BUTTON2PIN, INPUT_PULLUP);

  mySerial.begin(9600);
  //Serial.begin(9600);
}

// Message format: <recipient><sender><command><newline>
// "Node 1, this is your Node 0 speaking, light on."
void loop()
{
  if (!digitalRead(BUTTON1PIN)) {
    if (light_on){
      mySerial.write(recipient_address);
      mySerial.write(my_address);
      mySerial.write('f'); // Command light off
      mySerial.write(message_ending);
      light_on = false;
    } else {
      mySerial.write(recipient_address);
      mySerial.write(my_address);
      mySerial.write('o'); // Command light on
      mySerial.write(message_ending);
      light_on = true;
    }
    delay(50);
    while(!digitalRead(BUTTON1PIN)); // Wait until button released
  }

  if (!digitalRead(BUTTON2PIN)) {
    if (animation_state == animation_max){
      animation_state = 0;
    } else {
      animation_state += 1;
    }
    mySerial.write(recipient_address);
    mySerial.write(my_address);
    mySerial.write(animation_state); // Command light state
    mySerial.write(message_ending);
    delay(50);
    while(!digitalRead(BUTTON2PIN)); // Wait until button released
  }
  // For debugging:
  //if (mySerial.available()) {
  //  Serial.write(mySerial.read());
  //}
}

Peripheral

This program is for the RP2040 as it has the built-in NeoPixel. It reads the commands sent by the other board and using a switch-case structure acts accordingly.

Note that here the "animation state" is only a specific color. It could be something more sophisticated too.

Additionally the state is kept while the led is turned off.

software-serial_peripheral.ino
// Made for RP2040
#include <SoftwareSerial.h>
#include <Adafruit_NeoPixel.h>

SoftwareSerial mySerial(D8, D9); // RX, TX

#define NEOPOWERPIN 11 // Provides voltage for the built-in NeoPixel
#define NEOPIXELPIN 12 // Datapin for the the built-in NeoPixel
#define NUMPIXELS 1 //Amount of NeoPixels

Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELPIN, NEO_RGB + NEO_KHZ800);

const uint8_t my_address = 1; // Network address
const uint8_t control_address = 0; // Network address of the "host"
const char message_ending = '\n';

bool light_on = false;
uint32_t led_color = pixels.Color(255,255,255);
bool ignore_message = false;

void setup()
{
  pinMode(NEOPOWERPIN, OUTPUT); // initialize digital pin NEOPOWERPIN as an output.
  digitalWrite(NEOPOWERPIN, HIGH); // Power for the NeoPixel

  mySerial.begin(9600);
  //Serial.begin(9600); // for debugging

  pixels.begin();
}

// message: <recipient><sender><command>
void loop()
{
  if (mySerial.available()) {
    byte received = mySerial.read();
    //Serial.write(received);

    if (received == message_ending) {
      ignore_message = false;
      return;
    }
    if (ignore_message) {
      return;
    }

    if (received == my_address) {
      // There is a message for us
      while(!mySerial.available()){
        // Assume / hope the message will continue..
        delay(10);
      }
      if (mySerial.read() == control_address) {
        while(!mySerial.available()){
        // Assume / hope the message will continue..
          delay(10);
        }
        // And it's from the "host", now act on it:
        switch (mySerial.read()) {
          case 'o':
            light_on = true;
            break;
          case 'f':
            light_on = false;
            break;
          case 0:
            led_color = pixels.Color(100,100,100);
            break;
          case 1:
            led_color = pixels.Color(120,10,150);
            break;
          case 2:
            led_color = pixels.Color(155,100,10);
            break;
          case 3:
            led_color = pixels.Color(20,80,120);
            break;
          case 4:
            led_color = pixels.Color(100,90,120);
            break;
          case 5:
            led_color = pixels.Color(255,255,255);
            break;
          default:
            mySerial.println("Not correct command");
            break;
        }
        if (light_on) {
          pixels.clear();
          pixels.setPixelColor(0, led_color);
          pixels.show();
        } else {
          pixels.clear();
          pixels.show();
        }
      }
    } else {
      // The message is meant for someone else
      ignore_message = true;
    }
  }
}

Note on the after additions

Notice the added networking addresses. These were added later to comply with the networking requirement. Now the boards know the sender and recipient of a message. The message format is now:

(recipient)(sender)(command)(line-ending)

The parsing is quite primitive and assumes only one digit for each field. I have highlighted some of the more important lines regarding the addresses.

To make these changes I had to make fundamental changes to the code and the whole message format. Of course this is a small project but still took a moment to figure out. In the end was able to get it working exactly as presented above.

Working in train

End of week 13