How to configure Memory Registers for I2C Sensors

I've been working on an overhaul of the code base for our data loggers and in the process I standardized some of that spagetti code into a simple set of functions to access memory registers on I2C sensors. So posted those to the project blog to help others trying to figure out how to add a sensor to their own Arduino Project:

I'd love to hear from the more experienced eyes in this forum, if there are any serious mistakes there that need correcting.

cheers!

The main things are okay. There are a lot small little things to say about it.

When the sensor is not connected at all, you still return some kind of data without knowing that the data is wrong.
You need to avoid to get in a endless loop. For example when testing a status bit in a while loop.

This code is strange. Why do a while loop, when there is only one byte. An if statement would make more sense.

Wire.requestFrom(deviceAddress, 1);                     // request one byte
while(Wire.available())                                               // wait for data to be sent
{ resisterData = Wire.read(); } blog layout.   
return registerData;           // the returned byte from this function is the content from registerAddress
}

The delay(5) in "i2c_writeRegisterByte()" is not okay. What if someone needs data or a status bit repeatedly very fast ? Most sensors do not require that delay.

Please do not use "B00001111". That is something Arduino specific. In my opinion a mistake by Arduino. There is no need to fix something or to change something that is already perfectly okay. Use the normal 'c' language and use "0x0F" or "0b00001111".

Arduino has handy macros. For example word() to combine two bytes, or highByte() and lowByte(), bit(), bitRead().

Search that page for two semicolons ";;". Only one is needed.

Should be very helpful, thanks. Perhaps you can convince the moderators to post a link in the Programming section, in one of the stickies.

Speaking of memory:

it quickly overruns the 7±2 things this average human can hold in his head at one time

That outdated textbook value overestimates the average human's short term retention capacity by a factor of about two. It is actually 3-4 "items".

Read about Edward Awh's new work here. I heard him talk and give demonstrations once, and it was fascinating!

@ Koepel

Thank you for taking the time to read it over so thoroughly. It takes me ages to spot my own mistakes!

It was quite a while in my own learning path before I could tackle hardware alarms & interrupts, so I wanted to provide a quick method that people could use to check DRDY without them. I honestly did not know that the bus would return data junk data if a sensor was not connected, so I used a while loop for the status bit check because it always passes through at least once before the test. Now I will have to go hunting for a better way to do it.

The loop in the i2c_readRegisterByte was a hold over from a version that had a for-loop inside it to read multiple bytes into an array. Since it still works with only one byte,I thought it might be good to leave it there in case people run into array filling read loops later on... but you are right - it is weird and I should probably get rid of it.

I'm not sure how to respond to the "B00001111" comment, as this is one of the things that makes it really easy to see what is going on when masking. I think it's very helpful for beginners, and isn't that the same as telling users to use Arduino specific macros like highByte & lowByte?

@jremington
Thanks for the update on that research, as it makes me feel slightly less dumb. My hope is that my own struggles to understand this stuff actually helps me write material that reaches a wider audience.

I’m not sure how to respond to the “B00001111” comment, as this is one of the things that makes it really easy to see what is going on when masking.

Use of upper case B is not standard. Use 0b00001111 instead.

I have updated the post to standard 0b00001111 notation.

We are used to ‘c’ language, and the “0b00001111” is handled by the compiler and never fails.
The Arduino definition of “B00001111” is not as versatile.

There are two ways to check the I2C communication: the error return value of Wire.endTransmission() and the number of received bytes return value of Wire.requestFrom().
The Wire.available() can also be used instead of the return value of Wire.requestFrom().

When you write something to the sensor for the first time, it might be good to test the return value of Wire.endTransmission(). Your function “i2c_writeRegisterByte()” returns that error, but there is no example how to use it.
If the first test is okay (error is 0), then you may assume that the I2C bus is working and you don’t have to do extra tests.

In case the slave is an Arduino board, perhaps testing the return value of Wire.requestFrom() or Wire.available() could be added.

If a normal sensor does not return data, there must be something very wrong with the I2C bus. But since many users use long wires, long cables, wrong pullup resistors, wrong voltage levels, and so on, there is often something very wrong with the I2C bus. Perhaps adding extra checks can be usefull for such situations as well.

I think the best way is not to fill the variables with data, when there is no valid data.
There is more than one way to check if data was received. Here are a few examples:

byte n = Wire.requestFrom( 0x27, 4);
if( n == 4)
{
  Wire.readBytes( buffer, 4);
}


if( Wire.requestFrom( 0x27, 4) == 4)
  Wire.readBytes( buffer, 4);


Wire.requestFrom( 0x27, 4);
if( Wire.available() == 4)
{
  for( int i=0; i<4; i++)
  {
    buffer[i] = Wire.read();
  }
}

In the past there was an undocumented Wire.requestFrom() that did writing the register-address and reading the data (just like your “i2c_readRegisterByte”) all at once. I think it is removed since I can’t find it anymore.

@Kopel: "I think the best way is not to fill the variables with data, when there is no valid data."

I will use that while.available approach and update the post with a multi-byte read loop example.

