Trouble sending 2D array over I2C

Hello World,

Have been experimenting with Arduino for almost a decade in various contexts. Am still an amateur at Arduino and am new to I2C; have been struggling over the past couple of weeks with communication between 2 Arduino's over I2C and am looking for some assistance. I've read dozens of explanations of I2C and tried several troubleshooting steps with no luck so far.

What I'm trying to do:
Send 6x6 2D array from one Arduino to another over I2C. The two devices are wired to each other. Ground to Ground. SDA to SDA (ie. A4 to A4). SCL to SCL (ie. A5 to A5).

Methods:
Arduino 1 (I2C slave, address 8, a Pro Trinket) reads sensor data from a 6 x 6 sensor array. Each data point is an analog measurement (0-1023).
The data are stored in a 6 x 6 2D array called SensorDataArray, which I initialize with the line:
int SensorDataArray[6][6] = {-1};
As each data point is read, it is stored into this Array, and I can access the individual data elements and print to Serial without any issue. To simplify things, I convert each byte into a single integer from 0-9 by SensorValueConverted = 10 - (SensorValueElement / 100). This also reverses my values such that an analog reading of 0 = 10, analog reading of 400 = 6, analog reading of 900 = 1, analog reading of 1000 = 0, etc.

I now have a 6x6 2D array of integers ranging from 0-9.
For the sake of troubleshooting, rather than use real data, I generate random elements for my 2D array using: SensorValueConverted = random(10).

Arduino 2 (I2C master, no address specified, an Uno) sends a request to Slave #8 for 36 bytes.
Arduino 2 then packages each byte into a 2D array called DataInputArray. It then prints this Array to the Serial monitor.

Problem:
The array on Arduino 2 doesn't populate as expected.

Master Code

#include <Wire.h> // This includes the Wire library which we need for I2C 

int DataInputArray[6][6] = {-1}; // initialize our 6x6 data array
int DataInputElement = -1;

// These are two counters we will use in the for loops which control each axis of our 2D array
int AnalogCount = 0;
int DigitalCount = 0;
// and one random counter
int i = 0;
int index;

void setup() {
  // initialize serial communication:
  Serial.begin(9600);

  //initialize I2C communications:
  Wire.begin();                // join i2c bus with no address  (Master address optional)
}

void loop() {
 
index = 0;
Wire.requestFrom(8,36); // request 36 bytes from slave #8
  while (Wire.available() > 0 & index<36) {
      for(DigitalCount=0;DigitalCount<6;DigitalCount++) {
          for(AnalogCount=0;AnalogCount<6;AnalogCount++) {  
              DataInputElement = Wire.read();
              DataInputArray[DigitalCount][AnalogCount] = DataInputElement; 
              index++;
          }
      }
   }

  for (DigitalCount=0;DigitalCount<6;DigitalCount++) {
    if (DigitalCount>0){
      Serial.println();
      }
    for (AnalogCount=0;AnalogCount<6;AnalogCount++) {
      Serial.print(DataInputArray[DigitalCount][AnalogCount]);
      Serial.print(",");
    }
  }

  Serial.print("&"); // end trigger
   // clear the serial monitor screen by typing a few empty lines
  for (i=0;i<7;i++) {
    Serial.println();  
   }
 delay(1000);
}

Slave Code

// This includes the Wire library which we need for I2C 
#include <Wire.h>

// These are two counters we will use in the for loops which control the pins
int AnalogCount = 0;
int DigitalCount = 0;
// and one random counter
int i = 0;

// Our data will be recorded into a giant 36 element 6x6 2D array, here we initialize all values to -1
// Remember both the rows and colums are zero-indexed so 6 elements = elements 0-5
int SensorDataArray[6][6] = {-1};
int SensorValueElement;
int SensorValueConverted;

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);

  //initialize I2C communications:
  Wire.begin(8);                // join i2c bus with address #8
  Wire.onRequest(requestEvent); // register event when the Master sends a request
}
 
void loop() {
  for(DigitalCount=0;DigitalCount<6;DigitalCount++) {
     for (AnalogCount=0;AnalogCount<6;AnalogCount++) {
      SensorValueConverted = random(10);
      SensorDataArray[DigitalCount][AnalogCount] = SensorValueConverted;
      delay(20);
      }
  }
}


  
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
// also, print the results to the serial monitor, this should print values, 6 on each line
 
