Language: EN

arduino-i2c-json

Sending and Receiving Json Formatted Data over I2C in Arduino

In this post we are going to continue seeing the connection of two processors like Arduino through the I2C bus, seeing how to send and receive data in Json format.

In the previous post we saw how to connect two microprocessors via I2C, how to send data from the Master, and how to request data from one of the bus Slaves.

We saw that we had two important limitations. The first, that the communication was sending or requesting a certain amount of bytes. You cannot send a message of arbitrary length. The second is that the maximum message size in Arduino is 32 bytes (although it can be extended to 64).

The usual solution is for the devices to implement a common structure that we use as a message definition, and to use this structure as an encoded element in the same way in both processors for communication.

But this was a bit of a disappointment, because one of the most interesting points of using I2C communication was communicating between different types of processors, such as an Arduino with an ESP8266/32 or even a Raspberry Pi. And, at least in these last two, we aspire to use Json format as a standard in data exchange.

Well! But let’s not give up yet. In reality, it is possible and not too complicated to send a Json of arbitrary length and greater than 32 bytes through the I2C port.

To do this, it is as simple as making several requests to the Slave, in two phases.

  • Phase 1) We ask for the length of the data to be transmitted.
  • Phase 2) We collect the data in calls of maximum 32 bytes.

But we will see it much better if we do a little example.

Master Code

So, here we have the Master code

#include "Wire.h"
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>

const byte I2C_SLAVE_ADDR = 0x20;

String response;
StaticJsonDocument<300> doc;

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

void loop()
{
  askSlave();
  if(response != "") DeserializeResponse();
}

const char ASK_FOR_LENGTH = 'L';
const char ASK_FOR_DATA = 'D';
void askSlave()
{
  response = "";

  unsigned int responseLenght = askForLength();
  if (responseLenght == 0) return;

  askForData(responseLenght);
  delay(2000);
}

unsigned int askForLength()
{
  Wire.beginTransmission(I2C_SLAVE_ADDR);
  Wire.write(ASK_FOR_LENGTH);
  Wire.endTransmission();

  Wire.requestFrom(I2C_SLAVE_ADDR, 1);
  unsigned int responseLenght = Wire.read();
  return responseLenght;
}

void askForData(unsigned int responseLenght)
{
  Wire.beginTransmission(I2C_SLAVE_ADDR);
  Wire.write(ASK_FOR_DATA);
  Wire.endTransmission();

  for (int requestIndex = 0; requestIndex <= (responseLenght / 32); requestIndex++)
  {
    Wire.requestFrom(I2C_SLAVE_ADDR, requestIndex < (responseLenght / 32) ? 32 : responseLenght % 32);
    while (Wire.available())
    {
      response += (char)Wire.read();
    }
  }
}

char*  text;
int id;
bool stat;
float value;
void DeserializeResponse()
{
  DeserializationError error = deserializeJson(doc, response);
    if (error) { return; }
 
    text = doc["text"];
    id = doc["id"];
    stat = doc["status"];
    value = doc["value"];
 
    Serial.println(text);
    Serial.println(id);
    Serial.println(stat);
    Serial.println(value);
}

As we can see, the call to the Slave is done in two phases, contained in the ‘askSlave()’ function. This first calls the ‘askForLength’ function. Here we send to the Slave identified by ‘I2C_SLAVE_ADDR’ an identifier ‘ASK_FOR_LENGTH’ which is simply the character ‘L’.

The Slave will respond with the length of the message to be sent to us. If it is greater than 0, it means it has something to tell us, and we move on to phase two ‘askForData’.

In this second phase we transmit the identifier ‘ASK_FOR_DATA’, which in this code is the character ‘D’. The Slave will prepare to start phase 2 of data transmission. Finally, we make the data request to the Slave in blocks of up to 32 bytes, using the length it has sent us in the previous request.

Finally, in the main loop, we check if we have a pending response to process and, if so, we display the data via the serial port. In a real project, of course, we would do what we had to do with this data.

Slave Code

For its part, the Slave code is as follows,

#include "Wire.h"
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
 
String json;
StaticJsonDocument<300> doc;
void SerializeObject()
{
    doc["text"] = "myText";
    doc["id"] = 10;
    doc["status"] = true;
    doc["value"] = 3.14;
 
    serializeJson(doc, json);
}

const byte I2C_SLAVE_ADDR = 0x20;

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

  SerializeObject();

  Wire.begin(I2C_SLAVE_ADDR);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);
}

const char ASK_FOR_LENGTH = 'L';
const char ASK_FOR_DATA = 'D';

char request = ' ';
char requestIndex = 0;

void receiveEvent(int bytes)
{
  while (Wire.available())
  {
    request = (char)Wire.read();
  }
}

void requestEvent()
{
  if(request == ASK_FOR_LENGTH)
  {
    Wire.write(json.length());
    char requestIndex = 0;
  }
  if(request == ASK_FOR_DATA)
  {
    if(requestIndex < (json.length() / 32)) 
    {
      Wire.write(json.c_str() + requestIndex * 32, 32);
      requestIndex ++;
    }
    else
    {
      Wire.write(json.c_str() + requestIndex * 32, (json.length() % 32));
      requestIndex = 0;
    }
  }

}

void loop() 
{
}

The Slave code is even simpler. First, we have generated the text string that we want to send in the ‘Json’ variable at the beginning of the program. In a real project, of course, we would regenerate this variable when necessary, and probably activate a flag to know that we have pending data to send, for example.

The part that matters to us, the communication. On the one hand, we have the ‘receiveEvent’ callback function, which is responsible for receiving the characters that indicate the phase of the reception ASK_FOR_LENGTH and ASK_FOR_DATA, and storing it in the ‘request’ variable.

On the other hand, in the ‘requestEvent’ callback we check the current phase that we save in the ‘request’ variable. If we are in phase 1, we send the current length of the Json file. If it is phase 2, we send the Json file in blocks of up to 32 bytes.

Conclusion

We have seen how to send a message of indeterminate length between two microprocessors like Arduino by making the request from the Master to the Slave in two phases, and splitting the transmission into blocks of up to 32 bytes.

Working in Json format will have the advantage of not worrying about the size of the variables when working on different microprocessors (here an int is 2 bytes, there it is 4, etc etc).

On the other hand, as we know, a ‘normal’ 8-bit Arduino (Uno, Nano…) is quite tight on dynamic memory, and perhaps not the most suitable machines for working with Json format. Other processors such as the ESP8266/32, or the Arduino Mega, are much more comfortable with this task.

This does not mean that we should not do it, only that we should pay special attention when working with dynamic memory. Something that, on the other hand, we should always do in any case. For example, do not generate a String variable to contain the Json sent or received each time, if not reuse the same (as we have done in the example) or you will end up with fragmentation and the program will become unstable.

If we take into account these considerations regarding the use of dynamic memory, the truth is that the exchange in Json format through I2C is a great tool for sharing data between microprocessors, and a great way to connect an Arduino with an ESP8266/32 or a Raspberry Pi.