(Solved!) Transmitting int over i2c issue

I have an interesting problem when I try to send an int value over i2c from a master to a slave data-logger. I have a couple of functions that pass various messages in the form of a single byte to the data logger, and the logger uses String to write the appropriate message to an SD card for later retrieval and analysis. That part works fine. Now, I wish to also send an integer (2 byte) value to be displayed along with the message, and that's where I have the issue; I have to send the same message twice to record the correct value. The first iteration is ALWAYS wrong; it is either a zero, or it is the last value that was sent. It's like the Wire buffer is not being cleared for some reason.

This is the data logger (slave) code:

#include <SD.h>
#include <Wire.h>
#include <RTClib.h>
#include <SPI.h>

#define LOGGER_ADDRESS 118
#define chipSelect 10
#define cardDetect 8

//RTC_DS1307 rtc; //uncomment for old datalogger
RTC_PCF8523 rtc; //uncomment for new datalogger

File logFile;
byte loByte, hiByte;
volatile boolean eventFlag = false;
volatile boolean status = 0;
volatile byte event;
int intValue;

void setup () 
{

  rtc.begin();  
// rtc.adjust(DateTime(2019, 2, 10, 15, 12, 10));
  Serial.begin(9600);
  pinMode(chipSelect,OUTPUT);
  pinMode(cardDetect, INPUT_PULLUP);
  Wire.begin(LOGGER_ADDRESS);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
  Serial.begin(9600);
  SD.begin(chipSelect);
  }

 ////////////////////////////////////////////////////////////////////////////////////
void receiveEvent(int howMany)
{   
    if(Wire.available() == 1)
    {
      event = Wire.read();
      eventFlag = true; 
    }
    
    if(Wire.available() == 2)
    {
      hiByte = Wire.read();
      loByte = Wire.read();
      intValue = word(hiByte,loByte);    
      eventFlag = true;
    }
    
}
////////////////////////////////////////////////////////////////////////////////////
void requestEvent() 
{   
  Wire.write(status);
 }
////////////////////////////////////////////////////////////////////////////////////

void loop () 
{
 if(digitalRead(cardDetect))
 {
  status = 0;
 }    
 else status = 1;
  
  
  if(eventFlag)
  { 
   writeToSD(event);
   eventFlag = false;
  }
 }
////////////////////////////////////////////////////////////////////////////////////
void writeToSD(byte event)
{
  String dataString = "";
  DateTime now = rtc.now();

  dataString += String(now.year());
  dataString += "/";
  dataString += String(now.month());
  dataString += "/";
  dataString += String(now.day());
  dataString += " ";
  dataString += String(now.hour());
  dataString += ":";
  dataString += String(now.minute());
  dataString += ":";
  dataString += String(now.second());
  dataString += " ";
  switch (event){

    case 97: //a 
    dataString += F("Data Logger test");
    break;

    case 100: //d
    dataString += F("This is the first test");
    dataString += F("  Sent value: ");
    dataString += String(intValue);
    break;

    case 119: //w
    dataString += F("This is the second test");
    dataString += F("  Sent value: ");
    dataString += String(intValue);
    break;
}
  
  logFile = SD.open("Log.txt",FILE_WRITE);
  
  if(logFile)
  {    
    logFile.println(dataString);    
    logFile.close();     
    status = 1;
  }  
  else
  
    status = 0;  
  
 }

And this is the master code:

#include <Wire.h>
#define LOGGER_ADDRESS 118


byte loByte, hiByte;
void setup() {
  Serial.begin(9600);
  Wire.begin();  
  Wire.beginTransmission(LOGGER_ADDRESS);
  Wire.write(97);
  if(Wire.endTransmission() != 0)
  {
    Serial.println("Logger not connected");
  }
  else Serial.println("Logger connected");
}