void requestEvent() {
  Serial.print("%"); // start trigger
  for (DigitalCount=0;DigitalCount<6;DigitalCount++) {
    if (DigitalCount>0){
      Serial.println();
      }
    for (AnalogCount=0;AnalogCount<6;AnalogCount++) {
      Serial.print(SensorDataArray[DigitalCount][AnalogCount]);
      Serial.print(",");
      Wire.write(SensorDataArray[DigitalCount][AnalogCount]);
    }
  }


  Serial.print("&"); // end trigger
   // clear the serial monitor screen by typing a few empty lines
  for (i=0;i<7;i++) {
    Serial.println();  
   }


  // wait 1 seconds before doing the whole thing again
    delay(1000);
}

Example Slave Serial Output:

%3,9,0,5,5,4,
4,3,8,2,2,6,
4,0,0,1,1,5,
1,7,0,8,5,3,
7,9,6,3,1,5,
8,0,9,2,4,3,&

  • This is exactly as expected. A 6x6 2D matrix of values 0-9, pre-pended by a % and ended by a &.

Example Master Serial Output:

4,255,255,255,255,255,
255,255,255,255,255,255,
255,255,255,255,255,255,
255,255,255,255,255,255,
255,255,255,255,255,255,
255,255,-1,-1,-1,-1,&

  • Not as expected. This should mimic the slave output exactly.

NOTES:

  • I realize that the Wire.h allows only for a buffer of 32 bytes, which explains why the last 4 bytes in my master output are not populated (they show up as -1). I can work around this later.

  • The first byte shown in the master output does not correspond to the first byte shown in the slave output. So, I'm not quite sure where that value comes from, or where the 255 values come from.

Any advice is greatly appreciated!

Thank you!

The buffer is only 32 byte, and I'm not sure if all the 32 bytes can be used, or just 31 byte.

You have to keep the length within those 32 bytes ! Perhaps the code of the Wire library is not perfect and using more will make it go haywire.

The requestEvent() is (part of) an interrupt handler. You may not use any delay() or Serial function there. I know that using Serial is in the examples, but you should not do that.
Keep the requestEvent() as short and as fast as possible.

The array is used in the loop() and in requestEvent(). Therefor it must be 'volatile'.

An integer (two bytes) array of 6x6 = 72 bytes.
You have reduced the number, therefor you could use a 'byte' or 'char' array, that will be 36 bytes, and that is still too much.
It is possible that the master selects a 'set' of data by writing a number to the Slave.
It is also possible to use a nibble. I mean a number of 0...15 in a nibble, and two nibbles in a byte.

A variable called "command" could be used, which is set in requestEvent() and used in the loop().

volatile byte data[2][2];
volatile byte command = 0;         // must be 8-bit for Arduino Uno, because reading/writing 8-bit can not be interrupted.
...
void loop()
{
  // fill array, but don't let the requestEvent() happen while the array is being updated.
  noInterrupts();
  data[0][0] = 100;
  data[0][1] = 101;
  interrupts();
  
  // test command
  if( command == 1)
  {
    Serial.println(F( "Trigger"));
    command = 0;                         // clear command
  }
}

void requestEvent()
{
  Wire.write( data, sizeof( data));
  command = 1;
}

Thank you, Koepel! I will try this hopefully later today and let you know how it goes.

So I do know that the buffer can only handle 32 bytes, and the results are the same if I request 36, or 32, or even just 20 bytes. I don't think that that is my issue, but thanks for the reminder!

How many data do you want to transfer (not the test version, but for the final project) ? The full 10 bits of each sensor ?
How many times a second ?

Suppose 10 times per second, and full 10 bits.
The Master could send a command to reset the "row".
The Slave increments the row with every read by the Master, and the Master could do three requests for data, each request is for two rows of data (6 * 2 * 2 = 24 byte).

I hope you don't want to this 100 times per second.

Just the number 0...9 (or 0...15) with nibbles is 18 bytes (6*6/2). That can be done in a single request from the Master.

dadueler:
Hello World,

Have been experimenting with Arduino for almost a decade in various contexts. Am still an amateur at Arduino and am new to I2C; have been struggling over the past couple of weeks with communication between 2 Arduino's over I2C and am looking for some assistance. I've read dozens of explanations of I2C and tried several troubleshooting steps with no luck so far.

