Go Down

Topic: Wire library and blocking read/writes (Read 24830 times) previous topic - next topic

ardudillo

Hello, is there any way to preform non blocking read/writes over TWI? I spend a long time staring at twi.c, hoping to stick some:

if (a long time has passed) {
 return -1 or something equally absurd;
}

flavoured statements somewhere but drew a blank.

I 'm guessing the code is expecting to respond to some interrupts from somewhere. I 'm somewhat amazed that Google doesn't seem to find much in response to this (am I missing something?), it seems to me that having the Arduino hang if a cable to an I2C device gets unplugged can be a rather serious problem for a lot of applications.

retrolefty

As far as I can tell the Arduino TWI functions are already non-blocking. I say that because I built an application that used an external I2C EEPROM chip and if I unplugged the EEPROM the application continued to run, just returning all ones for the read requests.

Lefty

borref

Quote
As far as I can tell the Arduino TWI functions are already non-blocking.


The TWI library will wait/block for all read and write calls until the functions either complete with success or fail.

It is also my experience however that read/write calls will return (rather than hang forever) if the I2C device is removed from the circuit.This will then be after a timeout when no ACK's are received.

ardudillo

Are you guys sure? I am using the CMPS03 library which adds the following sketch to examples:

Quote

#include "Wire.h"
#include "CMPS03.h"

CMPS03 cmps03;

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

void loop()
{
  Serial.println(cmps03.read());
  delay(1000);
}



The code hangs if I cut power to the compass and resumes when I restore it.

The CMPS03 lib is very simple, here is the read() function.

Quote

unsigned int CMPS03::read ()
{
     Wire.beginTransmission(deviceId);
     Wire.send(2);
     Wire.endTransmission();
     delay(1);
     Wire.requestFrom(deviceId, (uint8_t) 2);
     unsigned int value = ((unsigned int) Wire.receive()) << 8;
     value = value + ((unsigned int) Wire.receive());
     return value;
}



I 'm fairly certain the guilty party is twi.c or perhaps Wire.cpp. I have managed to remove hanging by sticking "return" statements in various places such as twi_readFrom(), twi_writeTo() or TwoWire::endTransmission() for example, this removes the hanging by always returning 0 so it's useless but it points to these libs as being the hangers. I 'm using the 0018 IDE on a kubuntu 64-bit Linux platform.

borref

Quote
The code hangs if I cut power to the compass and resumes when I restore it.

Keeping the compass in the circuit may be an issue (e.g. SDA and/or SCL being pulled low).  Also I would add a call to Wire.available after every requestFrom and not proceed to read unless the data is actually available.

ardudillo

Some more info, I chopped up the example sketch of the CMPS03 lib to make the
following sketch which runs without need for the lib.

Quote

#include "Wire.h"


byte deviceId = 0x60;


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

void loop()
{

Serial.println("About to begin transmission");

Wire.beginTransmission(deviceId);

Serial.println("About to send a 2");
 Wire.send(2);

Serial.println("About to end transmission");
 Wire.endTransmission();

Serial.println("Transmission ended");
 delay(1);

Serial.println("About to request data");
 Wire.requestFrom(deviceId,
(uint8_t) 2);
 unsigned int value =
((unsigned int)
Wire.receive()) << 8;
 value = value + ((unsigned int)
Wire.receive());

Serial.print("Data received, value is: ");
 Serial.println(value);
 
 delay(1000);
}



If you have an I2C device you want to test it with, put the relevant deviceId.

Now, the code hangs if I cut power to the compass after having printed:

"About to end transmission"

so we know that something naughty is happening at:

Wire.endTransmission();


Looking inside the Wire.cpp we find:

Quote

uint8_t
TwoWire::endTransmission(void)
{
 // transmit buffer (blocking)
 int8_t ret = twi_writeTo(txAddress, txBuffer, txBufferLength, 1);
 // reset tx buffer iterator vars
 txBufferIndex = 0;
 txBufferLength = 0;
 // indicate that we are done transmitting
 transmitting = 0;
 return ret;
}