void loop() {
  byte code;
  byte r;
  if(Serial.available())
  {
  code = Serial.read();
  Serial.print("Code sent:");
  Serial.println(code);
  if(code == 'd')
  {
  Wire.beginTransmission(LOGGER_ADDRESS);
  Wire.write(code);
  Wire.endTransmission();
  delay(10);   // Changed this to 1 millisecond and now it works fine.
  sendInt(500);
  
   }
  else if(code == 'w')
  {
  Wire.beginTransmission(LOGGER_ADDRESS);
  Wire.write(code);
  Wire.endTransmission();
  delay(10);   // Changed this to 1 millisecond and now it works fine.
  sendInt(10000);
  
  }
  else
  {
  Wire.beginTransmission(LOGGER_ADDRESS);
  Wire.write(code);
  Wire.endTransmission();
  delay(10);
   }
  Wire.requestFrom(LOGGER_ADDRESS,1);
  while(Wire.available())  
  {
    r = Wire.read();
  }

  if(r == 0)
  {
    Serial.println("Card removed");
  }
  else if(r == 1)
  {
  Serial.println("Logging successful");
  }
  
  }  
}
//////////////////////////////////////////////////////
void sendInt(int valueToSend)
{
  loByte = lowByte(valueToSend);
  hiByte = highByte(valueToSend);


Wire.beginTransmission(LOGGER_ADDRESS);
Wire.write(byte(hiByte));
Wire.write(byte(loByte));
Wire.endTransmission();
Serial.print("Value:");
Serial.println(valueToSend);
}

This is the results on the serial monitor:

Logger connected
Code sent:97
Logging successful
Code sent:100
Value:500
Logging successful
Code sent:100
Value:500
Logging successful
Code sent:119
Value:10000
Logging successful
Code sent:119
Value:10000
Logging successful

This is the data written to the SD card:

2019/2/15 13:24:18 Data Logger test
2019/2/15 13:24:20 This is the first test  Test value: 0
2019/2/15 13:24:20 This is the first test  Test value: 500
2019/2/15 13:24:21 This is the second test  Test value: 500
2019/2/15 13:24:22 This is the second test  Test value: 10000

Note that the first test value should always be 500, and the second test value should always be 10000

It's most likely something stupid that I'm just not seeing. I have stared at it so long at this point that my mind is like a hamster in a wheel, going round and round. Thank you for any help you may offer!

Edit---made the changes Paul S suggested. Same issue, unfortunately.

 SD.begin(chipSelect);

This should be done ONCE, in setup(), not over and over in loop().

  Wire.beginTransmission(LOGGER_ADDRESS);
  if(Wire.endTransmission() != 0)

What is the purpose of this? Somewhere between beginTransmission(), which sets up buffers, etc. and endTransmission() which actually sends the data, you are supposed to supply some data.

void requestEvent()
{ 
 
  if(status == 0)
   {
     Wire.write(0);
   }
   if(status == 1)
   {
    Wire.write(1);
   } 
}

In other words, Wire.write(status);. Why do you need 8 lines of code, when one will do?

@Paul S...Thank you for wading through all that! You are right, I did this quite awhile ago, and I admit there was a lot of copy/paste. I just went back and fixed what you pointed out, but still have the same issue. I'll edit the original post to reflect the changes. I am also now using the highByte/lowByte method to sent the data, but still have the same exact problem. My feeling is that because the intValue variable is global, that is what the problem may be.....but I don't know the best way to fix it.

Well, the plot thickens, and now I am really bewildered. I changed the delay between when the master sends the single byte code, and the 2 bytes of the integer from 10 milliseconds to 1 millisecond, and now it works perfectly. If I eliminate the delay, the the code byte and integer bytes transfer fine, but the date stamp is gibberish. With a delay of 1 millisecond, everything, including the date, transfers and writes to the SD card just ducky.

So, I guess this is solved, but I'd feel a whole lot better if I knew why. It reeks of sorcery, to be honest.

@Paul S, I had to go back and add

if(digitalRead(cardDetect))
 {
  status = 0;
  do{}
  while(digitalRead(cardDetect));
  SD.begin(chipSelect);
 }    
 else status = 1;

to the loop, otherwise, when I removed and reinserted the card, it failed to write to it about 75% of the time. No idea why, but I'm sick of fighting with it, to be honest. If sorcery is what it needs to work, then sorcery it will get.

o, I guess this is solved, but I'd feel a whole lot better if I knew why. It reeks of sorcery, to be honest.

