Why is my I2C not read correctly?

I am attempting to send command packets between a control board and a peripheral. Unfortunately, when I attempt to read the packet on the peripheral device, the message is improperly truncated.

Hardware:

Command board - Custom 3.3V variant using ATMEGA2560
Peripheral board - Arduino Nano

I2C connection:

Since I am going from a 3.3V logic board to a 5V logic board, I have to adjust the signal. The command board includes through a PCA9517 level shifter with 10k pullups on each end. The longest wire in my test is ~20cm (8 in).

On the command board, I do not have access to the standard I2C ports, so I am using a SoftWire.h.

Minimal code:

The minimal code is attempting to send a 25-byte string, and the string is read out to the serial.

On the command board -

#include <SoftwareSerial.h>
#include "SoftEasyTransfer.h"

#define I2C_TIMEOUT 1000
#define I2C_PULLUP 1
#define SDA_PORT PORTC
#define SDA_PIN 3 // = 34 on mega
#define SCL_PORT PORTC
#define SCL_PIN 5 // = 32 on mega
#include <SoftWire.h>

struct CMD_DATA_STRUCTURE {
	char msg[25] = "";
};
CMD_DATA_STRUCTURE cmddata;
const bool DEBUG = true;

void xon_cmd(int unit) {
	/*
	 * Command the peripheral to turn on things
	 */
	for (size_t i = 0; i < 25; i++) {
		cmddata.msg[i] = 0x00;
	}
	// Add SOH, address, NACK, and EOT
	cmddata.msg[0] = 0x01; // SOH
	cmddata.msg[1] = 0x30 + unit; // ASCII "0" is master node
	cmddata.msg[2] = 0x11; // XON
	cmddata.msg[24] = 0x04; // EOT
	return;
}

void issue_cmd() {
	/*
	 * Issue the command we made to the peripheral
	 */
	if (DEBUG) {
		Serial.println("issuing cmd:");
		for (size_t i=0; i < 25; i++) {
			Serial.print(cmddata.msg[i], HEX);
			Serial.print(" ");
		}
		Serial.println("");
	}
	
	Wire.beginTransmission(8);
	for (size_t i=0; i < 25; i++) {
		if (DEBUG) {
			Serial.print(cmddata.msg[i], HEX);
		}
		Wire.write(cmddata.msg[i]);
		if (DEBUG) {
			Serial.print(" ");
		}
	}
	Wire.endTransmission(false);
	Serial.println("");
}


void setup() {
	// Power up 5v lines (required for custom board)
	pinMode(A1, OUTPUT);
	pinMode(A7, OUTPUT);
	digitalWrite(A1, HIGH);
	digitalWrite(A7, HIGH);
	
	// Empty command message
	for (size_t i = 0; i < 25; i++) {
		cmddata.msg[i] = 0xff;
	}
	
	if (DEBUG) {
		// Init serial comms for debugging
		Serial.begin(9600);
		Serial.print("Debugging: ");
		Serial.println(__FILE__);
	}
	Wire.begin();
}

void loop() {
	if (DEBUG) {
		Serial.println("XON attempt");
	}
	xon_cmd(2);
	issue_cmd();
	delay(1000);
}

On the peripheral board -

#include <Wire.h>
#include "SoftEasyTransfer.h"

const bool DEBUG = true;
struct CMD_DATA_STRUCTURE {
	char msg[25] = "";
};
CMD_DATA_STRUCTURE cmddata;

void receiveEvent(int howMany) {
	int cnt = 0;
	while (Wire.available()) {
		if (DEBUG) {
			Serial.print("Wire.available() = ");
			Serial.println(Wire.available());
		}
		// receive byte as a character into the command struct
		cmddata.msg[cnt] = Wire.read();

		cnt++;
	}

	for (size_t i = 0; i < 25; i++) {
		Serial.print(cmddata.msg[i], HEX);
		Serial.print(" ");
	}
	Serial.println("");

	return;
}

void setup() {
	if (DEBUG) {
		// Init serial comms for debugging
		Serial.begin(9600);
		Serial.print("Debugging: ");
		Serial.println(__FILE__);
	}
	Wire.begin(8);
	Wire.onReceive(receiveEvent);
}

void loop() {
}

Debugging Output:

Command board -

Debugging: /home/wes/sketchbook/test_sensorboard_rs485_command/test_sensorboard_rs485_command.ino
XON attempt
issuing cmd:
1 32 11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 
1 32 11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 

