Alternatives to SoftwareSerial on Arduino

I found I needed more serial connections than provided for by the Arduino Nano 33 BLE. SoftwareSerial doesn't exist on many Arduino platforms, but I couldn't figure out why. It's also a slow and suboptimal solution even in the best of times.

I found others online in the same boat, but the documentation on how to achieve this is sparse.

I will attempt to document everything I've found so far, providing example code where relevant.

A word of caution

Trying to use too many serial ports will certainly lead to sadness, confusion, and use a lot of pins.

For the Arduino Nano 33 BLE, if you absolutely need buffering and need more than two serial ports, you're out of luck. You will basically have to use another solution, or switch to a different Arduino with more pins. I offer some solutions below.

For other Arduinos: there basically is no solution, at least not a good one, other than the generic ones I outline below.

Without further ado...

Various Arduino models

How to achieve this varies based on Arduino model. There is no “one way” to do this.

A generic solution for everything

If you need more than 4 serial ports, you should probably consider using a multiplexer, or a microcontroller that has multiple UARTs and an SPI/I2C interface. You can find one with a basic search.

For just a spare port or two, DFRobot sells an I2C to dual UART module. If you need it sooner, Mouser sells them also.

Beware that more than four of these cannot be put on the I2C bus without an I2C multiplexer, but it would be inadvisable to do so anyway. I2C just doesn't have enough bandwidth for all these ports.

Arduino Nano 33 BLE

This platform is actually not the same as the Arduino Nano 33 IoT. It uses a different processor entirely, and the method used here is not applicable to that. More on that in a moment.

The Arduino Nano 33 BLE internally has a spare hardware serial connection that can be tied to (almost) any two pins. Here is an example (no headers required):

// Choose whatever digital/analog pins you want here
const int tx_pin = 2;
const int rx_pin = 3;

UART Serial2(tx_pin, rx_pin, NC, NC);

void setup() {
	Serial.begin(9600);
	// Wait for USB serial port
	while(!Serial);

	// Init all the pins we are going to use
	pinMode(tx_pin, OUTPUT);
	pinMode(rx_pin, INPUT);

	// Start our new serial port
	Serial2.begin(9600);
}

void loop() {
	Serial2.write("fnord");

	char buf[8] = {0};
	size_t count = Serial2.readBytes(buf, sizeof(buf));
	Serial.print("Count = "); Serial.println(count);
	Serial.print("Data = "); Serial.println(buf);
}

But if you need more than two serial connections, you have to get clever:

// Choose whatever digital/analog pins you want here
const int tx_pin_s2 = 2;
const int rx_pin_s2 = 3;
UART Serial2(tx_pin_s2, rx_pin_s2, NC, NC);

const int tx_pin_s3 = 4;
const int rx_pin_s3 = 5;
UART Serial3(tx_pin_s3, rx_pin_s3, NC, NC);

void setup() {
	Serial.begin(9600);
	// Wait for USB serial port
	while(!Serial);

	// Init all the pins we are going to use
	pinMode(tx_pin_s2, OUTPUT);
	pinMode(tx_pin_s3, OUTPUT);
	pinMode(rx_pin_s2, INPUT);
	pinMode(rx_pin_s3, INPUT);

	// Here we do not initalise serial, because we only have one spare hardware serial port.
	// We can partially overcome this limitation; more on that in a bit.
}

void loop() {
	char buf[8] = {0};

	Serial.println("Serial2:");

	// Start up Serial2 before use
	Serial2.begin(9600);

	Serial2.write("fnord");

	size_t count = Serial2.readBytes(buf, sizeof(buf));
	Serial.print("Count = "); Serial.println(count);
	Serial.print("Data = "); Serial.println(buf);

	// Ensure everything is completely flushed before we close it
	// Not strictly necessary, but done "just in case."
	Serial2.flush();

	// When we're done, shut down the port; we temporarily become unable to use it.
	// WARNING: nothing is buffered after this point on the port.
	// Anything transmitted to the port will be *LOST*. You have been warned.
	Serial2.end();

	/*****************************************************/

	Serial.println("Serial3:");

	// When we want to use Serial3, we do the same thing
	Serial3.begin(9600);

	Serial3.write("HAIL HAIL HAIL HAIL HAIL ERIS");

	// Clear buffer of previous contents
	memset(buf, 0, sizeof(buf));

	size_t count = Serial3.readBytes(buf, sizeof(buf));
	Serial.print("Count = "); Serial.println(count);
	Serial.print("Data = "); Serial.println(buf);

	// Same as Serial2
	Serial3.flush();

	// Same thing as Serial2, and same caveats apply.
	Serial3.end();
}

As noted in the example code, an important limitation applies: when we use one port, we can't use the other. Nothing will be buffered or saved when we are using one port. If this limitation is important to you, consider using another board like the Due, Mega, or Giga. These boards have four UART ports.

Note that each serial pair will consume two pins.

Arduino Nano 33 IoT and Arduino Uno

The Arduino Nano 33 IoT and the Arduino Uno are different from the BLE. They use what are called SERCOMs, which is a considerably more involved process.

I defer to Adafruit's guide on it all for this, as they do a better job explaining it all than I can.

Arduino Due/Mega/Giga

On boards such as the Due, Mega, or Giga, there are four serial ports. If you need more than that, you should probably defer to using one of the generic solutions I outlined above.

Conclusion

Although a lot of people think UART has gone the way of disco with peripherals that use SPI or I2C, it certainly has not. It is not outmoded or obsolete by any means. Many useful modules only use UART, and do not use SPI or I2C at all, not even optionally. GPS modules and many air quality sensors come to mind.

— Elizabeth Myers (Elizafox) Fedi (elsewhere): @Elizafox@social.treehouse.systems Tip jar: PayPal || CashApp || LiberaPay