The Master should be sending the command code and the integer in one transmission of 3 bytes.

The onReceive() function of the Slave is actually an interrupt service routine. It should read the three bytes, reassemble the integer, and act on the command.

The stepping through the two Wire.available() tests in one ISR is the wrong approach.

void receiveEvent(int howMany)
{   
    if(Wire.available() == 1)
    {
      event = Wire.read();
      eventFlag = true; 
    }
    
    if(Wire.available() == 2)
    {
      hiByte = Wire.read();
      loByte = Wire.read();
      intValue = word(hiByte,loByte);    
      eventFlag = true;
    }
    
}

cattledog:
The Master should be sending the command code and the integer in one transmission of 3 bytes.

The onReceive() function of the Slave is actually an interrupt service routine. It should read the three bytes, reassemble the integer, and act on the command.

The stepping through the two Wire.available() tests in one ISR is the wrong approach.

void receiveEvent(int howMany)

{ 
    if(Wire.available() == 1)
    {
      event = Wire.read();
      eventFlag = true;
    }
   
    if(Wire.available() == 2)
    {
      hiByte = Wire.read();
      loByte = Wire.read();
      intValue = word(hiByte,loByte);   
      eventFlag = true;
    }
   
}

Thank you. I hadn't thought of that. I'll try rewriting it, but I'm not sure how.

I'll try rewriting it, but I'm not sure how.

Give it a go, we'll help sort it out if it doesn't work.

If you send the three byte message in one transmission, then the basic receiveEvent() should look like this

void receiveEvent(int howMany)
{
  event = Wire.read();
  hiByte = Wire.read();
  loByte = Wire.read();
  intValue = word(hiByte, loByte);
  eventFlag = true;
}

cattledog:
Give it a go, we'll help sort it out if it doesn't work.

If you send the three byte message in one transmission, then the basic receiveEvent() should look like this

void receiveEvent(int howMany)

{
  event = Wire.read();
  hiByte = Wire.read();
  loByte = Wire.read();
  intValue = word(hiByte, loByte);
  eventFlag = true;
}

Will do. I already rewrote receiveEvent() just as you have it, but forgot to redo the send command on the master to send all 3 bytes at one go....doing it now. Thank you very much, as now I feel I am on the right track, minus the sorcery.

I give up for tonight. No matter how I try to organize and send the 3 bytes from the master, it either writes gibberish to the card, or nothing at all. Obviously, I am yet again overlooking something....

As a side note, I see that I am a Full Member now....how ironic. I really feel like a member at the moment, LOL

JohnDeere630:
So, I guess this is solved, but I'd feel a whole lot better if I knew why. It reeks of sorcery, to be honest.

1. A faulty system may become 'suddenly' operational during the course of 'fault finding'; if so, we should not assume/decide that the system has been fixed. We need to strife hard to find the reasons of male functioning and then proceed to trouble shooting according to a list what we call 'Check List'.

2. You should set your goals first as to the meaning of sending 97 and 500 (1000) to the Slave. What will Slave do after receiving 97? Also, what is the purpose of 500 (1000).

3. Your original Master Program is not properly constructed based on the I2C Protocol. You should gather/queue first all the data bytes and then transfer them to the Slave in ONE-GO. (Your Slave Program should follow the Master Program. There is no need to insert any time delay by calling delay() function as the I2C is a synchronized bus by virtue of ACK/NAK handshakings.)

4. You are sending 97 and 500 (when d-command comes from Serial Monitor) or 97 and 1000 (when w-command comes from Serial Monitor). The following codes (tested between UNO and NANO) should be under Master loop() function:

#include<Wire.h>
#define LOGGER_ADDRESS 118
byte code;

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

void loop()
{
  if (Serial.available() > 0)
  {
    code = Serial.read();
    if (code == 'd')
    {
      i2cTransfer(97, 500);
    }
    if (code == 'w')
    {
      i2cTransfer(97, 1000);
    }
  }
}

void i2cTransfer(int x, int y)
{
  Wire.beginTransmission(LOGGER_ADDRESS);
  Wire.write(x);
  Wire.write(highByte(y));
  Wire.write(lowByte(y));
  byte busStatus = Wire.endTransmission();
  if (busStatus != 0x00)
  {
    Serial.println("Data transaction has not happened....!");
    while (1);  //wait for ever
  }
}