Peripheral board -

Debugging: /home/wes/sketchbook/test_sensorboard_rs485_net/test_sensorboard_rs485_net.ino
Wire.available() = 25
Wire.available() = 24
Wire.available() = 23
Wire.available() = 22
Wire.available() = 21
Wire.available() = 20
Wire.available() = 19
Wire.available() = 18
Wire.available() = 17
Wire.available() = 16
Wire.available() = 15
Wire.available() = 14
Wire.available() = 13
Wire.available() = 12
Wire.available() = 11
Wire.available() = 10
Wire.available() = 9
Wire.available() = 8
Wire.available() = 7
Wire.available() = 6
Wire.available() = 5
Wire.available() = 4
Wire.available() = 3
Wire.available() = 2
Wire.available() = 1
1 32 11 0 0 0 0 0 0 0 0 0 0 0 ⸮

If each byte of the expected 25 length Wire.available is being read, why does it truncate part way through? Note, if I do NOT use while (Wire.available()) for the read loop and instead force a length while (size_t i=0; i<25; i++) I get the same problem. HOWEVER, if I increase the loop length to something like i<30, I get more of the message (although it still truncates imrpoperly). What have I done wrong here?

Whar are the reasons against using a serial connection.
Serial is easier to use than I2C and an Atmega 2560 has 3 hardware-serial ports.

best regards Stefan

Try replacing the 10K resistors with 4.7K on the 5V side and 3.3K on the 3.3V side. If you have a scope I would expect rounding of the signals.

First, a clarification. The custom command board has 0 "free" ports, as it is a complicated monster. The easiest pins to access have the PCA9517 level shifters between the pin and the chip. Other accessible pins have things like AD converters blocking direct access to the chip ports. None of these available ports are the default Serial or I2C ports, so everything will require a "soft" library.

Why I2C vs Serial? The PCA9517 is designed for I2C (datasheet here). I'm not certain Serial would behave properly using this device.

Yes, there is rounding on the signals. I'll see if I have those values and give it a shot.

1 Like

The 4.7k on the 5V side were easy, but it turns out I don't have any 3.3k resistors. I do, however, have a few thousand 10k. After perhaps the ugliest soldering I have ever done (PROTIP: solder them to the board one at a time rather than stack first), I have a less-rounded peak.

Results:
Same truncation of message as in the OP. I could try tuning the resistors some more, but there should be enough plateau in that peak to register as high.

Note to self:
Buy a better scope. Not having digital output is bad enough, but not having triggered storage made getting those shots with my phone super painful.

3.3 is a SWAG, anything from about 2.5K to 5K should work.

I'm sorry, I'm afraid I don't know what SWAG means here.

Surely you are using it as slang to describe the quality of my soldering /s.

Edit: Silly. Wild. Ass. Guess. I found a reference to it. Disregard this comment.

We recommend not to use Serial functions in a interrupt routine.
Because the onReceive handler is called from a interrupt and the Serial functions use interrupts themself. Store the data in a global variable and process the data in the loop().

There is one special condition that is guaranteed to crash the sketch. That is when so many data is put into the Serial buffer (it is 64 bytes, inside the Serial library) that it becomes full. The Serial.print() will then wait until a new free spot comes available in that buffer. That buffer is emptied in a interrupt. If you do all of that from an interrupt, then it will not work. That is what you do.

Thanks, I didn't realize that would be a conflict. I modified my code to read the buffer every second in the loop, and I removed all of the Serial.print from the interrupt hook.

This appears to have fixed it.

We use a 'volatile' flag to let the loop() know that there is new data.

volatile byte data[25];
volatile bool newData;

void receiveEvent(int howMany) 
{
  if( howMany == 25)    // extra check must be done before using Wire.readBytes
  {
    // The Wire.readBytes is not the perfect function for this.
    // It can be used here if it is 100% certain that all the 25 bytes are available.
    Wire.readBytes( data, 25);
    newData = true;      // set flag to notify the loop() that there is new data.
  }
}

void loop()
{
  if( newData)    // is new data received ?
  {
    // process the data
    for( auto a:data)
    {
      Serial.println( a, HEX);
    }
    newData = false;     // release the data[] variable
  }
}

