Problem sending string using I2C

I have a Nano (Master) sending data to Nano (Slave) using I2C. I'm using the Wire examples included with Wire library. I edited the master_writer to send at string. The problem is the string is cut off and an unknow integer is added to the end. What am I doing wrong. Thanks

Master Code:

#include <Wire.h>

void setup() {
  Wire.begin(); // join i2c bus (address optional for master)
}

byte x = 0;

void loop() {
  Wire.beginTransmission(8); // transmit to device #8
  Wire.write("81,0.00,0.00,70.14,54.41,48.97,29.86,0,1");        // sends five bytes 
  Wire.endTransmission();    // stop transmitting

  
  delay(500);
}

Slave Code:

#include <Wire.h>

void setup() {
  Wire.begin(8);                // join i2c bus with address #8
  Wire.onReceive(receiveEvent); // register event
  Serial.begin(9600);           // start serial for output
}

void loop() {
  delay(100);
}

// function that executes whenever data is received from master
// this function is registered as an event, see setup()
void receiveEvent(int howMany) {
  while (1 < Wire.available()) { // loop through all but the last
    char c = Wire.read(); // receive byte as a character
    Serial.print(c);         // print the character
  }
  int x = Wire.read();    // receive byte as an integer
  Serial.println(x);         // print the integer
}

Slave Serial Monitor:

16:04:42.827 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:43.339 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:43.809 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:44.325 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:44.825 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:45.336 -> 81,0.00,0.00,70.14,54.41,48.97,50
16:04:45.849 -> 81,0.00,0.00,70.14,54.41,48.97,50

Hello, do yourself a favour and please read How to get the best out of this forum. I've added code tags for you this time.


There are five buffers used by the TWI and Wire library. They are defined as 32 bytes. So that's likely why your data is truncated

We don't send readable ASCII text over I2C.
You can combine a few variables in a 'struct' and transfer the struct over I2C. Then the data has a fixed length.
If they are only 'float' numbers, then you can also transfer an array of 'float' numbers over I2C.

Thanks for the reply. Could you suggest a way to fix my problem. Thanks

Can you tell more about your project ?
Why do you want to use I2C between Arduino boards ? Is there some distance between the boards ? What kind of data do you want to transfer ?

If you can do your project with a single Arduino board, then it will be easier.

My project is a weather station. I'm new to Arduino territory. Thought this would be a good project to learn a lot about communication, sensors and ect. My problem is I' m low on memory so I thought I would split my program and use two nanos. All sensors input into the master and the slave is to be used for processing data and sending it to the web. I was sending sensor data by concatenating it into a string and using I2C send it to slave.
So that's my story. I'm thinking maybe I should use SPI or Software serial instead of I2C. Would one of them work better. Thanks

Solving a problem by getting into more problems is not the right solution.
Can you buy a Arduino board with more memory ?

Serial/UART communication with SoftwareSerial is possible, but then I suggest to buy a Arduino board with more serial ports. The SoftwareSerial takes over the entire Arduino and might conflict with libraries that use interrupts or turn off interrupts for some time.

If you buy a board with more serial ports, then buy a Arduino board with more memory, so the project can be done with a single Arduino board :wink:

It is perfectly allowed and works well, but limited by 32-byte buffer size which can be increaed by editing the Library.

As @J-M-L has mentioned in Post-2, your received string is limited to 32-byte due to buffer size of the I2C Library. You can increase the buffer size to 64-byte in the following ways.

Goto the following path (in my PC)
Start ---> File Explorer ---> This PC ---> Windows (C:) ---> Program Files (0x86) ---> Arduino ---> hardware ---> arduino ---> avr ---> libraries ---> Wire ---> src ---> Wire.h (open with WordPad in admin mode) and change this line: #define BUFFER_LENGTH 32 to #define BUFFER_LENGTH 64 and then save and then exit.

Please be careful, the function you specify in Wire.onReceive(), in this case receiveEvent, is called by an ISR (interrupt service routine) and is used as an interrupt handler. DO NOT use Serial.print in this function because it can lock up your code if the Serial transmit buffer becomes full. Also note that the argument howMany will contains the number of bytes received from the master, so there is no need to check Wire.available.

1 Like

That’s no longer the case (the code checks for interrupts) but it is indeed a bad idea to do lots of work In an interrupt Context

this is an example

MASTER CODE:

#include <Wire.h>

uint8_t payload[] = "0123456789";
const size_t payloadSize = sizeof payload - 1 ; // -1 to not send the trailing null

void setup() {
  Serial.begin(115200);         // start serial for output
  Serial.println(F("\n--- WIRE MASTER ---"));
  Wire.begin();                 // join i2c bus as master
  Serial.println(F("Enter any key to get started"));
  while (Serial.read() == -1); // wait for user input
}

void loop() {
  Wire.beginTransmission(8);    // identify the target device (address 8)

  // build the payload (multiple write possible up to 32 bytes)
  Wire.write(payload, payloadSize);
  Wire.write(payload, payloadSize);
  Wire.write(payload, payloadSize);
  Wire.write(payload, payloadSize);  // this one will get truncated and only "01" will be sent as the wire buffer is 32 bytes deep
  Serial.println();
  Wire.endTransmission();       // send the payload over

  delay(5000);                  // wait a bit before sending it again
}

SLAVE CODE

#include <Wire.h>
const byte bufferSize = 32;
volatile byte receivedBuffer[bufferSize];
volatile bool dataReceived = false;
volatile byte dataSize = false;