5. The following is the Slave sketch which receives the data bytes (97, 500) or (7, 1000) from the Master; the bytes are stored in an array named dataArray[], and they are also showed in the Serial Monitor of Slave (tested in NANO).

#include<Wire.h>
#define LOGGER_ADDRESS 118
byte dataArray[20];
int intValue;
int i = 0;

void setup()
{
  Serial.begin(9600);
  Wire.begin(LOGGER_ADDRESS);

  Wire.onReceive(receiveEvent);
}

void loop()
{

}

void receiveEvent(int howMany)
{
  for (int i = 0; i < howMany; i++)  //received bytes are always equal to the variable howMany
  {
    dataArray[i] = Wire.read();//dataArray[] holds 3-byte data:(97, 01, F4 = 0, 500) or (97, 03, E8 = 0, 1000)
    if (dataArray[i] < 0x10)
    {
      Serial.print('0');  //show leading 0
    }
    Serial.print(dataArray[i], HEX); //received bytes are shown in hex
  }
  intValue = (dataArray[1] << 8) | dataArray[2];
  Serial.println();
  Serial.println(intValue, DEC); //shows 500 or 1000
  i = 0;
}

6. Now, add codes with Slave sketch to take action based on the values of dataArray[0] = 97 and intValue (dataArray[1]<<8 | dataArray[2]). In Arduion, it has been advised not to String data type with capital S; rahter, use normal character type string like char myArray[50] for the storage of character type data.

Well, I finally go it to work right, and the code isn't too badly mangled. I wrote a function that accepts the code byte and any int data, converts it to three bytes and transmits them to the slave. I'll post the new code below for the benefit of anyone coming across this in the future.

Many thanks to the great people of this forum that took the time to parse the dreadful original code and offer helpful advice/insight. Your efforts were not in vain....I learned a lot in this exchange.

Master code function:

