This article shows how you can send JSON documents between two boards connected by a serial link.

In this article, I’ll assume that we use two Arduino UNOs. Because I want to keep Serial for logging, I’ll use SoftwareSerial for the communication between the two boards.

Serial communication between two Arduino UNOs

If you use a board with several hardware serial implementations (such as Arduino Leonardo, Arduino Mega, Arduino Due, ESP8266, or ESP32), prefer using Serial1, Serial2, or Serial3 over SoftwareSerial, because it will provide better performance and reliability.

Avoid SoftwareSerial because it is notoriously unreliable. It’s used here only for demonstration.

Sender program

This program sends {"timestamp":1234,"value":687} every 5 seconds.

#include <ArduinoJson.h>
#include <SoftwareSerial.h>

// Declare the "link" serial port
// Please see SoftwareSerial library for detail
SoftwareSerial linkSerial(10, 11); // RX, TX

void setup() {
  // Initialize "debug" serial port
  // The data rate must be much higher than the "link" serial port
  Serial.begin(115200);
  while (!Serial) continue;

  // Initialize the "link" serial port
  // Use the lowest possible data rate to reduce error ratio
  linkSerial.begin(4800);
}
 
void loop() {
  // Values we want to transmit
  long timestamp = millis();
  int value = analogRead(1);

  // Print the values on the "debug" serial port
  Serial.print("timestamp = ");
  Serial.println(timestamp);
  Serial.print("value = ");
  Serial.println(value);
  Serial.println("---");

  // Create the JSON document
  StaticJsonDocument<200> doc;
  doc["timestamp"] = timestamp;
  doc["value"] = value;

  // Send the JSON document over the "link" serial port
  serializeJson(doc, linkSerial);

  // Wait
  delay(5000);
}

Receiver program

This program receive the JSON document and extracts the values.

#include <ArduinoJson.h>
#include <SoftwareSerial.h>

// Declare the "link" serial port
// Please see SoftwareSerial library for detail
SoftwareSerial linkSerial(10, 11); // RX, TX

void setup() {
  // Initialize "debug" serial port
  // The data rate must be much higher than the "link" serial port
  Serial.begin(115200);
  while (!Serial) continue;

  // Initialize the "link" serial port
  // Use the lowest possible data rate to reduce error ratio
  linkSerial.begin(4800);
}
 
void loop() {
  // Check if the other Arduino is transmitting
  if (linkSerial.available()) 
  {
    // Allocate the JSON document
    // This one must be bigger than for the sender because it must store the strings
    StaticJsonDocument<300> doc;

    // Read the JSON document from the "link" serial port
    DeserializationError err = deserializeJson(doc, linkSerial);

    if (err == DeserializationError::Ok) 
    {
      // Print the values
      // (we must use as<T>() to resolve the ambiguity)
      Serial.print("timestamp = ");
      Serial.println(doc["timestamp"].as<long>());
      Serial.print("value = ");
      Serial.println(doc["value"].as<int>());
    } 
    else 
    {
      // Print error to the "debug" serial port
      Serial.print("deserializeJson() returned ");
      Serial.println(err.c_str());
  
      // Flush all bytes in the "link" serial port buffer
      while (linkSerial.available() > 0)
        linkSerial.read();
    }
  }
}

Things used in these programs

Common problems

IncompleteInput

deserializeJson() returns IncompleteInput when the input JSON is truncated.

When applied to serial communication, this error is usually caused by:

  • receiver reading too slowly and dropping bytes
  • a timeout

In the first case, the sender writes bytes faster than the receiver reads them, filling up the receiver’s serial buffer until it overflows and drops incoming bytes. For example, this problem happens when the receiver is busy doing some other task when the sender is transmitting. It also occurs when the receiver logs incoming data at a slower rate; that’s why it’s crucial to get the “debug” serial running much faster than the “communication” serial.

You could solve this issue by increasing the serial buffer size; the details depend on each platform. For Arduino UNO, the default is 64 and can be changed by defining the SERIAL_RX_BUFFER_SIZE macro. For ESP8266 and ESP32, the default is 256 and can be changed by calling setRxBufferSize().