// this is to avoid compiler warning about memcpy and volatile
void * volatileMemcpy (void *dest, const volatile void *src, size_t len) {
  uint8_t *d = (uint8_t *) dest;
  const volatile uint8_t *s = (const volatile uint8_t *) src;
  while (len--) *d++ = *s++;
  return dest;
}

void useData() {
  static byte receivedBufferCopy[bufferSize];
  byte dataSizeCopy;

  // critial section to copy the data before it's used in case a new packet comes in
  noInterrupts();
  dataSizeCopy = dataSize;
  volatileMemcpy(receivedBufferCopy, receivedBuffer, dataSize); // the standard memcpy (https://www.cplusplus.com/reference/cstring/memcpy/) might not be compatible with volatile
  dataReceived = false;
  interrupts();
  // end of critical section, we are working on a clean copy

  // dump the data
  Serial.print(F("New data: "));
  for (byte i = 0; i < dataSizeCopy; i++) Serial.write(receivedBufferCopy[i]);
  Serial.println();
}

void wireCallback(int howMany) {
  dataSize = min(bufferSize, howMany);
  dataReceived = true;
  // empty the buffer and store what fits
  for (int i = 0; i < howMany; i++)
    if (i < bufferSize) receivedBuffer[i] = Wire.read();
}

void setup() {
  Serial.begin(115200);         // start serial for output
  Serial.println(F("\n--- WIRE SLAVE ---"));
  Wire.begin(8);                // join i2c bus with address #8
  Wire.onReceive(wireCallback); // register event
}

void loop() {
  if (dataReceived) useData();
}

In the master I try to send 40 bytes but you'll see only 32 are received in the slave. That's because I reached the limit of the Wire buffer.

The documentation states wrongly that wire.write() will return the number of bytes written but it's not true, it returns the size of whatever you tried to send, disregarding what was actually stored in the buffer, so there is no way to know if you reached the buffer limit ➜ you have to keep a count yourself in the master application

using ISR comes with strings attached: You'll see in this slave example that I used volatile variables, that's because you receive the data in an ISR context and you want to pass that data to the main loop where it will be used thanks to a flag indicating that you got new data in.

From my experience, receiveEvent() function is neither an ISR nor an interrupt context. It supports print() method as is seen in the following codes. The print() method requires enabled "interrupt logic of the MCU" which remains disabled in an ISR. The function is automatically called upon from the loop() function of the I2C Slave after the execution of the Wire.endTransmission() command by the I2C Master.

void receiveEvent(int howMany) 
//howMany == 2 == number of data bytes (excluding address byte) received from Master
{
    	byte myArray[2];
        for(int i = 0; I < howMany; i++)
	    {
          myArray[i] = Wire.read();     
    	}
	    Serial.println(myArray[0], HEX); //shows: 12 ; this line could be executed in loop()
	    Serial.println(myArray[1], HEX); //shows: 34			
} 

So your experience is misleading you....

As stated above, Print works now (did not used to be the case a while back) because they check if the interrupts are disabled and poll the data register empty flag directly, preventing this way the lockup.

The callback is indeed executed within an ISR context, since it's called from an ISR.

If you look at the source code, this is where the callback is triggered

if you look when TwoWire::onReceiveService is called, that's here in twi.c which is right in the ISR(TWI_vect) function.

hope this helps...

1 Like

So, print() method should not be executed in the receiveEvent() routine?

Well - print() β€” especially if the TX buffer gets full β€” is slow. You don't want a slow ISR because everything else is locked up. ➜ That's the reason why you should prefer to just get the data, raise a flag that data is ready and return rather than do the heavy lifting within the ISR context. My sample code above in #12 demonstrates that.

1 Like

This is one kind of solution to receive/print string using I2C Bus when you are not changing the I2C Buffer size from 32 to 64.
Master Sketch:

#include <Wire.h>

void setup()
{
  Serial.begin(9600);
  Wire.begin(); // join i2c bus (address optional for master)
}

void loop()
{
  Wire.beginTransmission(8); // transmit to device #8
  Wire.write("81,0.00,0.00,70.14,54.41,");
  Wire.print('\n');
  Wire.endTransmission();    // stop transmitting
  //----------------------------------------
  Wire.beginTransmission(8); // transmit to device #8
  Wire.write("48.97,29.86,0,1");        // sends five bytes
  Wire.print('\n');
  Wire.endTransmission();    // stop transmitting
  delay(1500);
}

Slave Sketch:

#include <Wire.h>
char myBuff[32];
byte counter = 0;
volatile bool flag = false;
byte m;

void setup()
{
  Wire.begin(8);                // join i2c bus with address #8
  Wire.onReceive(receiveEvent); // register event
  Serial.begin(9600);           // start serial for output
}

void loop()
{
  if (flag == true)
  {
    Serial.print(myBuff);
    flag = false;
    if (counter == 2)
    {
      Serial.println();
      counter = 0;
    }
  }
}

void receiveEvent(int howMany)
{
  byte m = Wire.readBytesUntil('\n', myBuff, 32);
  myBuff[m] = '\0';
  flag = true;
  counter++;
}

Slave's Serial Monitor:

81,0.00,0.00,70.14,54.41,48.97,29.86,0,1
81,0.00,0.00,70.14,54.41,48.97,29.86,0,1
81,0.00,0.00,70.14,54.41,48.97,29.86,0,1
81,0.00,0.00,70.14,54.41,48.97,29.86,0,1
81,0.00,0.00,70.14,54.41,48.97,29.86,0,1

Thanks for all the good info. I set out to learn from this project and it definitely has been a learning experience. I will give your suggestions a try.