What I'm trying to do:
Send 6x6 2D array from one Arduino to another over I2C. The two devices are wired to each other. Ground to Ground. SDA to SDA (ie. A4 to A4). SCL to SCL (ie. A5 to A5).

Methods:
Arduino 1 (I2C slave, address 8, a Pro Trinket) reads sensor data from a 6 x 6 sensor array. Each data point is an analog measurement (0-1023).
The data are stored in a 6 x 6 2D array called SensorDataArray, which I initialize with the line:
int SensorDataArray[6][6] = {-1};
As each data point is read, it is stored into this Array, and I can access the individual data elements and print to Serial without any issue. To simplify things, I convert each byte into a single integer from 0-9 by SensorValueConverted = 10 - (SensorValueElement / 100). This also reverses my values such that an analog reading of 0 = 10, analog reading of 400 = 6, analog reading of 900 = 1, analog reading of 1000 = 0, etc.

I now have a 6x6 2D array of integers ranging from 0-9.
For the sake of troubleshooting, rather than use real data, I generate random elements for my 2D array using: SensorValueConverted = random(10).

Arduino 2 (I2C master, no address specified, an Uno) sends a request to Slave #8 for 36 bytes.
Arduino 2 then packages each byte into a 2D array called DataInputArray. It then prints this Array to the Serial monitor.

Problem:
The array on Arduino 2 doesn't populate as expected.

Ok, Limit you I2C data blocks to less than 32 bytes. You master code:

Wire.requestFrom(8,36); // is a nogo from the start.

in your Slave code don't put Serial.print in your OnRequestEvent() code. onRequestEvent() is called as and interrupt, and During the call all interrupts are disabled. So if your Serial.print() statements happen to fill up the 63 byte outgoing Serial buffer. You code will hang Forever! waiting for room to appear in the output buffer. (The Serial Object services the buffer with interrupt functions. While your are in a Interrupt Service Routine, Other interrupts are Disabled therefore no other interrupt will be serviced.)

Also in your Slave code:

     }
    for (AnalogCount=0;AnalogCount<6;AnalogCount++) {
      Serial.print(SensorDataArray[DigitalCount][AnalogCount]);
      Serial.print(",");
      Wire.write(SensorDataArray[DigitalCount][AnalogCount]);
    }
  }

This won't work. Due to design choice, only a Single Wire.write() is allowed during the ISR.

As your code calls Wire.write() six times, only the last Wire.write() is actually accepted.

Inside the Wire.Write() library call, the output buffer is initialized at each call. And, Wire.write() only sends bytes, you can call it, passing and integer value (int), but it will only send the low byte.

This 'feature' only occurs when Wire.write() is call from onRequestEvent().!!!!!

To accomplish what you want you are going to have to do a couple of things:

  • Set up a machine state engine (as simple as an offset counter)
  • have the slave expect a two step process.
    First a 'Wire.write()' of the 'offset counter' (onReceiveEvent()).
    Second always send a fixed length data block, starting at 'offset counter' (onRequestEvent()).
  • Or create a buffer to pass to Wire.write(&data,count);

The second option is not too hard:

volatile uint8_t offset;
void onRequestEvent(){
char *ptr=(char*)SensorDataArray[offset]; // I am assuming the each requestFrom() gets
// the next 'line' of data (6 ints, 12 bytes of data)
Wire.write(ptr,12); // send 12 bytes from the memory address pointed to by ptr.
offset = offset + 1; // set up for next line (next requestFrom())
offset = offset % 6;  // keep offset valid

}

Now you have to understand that the Slave never knows how many bytes the Master actually read during a onRequestEvent(). the onRequestEvent() only happens Once for each Wire.requestFrom() operation. If the buffer returned by onRequestEvent() is smaller than the Master asked for, the Master receives bytes of 0xFF until the Master 'reads' the request count. If the buffer is larger, the Master IGNORES the data.

I agree these limits Suck. I rewrote the Wire library fix these issues, and add some error recovery function.

You can get a copy of my modified Wire library from github (stickbreaker) or do a search of is forum.

Chuck.