In the second case, the sender writes bytes slower than the receiver expects, so the read times out. Usually, this problem causes an EmptyInput error, but there are a few situations where it could return IncompleteInput. In this case, you can solve the problem by increasing the timeout or by polling Serial::available() before calling deserializeJson().

InvalidInput

deserializeJson() returns InvalidInput when the input JSON document is incorrect.

This problem is often due to errors in the serial communication. These errors come from:

  • clock skew
  • electric noise
  • voltage mismatch (5V vs 3.3V)
  • SoftwareSerial

Clock skew

Clock running at different speeds is a frequent problem when using different hardware on each side of the link (for example, an AVR and an ESP8266). If the sender is too fast, the receiver will occasionally receive phantom characters; and if the sender is too slow, the receiver will sometimes miss a character.

Clock accuracy is another problem of SoftwareSerial; that’s why you should always prefer hardware implementations.

You can mitigate this problem by reducing the baud rate, but you’ll never be able to eliminate the errors completely. If you need reliable serial communication, you must detect the errors (with a checksum, for example) and resend the data when needed.

Electrical noise

Any electric wire acts as an antenna; the longer the wire, the stronger the effect. This antenna picks up every electromagnetic field in the environment, which induces a current in the wire. On long wires, this current is strong enough to introduce errors in the communication.

You can mitigate this problem at the hardware and software levels.

At the hardware level, you can replace the wires with a coaxial cable: the shielding will prevent the inner wire from acting as an antenna.

At the software level, you can:

  1. reduce the baud rate
  2. use error-correction code
  3. add error detection

Reducing the speed of communication always improves the error ratio but is rarely enough.

Error-correction codes (ECC) are a way of transmitting the data with redundant information that allows the receiver to fix most of the errors. The most basic error-correction code is Hamming(7,4), which transmits 7 bits for every 4 bits of actual data. In other words, it adds 3 bits of redundancy for every 4 bits that you send. The magic with this code is that it can correct any 1-bit error in the 7 bits.

The simplest way to implement Hamming(7,4) on Arduino is to use the HammingStream class from the StreamUtils library:

HammingStream<7, 4> eccLinkSerial(linkSerial);

Now, you can use eccSerial1 in place of the original Serial1; it will automatically encode and decode the information.

As Hamming(7,4) only transmits 7 bits of data, you can safely downgrade the serial link from 8 to 7 bits. You can do this by passing SERIAL_7N1 as the second argument of Serial::begin(). This feature is not supported by SoftwareSerial, which is yet another reason to avoid it.

Error-correction codes are very powerful, but they’ll never eliminate errors completely. For example, Hamming(7,4) can only fix a 1-bit error, so if two or more bits are swapped, it will not fix them. To get more confidence in the integrity of the received data, the ultimate solution is to add an error detection scheme, like a checksum.

StreamUtils is a powerful library that deserves more attention. Please give it a star to spread the word.

Voltage mismatch

Not all microcontrollers use the same voltage for the serial port. Some use 5V logic; others use 3.3V; the table below shows the values for the most common development boards:

If you need to wire two devices with different voltages, you need a logic level converter.

SoftwareSerial

The AVR implementation of SoftwareSerial is notoriously unreliable 😱. The main problem is that it disables interrupts when sending data, which causes many issues like dropping incoming bytes on the regular Serial.

You may consider the following alternative libraries, but none of them is perfect:

Again, the best solution is to use a board with several UARTs, such as:

Troubleshooting

To troubleshoot an InvalidInput error, start by displaying the JSON input. The simplest way to do that is to use the ReadLoggingStream from the StreamUtils library:

ReadLoggingStream loggingStream(linkSerial, Serial);
deserializeJson(doc, loggingStream);

This program will print to Serial every byte it receives from linkSerial.

It’s crucial to have Serial running much faster than linkSerial; otherwise, the program will read too slowly from linkSerial. For example, in the program above, we used 115200 bauds for Serial and 4800 bauds for linkSerial.