[EDIT: Experienced users on the forum are very skeptical about this idea due to it's complexity and believe it will be buggy, so I have abandoned it, in favor of using additional GPIO pins so that the slave can tell the master when it's got data]
TL;DR; I’m trying to send different sized packets of data from a slave to a master via I2C. I’m trying to make the transaction more efficient, by initially telling the master how many byte’s it’s going to receive, and then sending the actual data. I’m looking for feedback as to whether it’s a good solution.
I’m trying to achieve seamless 2-way communication via I2C, with 1 Arduino Master, and 2 Slaves, without the potentially problematic multi-mastering solution.
This is achieved by the master sending data to the slaves, and the master also requesting data from the slaves using the Wire.requestFrom() function.
The master sends data to the slaves whenever it needs to, and makes a request to the slaves every few seconds, to check if they have anything they want to send.
However, as others have noted, this wire.request.From() function is a bit weird. If you make a request of 32 bytes, but the slave only has 2 bytes to wants to send, the receive buffer will fill up with those 2 bytes, and then 30 more bytes of junk. There doesn’t seem to be an easy software fix to this, as it’s inherent to the hardware. More info here; Wire.h: Wrong number of bytes returned from the slave device (I2C) · Issue #384 · arduino/ArduinoCore-avr · GitHub
You can easily parse out a smaller variables from the rest of these useless bytes with an end marker, at the end of the packet you’re sending, as shown in my examples (using the wire.readbytesuntil function).
However, lets say I wanted to send various sized packets of data to the master every few seconds, lets say between 4 and 32 bytes. Since the master has no idea what is being sent, it will have the number of bytes requested constantly set to 32 bytes.
If I make a request for data every few seconds, and most of the time the slave is only sending 4 bytes, I will still be filling up the Master’s receive buffer with the number of bytes of the largest package, i.e. 32 bytes, every few seconds.
So, my question is if it’s inefficient for the I2C receive buffer to continually be filled up with bytes the size of the maximum packet size every few seconds, even if most of it is useless data?
To combat this, I’ve devised a simple solution.
NOTE: I am using character arrays in this example, for easy of debugging, but my final intended solution will involve the sending of byte arrays, and parsing out mostly numerical data.
Every few seconds, the master requests just 3 bytes of data. When the slave has nothing to send, it just sends back “0” and the null terminator.
When the slave does have something to send, the slave first calculates the size of what it being sent, and then sends this size to the master. The slave then immediately puts the array it intends to send into the Wire.Onrequest interrupt routine.
When the master receives this size, it then changes the number of bytes it’s requesting, to the size of the variable that the slave wants to send. (i.e. Wire.requestFrom(address, numbytes))
Then the master immediately makes another request and receives the actual data the slave wants to send. After that, the master goes back to requesting just 3 bytes until there’s another thing to be sent.
I have also tried to make the interrupt routine on the slave end as short as possible but let me know if it’s too long. I'm assuming it's fine since it just contains the changing of booleans and if-else statements, without any big calculations. The code seems to work flawlessly so far.
You can test this yourself by opening the Serial. Monitor on the slave end and entering in any data between a start and end marker “<” and “>”. E.g. type in < hello > and press enter. You can then see it being printed out on the Master end, along with how much is being placed into the receive buffer.
Let me know if this is a good solution, or if the continual filling up of the 32-byte buffer every few seconds is not actually going to produce that much of a performance disadvantage. And whether my solution could add too much complexity and potentially cause bugs.
Finally note that I’m already aware of UART and SPI and how easy it is to send the data bidirectional with these methods, but for interconnecting 3 or more microcontrollers, I2C seems to be the easiest solution.
Thanks
A quick summary of the programs;
- The master requests 3 bytes every second from the slave.
- When the slave has no data to send, it just sends back “0”.
- When the slave does have something to send, the slave initially sends the length (in bytes) of the data.
- Upon receiving this length, the master makes another request for the data itself.
- The master then parses this data into the appropriate variables.
- The master then dumps this received array, and then starts making new 3 byte requests for new data.
- In this demo, you give the slave new data from the serial monitor.
- The slave then sends the length of this data in bytes to the master.
- Then the data itself is sent.
Code for the master
#include <Wire.h>
char receivedArr[30];
const byte otherAddress = 9;
byte numBytes = 3; //default
byte receivedLength;
void setup() {
// put your setup code here, to run once:
Wire.begin();
Serial.begin(9600);
}
void loop() {
byte stop = true;
//STEP 1; 1 request made every second. The default number of bytes being requested is 3.
Wire.requestFrom(otherAddress, numBytes, stop);
Wire.readBytesUntil( '\0',(byte*) &receivedArr, numBytes);
//STEP 2; receiving the string length.
if(receivedArr[0] != '0'){ //this statement is true whenever there's new data, other than "0", being sent
//the first thing that will be sent from slave is the size of the data that it wants to send
byte receivedLength = atoi(receivedArr); //convert the received length from char to a byte
numBytes = receivedLength; //changes the requested number of bytes in the Wire.request function to the size of the incoming variable
Serial.print("length = ");
Serial.println(receivedLength);
//STEP 3; receiving the string itself.
Wire.requestFrom(otherAddress, numBytes, stop); //numBytes is now equal to size of incoming variable
Serial.print("bytes in buffer = ");
Serial.println(Wire.available());
Wire.readBytesUntil( '\0',(byte*) &receivedArr, numBytes); //the actual variable we want to send comes through here.
Serial.print("received string = ");
Serial.println(receivedArr); //print the variable
numBytes = 2;
memset(receivedArr,0,sizeof(receivedArr));//reset array
}
else{ //only use else for Serialprint. delete after
Serial.println(receivedArr);
}
delay(1000);
}
Code for the slave
#include <Wire.h>
const byte thisAddress = 9;
char sendNothing[2];
//editing array though serial line
const byte numChars = 30;
char receivedChars[numChars];
boolean newDataToSend = false;
boolean sendStringLengthToMaster = false;
boolean newData = false;
//bytes written to master
byte stringLength;
char stringLengthArr[3];
byte sizeofStringLengthArr;
void setup() {
// put your setup code here, to run once:
Wire.begin(thisAddress);
Wire.onRequest(sendEvent);
Serial.begin(9600);
receivedChars[0] = '0';
receivedChars[1] = '\0';
stringLength = 2;
sendNothing[0] = '0';
sendNothing[1] = '\0';
}
void loop() {
recvWithStartEndMarkers();
showNewData();
}
void sendEvent() //interrupt routine that sends variable to master
{
if(newDataToSend == false){
Wire.write((byte*) &sendNothing, 3 ); //STEP 1; by default it sends just "0" and terminator.
}
else{
if(sendStringLengthToMaster == false){
Wire.write((byte*) &receivedChars, stringLength); //STEP 3; lastly, we send the string itself
newDataToSend = false;
}
else{
Wire.write((byte*) &stringLengthArr, sizeofStringLengthArr); //STEP 2; when message ready to send,
//it first sends the length of the string
sendStringLengthToMaster = false;
}
}
}
//RECEIVE A NEW STRING THROUGH SERIAL LINE.
void recvWithStartEndMarkers() {
static boolean recvInProgress = false;
//receivedChars[0]= '0';
static byte ndx = 0;
char startMarker = '<';
char endMarker = '>';
char rc;
while (Serial.available() > 0 && newData == false) {
rc = Serial.read();
if (recvInProgress == true) {
if (rc != endMarker) {
receivedChars[ndx] = rc;
ndx++;
if (ndx >= numChars) {
ndx = numChars - 1;
}
}
else {
receivedChars[ndx] = '\0'; // terminate the string
recvInProgress = false;
stringLength = ndx;
ndx = 0;
newData = true;
}
}
else if (rc == startMarker) {
recvInProgress = true;
}
}
}
void showNewData() {
if (newData == true) {
Serial.print("length in bytes to send ... ");
Serial.println(stringLength);
Serial.print("string to send ... ");
Serial.println(receivedChars);
memset(stringLengthArr,0,sizeof(stringLengthArr)); //reset stringlength
itoa(stringLength, stringLengthArr, 10); //converts to char array
sendStringLengthToMaster = true;
sizeofStringLengthArr = sizeof(stringLengthArr);
newData = false;
newDataToSend = true; //statement made after byte is parsed
}
}
Example serial monitors. Com3 is slave, Com6 is master. "0" is received and printed when nothing else is being sent