Koepel:
How many data do you want to transfer (not the test version, but for the final project) ? The full 10 bits of each sensor ?
How many times a second ?

Suppose 10 times per second, and full 10 bits.
The Master could send a command to reset the "row".
The Slave increments the row with every read by the Master, and the Master could do three requests for data, each request is for two rows of data (6 * 2 * 2 = 24 byte).

I hope you don't want to this 100 times per second.

Just the number 0...9 (or 0...15) with nibbles is 18 bytes (6*6/2). That can be done in a single request from the Master.

At this stage, the final project needs to do 6 x 9 = 72 data points (data point = 0-10 value) at 1Hz. I might end up expanding that to 10Hz and up to 36 x 72 data points, so you are right, breaking it down into multiple "rows" (and probably "columns"). Thanks for that insight!

Sadly, haven't had a chance to implement any changes yet... Will keep you updated!

Thanks Chuck! I appreciate your insights, I had no idea about the limitations of the Wire.h library. Glad to get help from people smarter and more experienced than I am!

I'll check out your rewritten library and also implement the changes you suggested.

I do like the idea of using a state machine, similar to what Koeppel suggested above. Since I'm not actually a programmer, or an engineer, I'll have to spend some time re-familiarizing myself with them (the last time I used one was in LabVIEW years ago).

Quick question, though, If I don't use Serial output, how else could I check to see if my data is being transferred appropriately?
Also, right now I was using Serial outputs, but ultimately will be using an ESP8266 to post to an online server. How would you recommend I can input and output data without issue?

Thanks!

dadueler:
Thanks Chuck! I appreciate your insights, I had no idea about the limitations of the Wire.h library. Glad to get help from people smarter and more experienced than I am!

I'll check out your rewritten library and also implement the changes you suggested.

I do like the idea of using a state machine, similar to what Koeppel suggested above. Since I'm not actually a programmer, or an engineer, I'll have to spend some time re-familiarizing myself with them (the last time I used one was in LabVIEW years ago).

Quick question, though, If I don't use Serial output, how else could I check to see if my data is being transferred appropriately?
Also, right now I was using Serial outputs, but ultimately will be using an ESP8266 to post to an online server. How would you recommend I can input and output data without issue?

Thanks!

Instead of writing directly to Serial from you ISR, have the ISR set a flag. Check the status of the flag in you loop() code:

volatile uint8_t flag;  // a byte used for communication between the ISR and your Main Loop
//it is marked volatile because it can change at any time.  The compiler must not assume a value
// it has stored in a register is current,  every access must be from the 'memory' location

void onRequestEvent(){
// other code 
flag = flag | (1 << offset); // every time we send a block of data we mark that we sent it
}

void loop(){
if (flag){ //something has been sent since last we checked
  noInterrupts(); // turn off interrupts while save, update the volatile flag 
  uint8_t sf =flag;
  flag = 0; //clear flag
  interrupts(); // ok, now I have a local copy of flag, onRequestEvent() can make new changes
// without me not seeing,loosing them.
  uint8_t i = 0;
  char buffer[60]; // 60 byte buffer for my println
  while(i<6){// check each of the six bit flags that represent a line of data
    if(sf&(1<<i)) { // the i'th bit is set
      sprintf(buffer,"Sent Block %d: %d, %d, %d, %d, %d, %d\n",i,
        SenorDataArray[i][0],SenorDataArray[i][1],SenorDataArray[i][2],
        SenorDataArray[i][3],SenorDataArray[i][4],SenorDataArray[i][5]); 
      Serial.print(buffer); 
      }
    i=i+1;
    }
  }
// other main loop stuff
}

Chuck.

36 * 72 is too much. Should the Master send that data to the computer ? How ? The Master has trouble with that amount of data. Or should the Master do calculations with it ? Which program on the computer is receiving that data ?

You can continue for now with 6 * 9. I think with the data as bytes and a few rows per I2C transmission will work.
The I2C is slow and only for short wires (about 50 cm maximum). The SPI bus is faster.
If it is possible to do everything with a single Arduino Mega 2560, or with a single Arduino Leonardo or Micro, that would be a lot easier.

Thank you both! Got it working Thursday after implementing suggestions from both of you, so am very grateful!
Am presently working on the next update, and will keep you posted here!
Hopefully by tonight should have updated code up!