volatile is a good suggestion, but I didn't see that in any of the Wire.h examples. Notably, in an example from the library for a digital pot, the byte val = 0 is not declared volatile. Is there some documentation you could point me to that explains what is going on in the back-end? Specifically, if I was a newbie, what would I need to read to know that Serial functions inside interrupts are a bad idea?

One of those things you learn with experience, by reading tutorials on the web or in books, or on the forum. You may not call any function that uses interrupts, from inside an interrupt function, because interrupts are turned off temporarily.

Furthermore, in the main program, it is sometime necessary to turn off interrupts so that time-critical steps are not affected. There are functions for that purpose:

void loop() {
...
noInterrupts();  //turn off all interrupts
time_critical_action(); //do something
interrupts(); //back on
...

When the Arduino is a Master, it does not use interrupts. The Wire library uses internally interrupts, but that is hidden from the sketch.

When the Arduino is a Slave, then the onReceive and onRequest handlers are running in a interrupts. That is when the trouble begins :face_with_raised_eyebrow:

The 'volatile' keyword should be used when the loop() processes data that can be changed in a interrupt. With 'volatile' the compiler uses the data from the memory location of the variable, and without it, the compiler can optimize things by keeping the data in a register without looking at the variable.

There are many bad examples for using a Arduino as a Slave. Even the examples on the official Arduino reference pages.

Serial functions inside a interrupt is a bad idea. It is. Everyone can confirm that.
The addition by @jremington about disabling interrupts is of course correct. If you need to keep the 25 bytes together, then you might have to make a copy while the interrupts are turned off.

Basic tutorial by Nick Gammon: http://www.gammon.com.au/i2c

My wiki at Github: https://github.com/Koepel/How-to-use-the-Arduino-Wire-library/wiki.
If you read all the pages, then you are a I2C expert :wink:

Tutorial by Robin2: https://forum.arduino.cc/t/use-i2c-for-communication-between-arduinos/653958
It show how to transfer data over I2C. A 'struct' is the easiest way.

Tutorial by GolamMostafa: https://forum.arduino.cc/t/ch-6-i2c-bus-based-serial-data-communication/659019
I'm not sure if this is the Master-Slave example that I was looking for.

The noInterrupts() command is interesting. I've never used that one. The usage is reminiscent of the semaphore.h commands used for multithreading.

Thanks for the excellent list of resources. I glanced at Nick Gammon's writeup prior to starting this post, but I must have missed the bold, obvious notes which summarize what went wrong here:

You should not :

  • Do serial prints
  • Use "delay"
  • Do anything lengthy
  • Do anything that requires interrupts to be active

I am attempting to communicate between a 3.3V custom Mega2560 and a 5V Nano using Wire I2C. In a previous question, helpful folks encouraged me to tweak resistors and not make stupid Serial interrupt mistakes, but I am having new problems.

Why does my controller unit not read any bytes when it requests them?

Simplified Code:

Master/Controller

#define I2C_FASTMODE 1
#define I2C_TIMEOUT 1000
#define I2C_PULLUP 0
#define SDA_PORT PORTC
#define SDA_PIN 3 // = 34 on mega
#define SCL_PORT PORTC
#define SCL_PIN 5 // = 32 on mega
#include <SoftWire.h>

SoftWire soWire = SoftWire();
struct CMD_DATA_STRUCTURE {
	volatile byte msg[25];
};
CMD_DATA_STRUCTURE cmddata;

void xon_cmd(int unit) {
	for (size_t i = 0; i < 25; i++) {
		cmddata.msg[i] = 0x00;
	}
	// Add SOH, address, NACK, and EOT
	cmddata.msg[0] = 0x01; // SOH
	cmddata.msg[1] = 0x30 + unit; // ASCII "0" is master node
	cmddata.msg[2] = 0x11; // XON
	cmddata.msg[24] = 0x04; // EOT
	return;
}

void issue_cmd() {	
	soWire.beginTransmission(8);
	for (size_t i=0; i < 25; i++) {
		soWire.write(cmddata.msg[i]);
	}
	soWire.endTransmission(false);
	Serial.println("");
}

void setup() {
	if (DEBUG) {
		// Init serial comms for debugging
		Serial.begin(9600);
		Serial.print("Debugging: ");
		Serial.println(__FILE__);
		}
		Serial.println("");
	}
	
	soWire.begin();
}