void WriteToLog(byte code, int valueToSend)
{
  byte r; 
  
    if(valueToSend > 0)
  {
 byte loByte = lowByte(valueToSend);
 byte hiByte = highByte(valueToSend);
  
Wire.beginTransmission(DATA_LOGGER_ADDRESS);
Wire.write(code);
Wire.write(byte(hiByte));
Wire.write(byte(loByte));
Wire.endTransmission();
  }
  else
  {
Wire.beginTransmission(DATA_LOGGER_ADDRESS);
Wire.write(code);
Wire.endTransmission();
  }  
  delay(10); //This is neccessary
  Wire.requestFrom(DATA_LOGGER_ADDRESS,1); //Check to be sure data was recorded
  while(Wire.available())  
  {
    r = Wire.read();
  }
  if(r != 1)
  {
  //something wrong, indicate write error
  }
  else 
  {
    //Success!
  }

Slave (data logger) sketch:

#include <SD.h>
#include <Wire.h>
#include <RTClib.h>
#include <SPI.h>

#define LOGGER_ADDRESS 118
#define chipSelect 10
#define cardDetect 8

//RTC_DS1307 rtc; //uncomment for old datalogger
RTC_PCF8523 rtc; //uncomment for new datalogger

File logFile;
byte loByte, hiByte;
volatile boolean eventFlag = false;
volatile boolean status = 0;
volatile byte event;
int intValue;

void setup () 
{

  rtc.begin();  
// rtc.adjust(DateTime(2019, 2, 10, 15, 12, 10));
  Serial.begin(9600);
  pinMode(chipSelect,OUTPUT);
  pinMode(cardDetect, INPUT_PULLUP);
  Wire.begin(LOGGER_ADDRESS);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
  SD.begin(chipSelect);
  
  }

 ////////////////////////////////////////////////////////////////////////////////////
void receiveEvent(int howMany)
  { 
    event = Wire.read();  
    hiByte = Wire.read();
    loByte = Wire.read();
    intValue = word(hiByte,loByte);      
    eventFlag = true;  
  }
////////////////////////////////////////////////////////////////////////////////////
void requestEvent() 
{   
  Wire.write(status);
 }
////////////////////////////////////////////////////////////////////////////////////

void loop () 
{
 if(digitalRead(cardDetect)) //This if statement checks for a removed card, and only invokes SD.begin() when 
 {                                     // the card has been reinserted.
  status = 0;
  do{}
  while(digitalRead(cardDetect));
  SD.begin(chipSelect);
 }    
 else status = 1;
  
  
  if(eventFlag)
  { 
   writeToSD(event);
   eventFlag = false;
  }
 }
////////////////////////////////////////////////////////////////////////////////////
void writeToSD(byte event)
{
  String dataString = "";
  DateTime now = rtc.now();

  dataString += String(now.year());
  dataString += "/";
  dataString += String(now.month());
  dataString += "/";
  dataString += String(now.day());
  dataString += " ";
  dataString += String(now.hour());
  dataString += ":";
  dataString += String(now.minute());
  dataString += ":";
  dataString += String(now.second());
  dataString += " ";
  switch (event){

    case 97: //   Log entry with only code 
    dataString += F("Data Logger test");
    break;
    
    
    case 100: //  Log entry with data
    dataString += F("Test message");
    dataString += F("   Test data: ");
    dataString += String(intValue);
    break;
 
//Rest of the cases 

    default: break;
  }
  
  logFile = SD.open("Log.txt",FILE_WRITE);
  
  if(logFile)
  {    
    logFile.println(dataString);    
    logFile.close();     
    status = 1;
  }  
  else
  
    status = 0;  
  
 }

Your efforts were not in vain....I learned a lot in this exchange.

If you would like to learn a bit more, I recommend that that get rid of any String usage in your code.

String dataString = "";

Using the String Object-- "Strings" with a capital "S" --can create memory problems which are fatal to a small microprocessor like in the Arduino.

You should learn to use "strings" with a small "s" which are null terminated character arrays.

You are using the "String" to concatenate the time/date/message/data into one packet for writing to the SC card. This is not necessary. The SD card does not care if it gets a long concatenated line of characters or one character at a time. The physical writing to the SD card is buffered, and the logFile.print() command writes to a 512 byte buffer which is sent to the card when it is full or if logFile.close() or logFile.flush() is called.

If you insist on concatenating what is to be written, you should use sprintf() instead of the String function += concatenation.

@cattledog,

Point taken. I must admit I copied that part of the sketch pretty much verbatim from Adafruit's page on the datalogger quite a while ago. Now, I realize that String class can cause memory leaks which can kill a microcontroller, and don't use them anywhere else. Right now, I am working to be able to transmit longs and floats as well as ints, but the original logger is back on the process controller, and appears to be working fine ATM.

I don't have much experience with SD cards and char arrays, but I suppose now is as good a time as any to learn.

JohnDeere630:
I don't have much experience with SD cards and char arrays, but I suppose now is as good a time as any to learn.

Self learning approach is established on two strategies:

1. SSS (Small Start Strategy):
(a) Always begin with the Basic (elementary and simple) thing -- minimum hardware below which you can't go anymore (this is basic) and the associated software. Make it work -- check that you have got the expected result on the Output Device (the Serial Monitor = SM).

(b) Next add another basic piece of hardware (for example LCD) and include the associated software. Now check that the expected result has appeared on the LCD and SM.

(c) This way, you proceed to complete the project.

2. Playing with Working Things
(a) Collect a piece of hardware and the associated software that works. Play with it and try to understand the need of every component of the hardware and the every line of the program.

(b) Modify the program or write your version of program and check that the hardware is still behaving in the same manner as it was behaving under previous program.

Update: I followed cattledog's advice and cleared out all the String code and replaced it with simple char arrays. There are more simplifications to be made, for sure, but it works and I have other things to work on now.

I wound up using an unconventional way to transmit and receive long numbers, and that is using ltoa() to convert the long into a char array, transmit it, and then using a while() loop to read the bytes and using atol() to convert them back. I tried the method in the Arduino Cookbook with highByte() and lowByte(), but darned if I could make it work for longs. Ints were fine, but I could not get his makeLong() macro to work to save my soul. I couldn't get bit-shifting methods to work either. Either a dozen people on the 'net are wrong, or I'm missing something, because none of the examples I saw could be made to work correctly.

Just for giggles, here are the relevant bits:
Master:

void WriteToSDLong(byte code,long value)
{  
char buffer[20];

ltoa(value, buffer, 10);
 Wire.beginTransmission(LOGGER_ADDRESS);
 Wire.write(code);
 Wire.write(buffer,sizeof(buffer));
 Wire.endTransmission();
}

And on the receiving (slave) end:

void receiveEvent(int howMany)
  {  
    byte index = 0;
    char recBytes[20];   
    event = Wire.read();
    while(Wire.available() > 0 && index < sizeof(recBytes))
    {
      recBytes[index] = Wire.read();
      index++;
    }
    longValue = atol(recBytes);    
     eventFlag = true;
  }

I wound up using an unconventional way to transmit and receive long numbers, and that is using ltoa() to convert the long into a char array, transmit it, and then using a while() loop to read the bytes and using atol() to convert them back. I tried the method in the Arduino Cookbook with highByte() and lowByte(), but darned if I could make it work for longs. Ints were fine, but I could not get his makeLong() macro to work to save my soul. I couldn't get bit-shifting methods to work either. Either a dozen people on the 'net are wrong, or I'm missing something, because none of the examples I saw could be made to work correctly.

I know you want to move on, but I can't resist some follow up.

highByte() and lowByte() did not work for the long number, because a long is 4 bytes on the Arduino.

You clearly understand how all i2c transactions need to be broken down into bytes and reassembled. There as several ways to do this as you have found in your searching.

Using ascii representations of numerical data has the advantage of easy debugging but can lead to larger data packets and multiple conversions when starting and ending with numerical data.

Bit shifting and masking to isolate and combine bytes is one common method which keeps the data as numbers/bytes without converting to ascii. Unions are another method to get the data broken into bytes. There really is no "best" way, and you need to find something you understand and can become comfortable with.

The way I prefer to use is pointer type casting to byte*.

Every data type in C++ is stored in adjacent memory locations. Every memory location is one byte. You have seen how an integer(16 bits) is stored in two bytes. Longs and floats are stored in 4 bytes. Arrays contain multiples of the same type, and a data structure contains different types of variables.

Pointers point to a memory location or address. The byte pointer (byte*) tells the code to treat the data in the memory location pointed to as bytes. The '&' is the address of operator. The syntax for the casting a block of memory into bytes is

 (byte*) &data, sizeof(data)

This byte casting integrates well with the block writing and reading functions of the Wire library. It can be applied to all data types, arrays and structures.

Your function for the code and a long will look like this

void WriteToSDLong(byte code,long value)
{  
 Wire.beginTransmission(LOGGER_ADDRESS);
 Wire.write(code);
 Wire.write((byte*)&value,sizeof(value));
 Wire.endTransmission();
}
void receiveEvent(int howMany)
  {  
    event = Wire.read();
    Wire.readBytes((byte*)&longValue , sizeof(longValue));   
     eventFlag = true;
  }

cattledog:
I know you want to move on, but I can't resist some follow up.

highByte() and lowByte() did not work for the long number, because a long is 4 bytes on the Arduino.

@cattledog, Thank you! I will plug this in when I get a minute. All this is way over my head, but I have several good books and was in the process of trying to understand pointers. I'll get there someday, if I live long enough.

In the Arduino Cookbook, he explained how to split a long into 2 words, then using highByte() and lowByte() to end up with 4 bytes. I could dissasemble and reassemble ints with no problem, and the example in the book worked as long as I used the same number, but I got gibberish when I plugged in a different number. I expect there was something wrong with the makeLong() macro he provided, as I couldn't get hiWord and loWord to recombine properly. Part of the confusion was he had two versions of makeLong(): #define makeLong(hi,low) ((hi) << 16 & (low)) and #define makeLong(hi,low) (((long)hi) << 16 | (low))

The first wouldn't compile without errors, and the second seemed not to work properly. I have no idea what I was doing wrong. Unions was another method, but that was also way above my pay-grade. Thank you for your help, (and patience)!