In this post, we will continue exploring the connection of two processors like Arduino via 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 Followers on the bus.
We saw that we had two important limitations. The first, that communication involved sending or requesting a specific number 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 expanded to 64).
The usual solution is for the devices to implement a common structure that we use as the message definition, and to use this structure as an element encoded in the same way in both processors for communication.
But this was a bit of a cold shower, because one of the most interesting points of using I2C communication was communicating between different types of processors, like 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 the standard for data exchange.
Well! But let’s not throw in the towel yet. Actually, it is possible and not too complicated to send a Json of arbitrary length and greater than 32 bytes via the I2C port.
To do this, it’s as simple as making several requests to the Follower, 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 a maximum of 32 bytes.
But we’ll see it much better if we do a small example.
Master Code
So, here is the master code
#include "Wire.h"
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
const byte I2C_FOLLOWER_ADDR = 0x20;
String response;
StaticJsonDocument<300> doc;
void setup()
{
Serial.begin(115200);
Wire.begin();
}
void loop()
{
askFollower();
if(response != "") DeserializeResponse();
}
const char ASK_FOR_LENGTH = 'L';
const char ASK_FOR_DATA = 'D';
void askFollower()
{
response = "";
unsigned int responseLenght = askForLength();
if (responseLenght == 0) return;
askForData(responseLenght);
delay(2000);
}
unsigned int askForLength()
{
Wire.beginTransmission(I2C_FOLLOWER_ADDR);
Wire.write(ASK_FOR_LENGTH);
Wire.endTransmission();
Wire.requestFrom(I2C_FOLLOWER_ADDR, 1);
unsigned int responseLenght = Wire.read();
return responseLenght;
}
void askForData(unsigned int responseLenght)
{
Wire.beginTransmission(I2C_FOLLOWER_ADDR);
Wire.write(ASK_FOR_DATA);
Wire.endTransmission();
for (int requestIndex = 0; requestIndex <= (responseLenght / 32); requestIndex++)
{
Wire.requestFrom(I2C_FOLLOWER_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 Follower is done in two phases, contained in the function ‘askFollower()’. This first calls the function ‘askForLength’. Here we send to the Follower identified by ‘I2C_FOLLOWER_ADDR’ an identifier ‘ASK_FOR_LENGTH’ which is simply the character ‘L’.
The Follower will respond with the length of the message it has to send us. If it is greater than 0, it has something to tell us, and we move to phase two ‘askForData’.
In this second phase we transmit the identifier ‘ASK_FOR_DATA’, which in this code is the character ‘D’. The Follower will prepare to start phase 2 of data transmission. Finally, we request data from the Follower in blocks of a maximum of 32 bytes, using the length it 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 serial port. In a real project, of course, we would do whatever we needed to do with this data.
Follower Code
For its part, the Follower 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_FOLLOWER_ADDR = 0x20;
void setup()
{
Serial.begin(115200);
SerializeObject();
Wire.begin(I2C_FOLLOWER_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 Follower code is even simpler. First, we have generated the text string we want to send in the variable ‘Json’ during the start of the program. In a real project, of course, we would regenerate this variable when necessary and, probably, set a flag to know that we have pending data to send, for example.
The part that matters to us is the communication. On one hand, we have the callback function ‘receiveEvent’, which is responsible for receiving the characters that indicate the reception phase ASK_FOR_LENGTH and ASK_FOR_DATA, and storing it in the variable ‘request’.
On the other hand, in the ‘requestEvent’ callback we check the current phase stored in the variable ‘request’. 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 a maximum of 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 Follower in two phases, and dividing the sending into blocks of a maximum of 32 bytes.
By working in Json format we will have the advantage of not worrying about variable sizes when working on different microprocessors (here an int is 2 bytes, there it’s 4, etc etc).
On the other hand, as we know, a ‘normal’ 8-bit Arduino (Uno, Nano…) is tight on dynamic memory, and perhaps they are not the most suitable machines for working with Json format. Other processors like the ESP8266/32, or the Arduino Mega are much more comfortable with this task.
This does not mean we shouldn’t do it, only that we should pay special attention when working with dynamic memory. Something that, by the way, we should always do anyway. For example, don’t create a String variable to contain the sent or received Json each time, but reuse the same one (as we did in the example) or you will end up fragmenting and the program will become unstable.
If we take these considerations regarding dynamic memory usage into account, the truth is that Json format exchange via 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.