Now, it seems that twi_writeTo() is the guilty party so if I go to "twi.c"
("utility" folder of "Wire" library) and stick a "return 0" at the very start of
the function, my original code gets past the previous snag, only to get stuck
at:

"About to request data"

Same detective work uncovers that "Wire.requestFrom()" in "Wire.cpp" goes and
calls "twi_readFrom()" in "twi.c". Again going and sticking a "return 0" at the
start of "twi_readFrom()" unsnags my code and everything flows perfectly. Of
course all results are zeros but hey, the code does not hang.

The really weird bit is the placing of the "return" statements. When looking
at, say, "twi_readFrom()" I saw two while loops (line 129 and 153) where things
could be getting stuck and thought it would be easy to modify them to exit
after a timeout had elapsed. No such luck. The first while loop and following
code is as follows:

Quote

 // wait until twi is ready, become master receiver
 while(TWI_READY != twi_state){
   continue;
 }
 twi_state = TWI_MRX;
 // reset error state (0xFF.. no error occured)
 twi_error = 0xFF;



sticking the return immediately after the loop closing bracket still makes the
code flow without hanging so there is nothing bad happening in there.

However, placing the "return 0" after the "twi_state = TWI_MRX;"

breaks the code completely, so completely that it hangs after "About to request
data" irrespective of whether the compass is on or off.



So, any ideas what could be happening here? BTW, a couple more clues. BenF
pointed out something about the lines being pulled low and adding a
"Wire.available()" before trying to read. Now, the (first) hanging occurs after
"Wire.endTransmission()" so that is not really an option. However, notice that
I 'm using an Arduino Mega and not a Duemilanove. Now, from what I remember in
the Duemilanove you have to use a couple of pull up resistors to pull the SDA
and SCL lines to +5V. In the Mega pulling the lines up results in nothing being
read so I plug
the lines directly to the relevant pins (20 and 21). Could that be significant
i.e., the internal wiring of the Mega not be the same as that of the Duemilanove
where the pulling up was done externally? After all, if the lines were pulled up
internally, nothing would change if I also pulled them up externally, right?

One more clue, when I cut power to the compass (without having altered the libs)
and it hangs after printing:

"About to end transmission"

when I restore power, the value read is zero, as opposed to the expected value.
This leads me to believe that something somewhere really does time out but then
the result does not trickle up to my sketch.

wayoda

Hi,
I must admit I haven't read all of your posting, but I have my doubts that you really found a bug in the Wire-lib, since this code hasn't been updated for years and is in use by thousands of projects.

I would look for problems on the slave-side rather than on the Arduino side.

The fastes easiest way to find out what is happening would be to hook up a logic-analyzer to the signals and see if the communication follows the (well-defined) I²C-protocol.
But since this nothing to be found in everybodys household ....

The only situation where the (arduino-) master must wait endlessly for a slave device, is when the slave pull the SCL-Line low during clock-stretching. http://en.wikipedia.org/wiki/I2c#Clock_stretching_uses_SCL

If the compass module never releases that line, this is a software/hardware-bug on the slave side.

That a powered down compass module also seems to stall the complete communication also sounds like a hardware bug on the compass side. I don't know if the I²C-Protocol says anything about this situation, but I never had a slave device the acted that way.

And the I²C-Bus supports up to 127 devices. I does not make any sense that a failure in one slave device should bring the whole bus down

Eberhard

ardudillo

Guys, thanks for taking the time to help, I 'm as mystified as you are about what's happening. Let me tell you my newest discoveries and you can let me know what you think.

I let go of the Mega for the time being and went back to a Duemilanove. I wired up the compass as shown in this tutorial, uploaded the chopped up sketch I have quoted above, fired up the serial monitor and ... nothing.

I was pretty amazed since I have been using this exact same circuit with this sensor before so I double and triple checked my circuit and all seemed in order. Then I decided to pull out the pull up resistors and everything started working. The only thing that has changed as far as I can tell from my previous circuits is that I switched to the 0018 IDE. Is it possible that internal pull ups have been enabled for the Duemilanove in the new IDE?

Anyway, my Duemilanove is happily chatting to the compass using the above sketch by plugging its SDA and SCL directly into the board and its power lines directly into the board's power. Now:

