Custom i2c sensor: when to update sensor values?

Hi there!

I really like the idea to create my own i2c devices communicating with a master. I found some tutorials but I do have a question with the timing when the chip does its thing in the loop and the onRequest/onRecieve events.

I have this very simple Attiny85 i2c device with a photoresistor that I would like to read its value via i2c which works so far.

Code (Master, library methods):

void SCi2cLDR::refresh()
{
  int i=0;
  byte buf[2];
  Wire.requestFrom(deviceAddress, 2);
  while (Wire.available()) 
  { 
    buf[i] = Wire.read();
    i++;
  }
  byte tmp[] = {buf[0],buf[1]};
  lightIntensity = bytesToUInt16(tmp);
}

int SCi2cLDR::bytesToUInt16(byte b[])
{
  int outVal = b[1] << 8;
  outVal = outVal + b[0];
  return outVal;
}

Code (Slave on ATTiny85):

#include <TinyWireS.h>

#define I2C_ADDRESS 0x40
#define PIN_LDR    A3

int REGISTER_SIZE     = 2;
int REGISTER_POINTER  = 0;

byte REGISTERS[] = 
{
  0x00,
  0x00,
};

void setup() 
{
  pinMode(PIN_LDR,INPUT);
  TinyWireS.begin(I2C_ADDRESS);
  TinyWireS.onRequest(requestEvent);
}

void loop() 
{
  TinyWireS_stop_check();
}

void requestEvent()
{  
  if (REGISTER_POINTER == 0)
  {
    readSensor();
  }
  TinyWireS.send(REGISTERS[REGISTER_POINTER]);
  REGISTER_POINTER++;
  if (REGISTER_POINTER >= REGISTER_SIZE)
  {
    REGISTER_POINTER = 0;
  }
}

void readSensor()
{
  int value = analogRead(PIN_LDR);
  REGISTERS[0] = value >> 8;
  REGISTERS[1] = value & 0xFF;
}

As you can see Im converting the analogRead integer value to 2 bytes and write it to the register array on the tiny exactly when the requestEvent() is called. I dont think this is good practice...

The code here works but I have one question:

Is it okay to do the reading/refresh of the LDR value in the requestEvent()? As far as I understand i2c is quite timing critical and I want to have clean code for future i2c devices where the update/refresh of its sensor values could take more time and confuse the i2c timing maybe.

I moved the readSensors() method in the loop so the ATTiny firmware looks like this:

void loop() 
{
  readSensor();
  TinyWireS_stop_check();
}

void requestEvent()
{  
  TinyWireS.send(REGISTERS[REGISTER_POINTER]);
  REGISTER_POINTER++;
  if (REGISTER_POINTER >= REGISTER_SIZE)
  {
    REGISTER_POINTER = 0;
  }
}

Which gives me gibberish and I suspect that this is due the timing of i2c and I think that the requestEvent() gets called maybe between readings and in one buffer is the old value and the new value in the other.

So in a nutshell:
When is the best time to read the sensors and write it in the register without interfering the i2c protocol?

Thanks in advance!

A few analogRead() is possible in the onRequest handler. If you want to do more, then you can do that in the loop() and update global variables and send those global variables when requested.

Those global variables should be "volatile" and updating the global variables should be done with the interrupts turned off (for a very short time) if they are larger than a single byte.

Most Arduino users do not turn off the interrupts, and in most cases that is no problem. However, the onRequest handler could run while a global variable is only half-written. If you want to be sure that the bytes belong to each other, then turn off the interrupts.

We prefer full sketches. There is a website for that: https://snippets-r-us.com/

The Master and the Slave should work with the same amount of bytes. If the Master requests 2 bytes, then the Slave should send 2 bytes in the code.

In your sketch, the bytes are copied many times. There is no reason for that.

Suppose you have 4 sensors and each sensor returns an integer:

Master code:

int myData[4];

...

int n = Wire.requestFrom(deviceAddress, sizeof(myData));
if (n==sizeof(myData))     // received the same amount as requested ? 
{
  Wire.readBytes( (byte *) myData);
}

Slave code (simple):

void requestEvent()
{
  int myData[4];
  for( int i=A0; i<=A3; i++)
  {
    myData[i] = analogRead(i);
  }
  Wire.write(myData, sizeof(myData));
}

Slave code (sophisticated):

volatile int myData[4];

...

void loop()
{
  int myTemporaryData[4];
  for (int = A0; i<=A3; i++)
  {
    myTemporaryData[i] = analogRead(i);
  }

  // update the global variable
  noInterrupts();
  memcpy( (char *) myData, (char *) myTemporaryData, sizeof(myData));
  interrupts();

  ...

}