void loop() {
	xon_cmd(2);
	issue_cmd();
	delay(100);
	soWire.requestFrom(8, 25);
	while(soWire.available()) {
		byte c = soWire.read();
		Serial.print(c, HEX);
	}
	delay(1000);
}

Slave/Peripheral

#include <Wire.h>
#include "SoftEasyTransfer.h"
int HAVE_MSG = 0;
int HAVE_REQ = 0;
struct CMD_DATA_STRUCTURE {
	volatile byte msg[25];
};
CMD_DATA_STRUCTURE cmddata;

void ack_cmd() {
	for (size_t i = 0; i < 25; i++) {
		cmddata.msg[i] = 0x00;
	}
	// Add SOH, address, ACK, and EOT
	cmddata.msg[0] = 0x01; // SOH
	cmddata.msg[1] = 0x30;
	cmddata.msg[2] = 0x06; // ACK
	cmddata.msg[24] = 0x04; // EOT
	return;
}

void receiveEvent() {
	int cnt = 0;
	while (Wire.available()) {
		cmddata.msg[cnt] = Wire.read();
		cnt++;
	}
	HAVE_REQ++;
	return;
}

void requestEvent() {
	ack_cmd();
	HAVE_MSG++;
	Wire.write((byte *) &cmddata, sizeof(cmddata));
	return;
}

void setup() {
	if (DEBUG) {
		// Init serial comms for debugging
		Serial.begin(9600);
		Serial.print("Debugging: ");
		Serial.println(__FILE__);
	}

	Wire.begin(8);
	Wire.onReceive(receiveEvent);
	Wire.onRequest(requestEvent); // register event
}

void loop() {
	Serial.print(HAVE_MSG);
	Serial.print(" ");
	Serial.print(HAVE_REQ);
	Serial.print(" ");
}

Expected Behavior

When I run this and monitor the command unit, I should see bytes received from the peripheral. When I monitor the peripheral unit, I should see the HAVE_MSG and HAVE_REQ counters increase roughly every second

Observed Behavior

The counters increase correctly on the peripheral, but nothing is reported by the command unit.

Notes

  • This code is attempting to communicate using the default Wire and SoftI2CMaster's SoftWire wrapper. The soft variant has no methods for peripheral units by design, but there should be nothing stopping us from kludging together a peripheral device using Wire...right?
  • I tried switching from I2C protocol to Serial/SoftSerial solution, but I observed similar results. The command unit cannot read info coming to it from the comm lines.
  • I don't have a good logic analyzer on hand, but I do have a spare Arduino. The signal is going faster than my little Uno can print conveniently, but I can tell that the signals are moving.

Do you use SoftWire by Steve Marple from the Library Manager ?

When connecting a 3.3V I2C bus to a 5V I2C bus, there is a voltage mismatch and you might expect troubles.
On top of that, you use a software I2C library, a sketch that is too complex, you create a full Serial TX buffer in the Slave (which is bad), there is no STOP condition in the function issue_cmd(), some variables should be 'volatile', using a 16-bit integer on a 8-bit microcontroller in a interrupt is not fail-safe, and so on.
I see at least six problem at first glance.

Can you start with two Arduino Uno boards and transfer a single byte ? If that works, then you can try SoftWire on the Master board, if that works, then you can try to transfer two bytes, and so on.

Why do you use a software I2C library ? Is there a good reason for that ?

Did you connect the grounds ?

Did you read my opinion that the I2C bus is not the best solution between Arduino boards and that a voltage mismatch on the I2C bus can not be ignored and Arduino in Slave mode requires knowledge about clean programming ? https://github.com/Koepel/How-to-use-the-Arduino-Wire-library/wiki/How-to-make-a-reliable-I2C-bus

[ADDED] I am very fond of the LHT00SU1 logic analyzer in combination with Sigrok/PulseView. It can decode Serial/UART and I2C data. https://www.banggood.com/Geekcreit-LHT00SU1-Virtual-Oscilloscope-Logic-Analyzer-I2C-SPI-CAN-Uart-p-988565.html

@whoneyc:

We strongly discourage starting new posts that continue a topic. In your previous post, you stated that you were using the required level shifter for the 5V to 3.3V I2C connection.

Here, that omission misled forum user Koepel and potentially creates a lot of confusion.

Please ask the moderator to merge these two threads. (Flag Icon).

Not entirely. As you can see I did not give a working sketch. I wrote that there is a truck load of problems.