Another approach I've used is to something like memset(rawTemps,0,sizeof(rawTemps)); to clear the data array after the data has been transferred to a storage buffer. That makes it easy to spot when sensor dropouts happen due to bus problems because the data all goes to zero.

Can I ask you another question? I've always wondered what the proper use was for the false modifier on endTransmission: Wire.endTransmission(false); What kind of sensor reading situations would that be helpful for?

I prefer to leave the variable as they are, but you can clear them. As long as can be determined if there is communication with the sensor or not at all.

The Wire.endTransmission(false) means no stop. It is called a repeated start.

Suppose the Master wants to keep control over the I2C bus between writing the register-address to the sensor and reading data with Wire.requestFrom(). Then a repeated start will not release the I2C bus.

Some sensors specify that a repeated start must be used.

Some sensors don't say anything in the datasheet about a repeated start, but still accept it.

Is it helpful ? I don't know. Perhaps to show off that you know fancy things like a repeated start.

Perhaps to show off that you know fancy things like a repeated start.

If the sensor requires a repeated start, then it is not "showing off" to use a repeated start.

Does a repeated start mean I can't use the zero returned from endTransmission to check if the bus coms to the sensor are good?

You can still use it. The Slave does not respond to a stop or a repeated start. The Slave responds to the I2C address with an acknowledge at the begin of the I2C transaction and to each databyte that is written to the sensor.

As far as I know, most sensors just eat any databyte, therefor the error code returned by Wire.endTransmission is mainly a test if the sensor did acknowledge the I2C address.

If a I2C device did not respond to a I2C address, the Wire library stops the I2C transaction right there. No data will be send.

If I have to guess then perhaps a few percentage of the sensors have in the datasheet that a repeated start must be used. Perhaps it was easier to implement that in the hardware of the sensor.

Do you have a logic analyzer ? They often can decode the I2C procotol, so you can see every start, stop, repeated start, ACK or NAK and the data.
Expensive, but awesome: www.saleae.com
Cheap, but useable: sigrok.org. Use sigrok with pulseview and find a supported logic analyzer on AliExpress for 5 dollars.

I've updated the post with that information about the repeated start. I haven't run into one of those sensors yet, but I'm sure I will.

Wrt logic analyzers, it makes me wonder if I couldn't bodge together some sort of simplified I2C bus problem diagnosis tool with an Arduino? For things like curve shapes, rather than absolute voltage levels, you can really crank up the ADC clock.

Eg: I used to use a scope to track power on my loggers, but now I track brief current events with one of the old UNO's I have lying around: Tutorial: Using the Arduino UNO Serial Plotter for Live Data Acquisition | Underwater Arduino Data Loggers

The SCL pin can be used by the Master for other things. The first high-to-low edge of SDA is the start condition. That is when the sensors begin to get nervous. So as long as SDA stays high, you can do with SCL whatever you want, perhaps even use it for other busses.

When SDA is set temporary low, that would be a start condition followed by a stop condition. That should be no problem either.

It is possible to make the digital pin of SCL output and low, and then input, and measure the SCL pin. The Arduino Uno has both analog input and digital input/output on the SDA and SCL pins.
The ATmega328P has seperate hardware in the chip for analog input, I2C and digital input and digital output. Those four things can be used at the same time.

Not many people know it, but the I2C has indeed its own hardware in the chip, it does not use the normal digital input and output circuit. That is because it has to meet the I2C specifications for pull-down current and it filters the input signal and so on.

However, other Arduino boards have other microcontrollers, it might not work for the Arduino Due or the ESP8266 or ESP32.

Therefor I suggest not to waste your time on a nerdy I2C tester, unless it is just for fun :grinning:

I agree it might not be worth my time compared to simply buying one, but curiosity might get the better of me anyway...

and I just came across quick and dirty I2C sniffer Arduino sketch at:

http://www.johngineer.com/blog/?p=455

@EKMallon

Thanks for the informative and elegant write up. However, I would like to make few observations:

1. Wire.beginTransmission(deviceAddressByte);

Why Byte? The device addresses for the commonly used TWI Bus devices are usually 7-bit. I always prefer to write deviceAddress or 0b1101000 for 0x68 and not 0x68 as it might be interpreted by a human operator as: 0110 1000 where LS-bit is the Read-Write/ bit.

2.

Wire.beginTransmission(deviceAddressByte);
Wire.requestFrom(deviceAddressByte, x);

Do we really need to give the roll-call command before the Wire.requestFrom()? The Wire.requestFrom() contains the built-in roll-call command.

Thanks.

…GM

@GolamMostafa

I wrote deviceAddressByte to try to get across that you can store the device addresses in byte variables, as I see so many code examples using ints. But I did not mention the final bit being added for read/write, so I have removed the 'Byte' endings in the text.

On the roll-call command that was a newbie mistake on my part. I'm not sure where I picked it up but it's been hanging around in my code for a very long time. I just never noticed that half of my scripts have the duplicate roll cal and the other half doesn't... The compiler has never given me an error from it, nor have any of my sensors complained so I just never realized it was redundant.

Thanks for helping me clear up those errors!

@EKMallon

As you have good interest on TWI Bus, this link may give some more idea on TWI Bus Operation.