void requestEvent()
{
  Wire.write(myData, sizeof(myData));
}

Often a 'struct' is used between the Master and Slave instead of an array of integers.
When the Master sends data to the Slave, then often a bool flag is used. The onReceive handler sets a flag, and in the loop() the flag is check to see if new data has arrived.

Well, I don't :woozy_face:
The Slave can not run a library that turns off interrupts for a while (Neopixel, DHT, OneWire, SoftwareSerial, and so on). Extra code is needed to check if a sensor is disconnected. Perhaps the Master wants to know how old the sensor data is, that requires a lot of code. The I2C bus is not supposed to go into a cable, it is not supposed to go somewhere else. The I2C bus is a weak bus.

I'm not familiar with the specific I2C things of a ATtiny.

1 Like

Nothing really to add to Koepel's reply. AnalogRead() in the ISR should not be a problem, but your device has nothing to do anyway and it's good practice anyway, so let it poll the sensor in the main loop and us a flag to signal a receive event, as shown by Koepel's code.

However, you might be interested in my I2Cwrapper project, which aims to make these kinds of project much easier, as it cares for all the overhead involved in developing I2C target (slave) devices. It should literally be a matter of minutes to implement your device with its help by adding a new module.

Edit: If it's only about reading an analog value, you should be able to simply use the included PinI2C module and be done.

So do I. :wink:

1 Like

Thank both of you so much for your explanation, code and pointing me in the right direction! Awesome! I will try what you said when Im back in the workshop! Thanks again!!

Hmmm sorry... I tried it but somehow I allways get a reading of 0.

Here is the adjusted ATTiny firmware:

#include <TinyWireS.h>

#define I2C_ADDRESS 0x40
#define PIN_LDR    A3

int REGISTER_SIZE     = 2;
int REGISTER_POINTER  = 0;

volatile byte REGISTERS[] = 
{
  0x00,
  0x00,
};

void setup() 
{
  pinMode(PIN_LDR,INPUT);
  TinyWireS.begin(I2C_ADDRESS);
  TinyWireS.onRequest(requestEvent);
}

void loop() 
{
  readSensor();
  TinyWireS_stop_check();
}

void requestEvent()
{  
  TinyWireS.send(REGISTERS[REGISTER_POINTER]);
  REGISTER_POINTER++;
  if (REGISTER_POINTER >= REGISTER_SIZE)
  {
    REGISTER_POINTER = 0;
  }
}

void readSensor()
{
  int value = analogRead(PIN_LDR);
  noInterrupts();
  byte hByte = value >> 8;
  byte lByte = value & 0xFF;
  memcpy((char*)REGISTERS[0],(char*)hByte,sizeof(byte));
  memcpy((char*)REGISTERS[1],(char*)lByte,sizeof(byte));
  interrupts();
}

What am Im doing wrong? :frowning:

It is easier to store a integer in a integer array.
It is easier to transmit the complete array at once.

Which TinyWireS library do you use ? Can you give a link to it ?

Have you looked into a 'struct' ? That is often used in this situation.

Im using this library here:

I think that the TinyWireS lib only allows 1 byte to be send at a time with the "send" method?!

Im going to research a little about the struct method. Im aware what structs are and how they are declared etc. but Im not sure how to utilise this in this situation exactly.

Can you find a better library ?
That one is 9 years old and it seems indeed that only 1 byte can be send. If that is all it can do, then it is useless.

Which ATtiny core do you use ? I think this one is the most common : https://github.com/SpenceKonde/ATTinyCore. It has the I2C Slave mode included.

You could make the I2C sensors with a Arduino Nano and when everything works, then try to put it in a ATtiny. I prefer a project that works rather than an exercise with a vague destination.

I think there are 3 strategies.

  1. The sensor samples the measured quantity continuously and when requested reports the most recent value. This has two drawbacks: high current consumption and you don't know exact time when the sample was taken. Also if the value does not change you don't know if it is because the value truly did not change or if it was not updated yet.
  2. You sample on request - some command triggers the conversion and next time the result is collected. You can combine this - when the results is read the sensor takes a new sample. This way you know the result was valid just after you made your previous read.
  3. I2C is not timing critical. Any Slave may hold the SCL line LOW as long as it wishes - such as while it is preparing the requested data. Of course this stalls the I2C bus until the result is ready. And if some problem happens in the sensor and it does not release the SCL line the I2C bus is stuck without any chance to recover.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.