a) removing either SDA or SCL does NOT hang the sketch but the returned values are all zero.
b) removing the GND line does not hang the sketch but the returned values are all zero.
c) removing the +5V line hangs the sketch.
d) removing the GND and then the +5V line hangs the sketch.
e) further removing either the SDA or SCL line restarts the sketch but all returns are zero.

I then tried exactly the same on the Mega and I got the same results, although I once got the board to hang at step e, requiring a reset.


With the compass experimentation out of the way, I moved to a SRF08 ultrasonic ranger which also uses I2C. The Robot-electronics site has a connection diagram using the external pull-ups here. Again, I didn't use the external pull-ups (or the LCD they show) but I stole their example sketch and chopped it up, removing the LCD output and adding a timeout in their getRange() function to avoid getting trapped in an infinite loop in the sketch itself (see comments marked EDIT!!!). The modified sketch is as follows:

Quote
#include <Wire.h>

#define srfAddress 0x70                           // Address of the SRF08
#define cmdByte 0x00                              // Command byte
#define rangeByte 0x02                            // Byte for start of ranging data

byte highByte = 0x00;                             // Stores high byte from ranging
byte lowByte = 0x00;                              // Stored low byte from ranging

void setup(){
  
  Serial.begin(9600);                             // Begins serial port for LCD_03
  Wire.begin();                              
  delay(100);                                     // Waits to make sure everything is powered up before sending or receiving data
    
}

void loop(){
  
  int rangeData = getRange();                     // Calls a function to get range
  Serial.print("Range = ");
  Serial.println(rangeData, DEC);                 // Print rangeData
  
 delay(100);                                      // Wait before looping
}


int getRange(){                                   // This function gets a ranging from the SRF08
  
  int timeout = 100;                              // EDIT!!! introduce a 1/10th of a second timeout
  long lastTimeCheck;                             // EDIT!!! keep track of time in this var
  
  int range = 0;
  
  Wire.beginTransmission(srfAddress);             // Start communticating with SRF08
  Wire.send(cmdByte);                             // Send Command Byte
  Wire.send(0x51);                                // Send 0x51 to start a ranging
  Wire.endTransmission();
  
  delay(100);                                     // Wait for ranging to be complete
  
  Wire.beginTransmission(srfAddress);             // start communicating with SRFmodule
  Wire.send(rangeByte);                           // Call the register for start of ranging data
  Wire.endTransmission();
  
  Wire.requestFrom(srfAddress, 2);                // Request 2 bytes from SRF module
  lastTimeCheck = millis();                       // EDIT!!! log the current time
  while(Wire.available() < 2) {                   // Wait for data to arrive
    if (millis() - lastTimeCheck > timeout) {     // EDIT!!! make sure we don't stay in here longer than the timeout
      return -1;                                  // EDIT!!! return an illegal value
    }
  }
  highByte = Wire.receive();                      // Get high byte
  lowByte = Wire.receive();                       // Get low byte

  range = (highByte << 8) + lowByte;              // Put them together
  
  return(range);                                  // Returns Range
}



Poking around in a slightly more systematic way I found that the sketch hangs under these conditions:

a) BOTH SDA and SCL lines connected, +5V line disconnected (GND irrelevant, can be either connected or disconnected).

b) EITHER SDA or SCL line connected, GND connected.

In other words, if I plug either SDa or SCL and then plug either the other or GND, the board hangs.

Moving to the Mega, things get weirder. No matter what I pull out, I can't make it hang.


So, summing up, I have tried two different simple sketches, testing a CMPS03 magnetic compass and a SRF08 ultrasonic ranger on a Duemilanove and a Mega. Under certain conditions described above, I got the Duemilanove to hang using both the CMPS03 and the SRF08. I only got the Mega to hang with the CMPS03 but not with the SRF08.

I would appreciate it if anyone with an I2C device and a bit of time to spare could try to replicate this. Thanks.

borref

I must admit I find this issue somewhat constructed. Perhaps you should explain what you actually need a fix for? E.g. are you planning to power down part of your circuit?

If it is the latter - my reasoning is as follows:

- If you power down one (of several I2C devices), you must verify that SDA and SCL for the powered down IC are both high impedance - otherwise I2C communication with the remaining powered device(s) will be unreliable.
- It was suggested that the IC is buggy if it is not high impedance when not powered, but datasheets in general do not specify electrical characteristics for non or partially powered devices. As such you can't really blame the manufacturer, nor will it solve your issue.
- If you power down all I2C devices, it is no longer an issue since then you will not need I2C communication at all.


ardudillo

No no, I 'm just curious. I came across the hanging issue while mucking about and thought it was to do with the library which is why I started the thread. Then, after the replies, I started playing with the different boards and got the results I describe above. I don't have a problem as such, just exploring how the Wire lib works and how robust it is. I 'm still wondering why my recent Duemilanove sketch does not require external pull ups like my older ones did (and why it doesn't work with external pull ups when they shouldn't make a difference), why the ranger hangs the Duemilanove but not the Mega etc. Sure, I 'm a bit uneasy about a bus which can hang the board (a flying robot for example would be really unhappy if part of a sensor cable came undone and the whole thing froze) but I have no problem at the moment. Thanks for the help, if it wasn't for you guys I 'd still be trying to figure out the library code.

borref

If you remove power or ground to an IC and at the same still allow current to flow through other gates (such as SDA/SCL in this case) it is a fair chance that you damage the IC and possibly also your microcontroller. As such it is not a good way to test a library - unless you have a justifiable reason for doing so.

I've also experienced hangs during prototyping (suspecting the library to be at fault), but it has always been a wiring issue. You could avoid this by adding timeouts to the low level twi routines, but it doesn't really solve the problem. An elaborate fix for this would have to come at the expense of size/speed and still you may not be much better off as your device is malfunctioning. When building a truly critical component, higher level approaches such as using a watch-dog timer and redundancy are likely to be more effective as they will cover a broader range of potential faults.

As for your observations, the Duemillanove and the Mega should work the same way. The TWI library will enable gate pullups for SDA/SCL on either board. It is still recomended however to add external pullup resistors in the 4k7 to 10k range (look at the SDA/SCL lines in a scope and you will see why). If you purchase I2C devices on breakout boards however, they may already have pullups enabled. If you add to this, the pullup may become too strong. You should only have one set of pullup resistors for the bus. Other issues such as length of wiring, cross-talk, power supply ripple, electrical noise etc. may also explain why two assumed equal circuits behave differently.

ardudillo

OK, much clearer now, thanks a lot for your help.

lefstin

Is there any way to avoid enabling the pullups while using the Wire library?

I want to interface the Arduino to a 3.3v device using I2C.  From what I have read, this will work if I use pullups to 3.3, but I shouldn't expose the 3.3v device to 5v.  If the internal pullups are enabled, is that dangerous for the 3.3v device?

borref

Quote
Is there any way to avoid enabling the pullups while using the Wire library?

When you call Wire.begin, the library will enable internal pullups  on SDA and SCL. You could disable the pullups again (using digitalWrite or direct port IO), but this would not prevent the intial exposure to 5V.

One possibility is to modify the TWI source (look in library/twi/utility) for the twi.c file and the twi_init function.

Code: [Select]
 #if defined(__AVR_ATmega168__) || defined(__AVR_ATmega8__) || defined(__AVR_ATmega328P__)
   // activate internal pull-ups for twi
   // as per note from atmega8 manual pg167
   sbi(PORTC, 4);
   sbi(PORTC, 5);
 #else
   // activate internal pull-ups for twi
   // as per note from atmega128 manual pg204
   sbi(PORTD, 0);
   sbi(PORTD, 1);
 #endif


You could then comment out the relevant section above (Duemillanove or Mega) to prevent pullups from being enabled.

Another alternative is to add level shifting to the I2C bus so you can communicate with a mix of 3V3/5V devices without issues.

lefstin

Yes, I was thinking about removing those lines in the Wire library.

Is there any mechanism for doing that other than changing the Wire library file - like some of that mysterious preprocessor or #include magic?  Ideally I'd like to implement that hack only for this sketch, and have the sketch be portable.  Plus not have to re-hack it every time I install an Arduino software update.

Go Up