5

Is there a general I2C command to see if a device is still present on the bus after it is initialized once? For example an OLED display. The reason I ask this is to avoid the main program will freeze (when a device is disconnected) because of infinite loops present in the library code, in for example, the Wire library.

At startup of the MCU I want to check if a device is available or not, and initialize it when it is available. I do this with this function and works fine .....

bool MyClass::isPnpDeviceAvailable( uint8_t iAddress, bool bIsInitOnce = false )
{
     // Try to start connection
    Wire.beginTransmission( iAddress );

     // End connection without STOP command if already is initialized
    return ( Wire.endTransmission( !bIsInitOnce ) == 0x00 ); // No Error?, return true
}

.... however, when I want to check if a device is still there, before I perform an update, when I do this:

// 1.
if( isPnpDeviceAvailable( 0x3C, true )) 
 { /* Cause program hang */ }
// 2.
if( isPnpDeviceAvailable( 0x3C )) 
 { /* Cause display to turn off  */ }

Is there a general command available, to say/send just a "Hello, are you there" and wait for a reply without sending START and STOP commands and without interrupting device/bus status?


Here is the proto-type device I made with attached (optional PNP I2C) display.

Device with PNP I2C display

Codebeat
  • 6,501
  • 6
  • 57
  • 99
  • 3
    The image is nice, but I doubt it contributes something substantial to your question. – user0042 Sep 03 '17 at 23:49
  • Just there to give you some idea, is it bad to do this? – Codebeat Sep 03 '17 at 23:51
  • No, just what I said. I believe there must be a way, since linux kernels [seem to support it](https://www.systutorials.com/docs/linux/man/8-i2cdetect/). – user0042 Sep 03 '17 at 23:55
  • Okay, thanks, nice to know! – Codebeat Sep 03 '17 at 23:56
  • 2
    What would happen if someone unplugged the display at the exact moment just after you checked if it was plugged in? – user253751 Sep 04 '17 at 00:00
  • @Erwinus [This](https://playground.arduino.cc/Main/I2cScanner) looks like the equivalent Arduino functionality. – user0042 Sep 04 '17 at 00:02
  • 1
    user0042: Yep, my code is based on that. @immibis: Yeah, that is a bad case scenario, however, it will not happen when using the device because updates will only performed when there are changes to report for example changing the mode. It is very unlikely that you change the mode and unplug the display at the same time. I can also change the library with timeouts but want to avoid this (if possible) – Codebeat Sep 04 '17 at 00:21
  • @Erwinus What's the point of avoiding a timeout? Unless you have some general interrupt to detect that situation I cannot see a better way. – user0042 Sep 04 '17 at 00:40
  • @user0042, well, if there is a nicer or more elegant better way to do it without changing the library, i will use that first. – Codebeat Sep 04 '17 at 00:46

2 Answers2

3

Allright, it takes a longer journey to figure it out and test it. Also made a video of it, see link at the bottom of this answer. All credits go to @user0042 who points me into the right direction. The default Wire library is actually of no use when it comes to stability, reliability, so it is required to 'replace' it with this:


The I2C Master library - http://dsscircuits.com/articles/arduino-i2c-master-library


There are more benefits to use this library, it is smaller in compile size, read the article above for more information.

I changed my software, the 'key' to detect a device on the bus could be simplified to this:

bool TEnjoyPad::isPnpDeviceAvailable( uint8_t iAddress )
{
  return ( I2c.write( (int)iAddress, (int)0x00 ) == 0x00 ); 
}

Notice: The (int) typecasting is required to avoid a compiler warning but it does work fine without.

I send a **0x00 command** which do nothing, however, the device seems to answer. The function I made returns true when plugged in and false if not.

I doesn't test it with other i2c devices yet, however, will try later and update this question. For now it seems to working fine. NOTICE: SEE UPDATE BELOW:


The PNP approach

Step #1

In the first version I didn't use any resistors (lazyness) but it is a good idea to stabilize the readouts of the bus. Add two resistors (4.7K) on the +5V output to the data lines. It is very important to do this to avoid false detections and to avoid your Arduino can still freeze because of that.

Step #2

You need to keep track on changes/device state of each I2C device. I use three states:

  • Connected
  • Reconnected (aka was connected)
  • Disconnected (or never connected before)

Step #3

If you use a class to 'speak' to a device, it must be dynamically created when device comes available. In my example it is something like this:

TOLEDdisplay* display; // declaration
......
......
display = new TOLEDdisplay( SDA, SCL ); // To create it
display->begin(); // it's a pointer to an object so you need to use -> instead of . (simple explanation ;-) )
......
// etc 

Step #4

Before each update, you need to check the availability and the state of initialization (the three states mentioned in step #3). This is very important to avoid unnecessary delays/code execution (stress).

  • If it wasn't connected before, you need the create the class
  • If it was connected before (a reconnect), you have to reinitialize (the class and thus the device)

Step #5

You need to check for changes, in a loop or an interrupt. Better do it in a loop instead of an interrupt.

Step #6

Perform updates when changes are detected. Use a little delay of about 200ms seconds before the real update.


Some example code

You cannot use this code, however, it can give you some idea how to design your code. I use many macro's to simplify my actual code, so it is easier to read:

void TEnjoyPad::showAbout() // only showed at initialization
{
  __tepClearDisplay();

  __tepSetDisplayText( "ENJOYPAD v1.0"     , TOD_TEXT_ALIGN_CENTER, TEP_DISPLAY_LINE1 );
  __tepSetDisplayText( "(c) 2017 codebeat" , TOD_TEXT_ALIGN_CENTER, TEP_DISPLAY_LINE2 );


  __tepRefreshDisplay();
  setDelay( 2000 );
  updateDisplay();

}

void TEnjoyPad::updateDisplay()
{
 if( !__tepDisplayIsInit() )
  { return; }


 __tepDrawDisplayBitmap( TEP_DISPLAY,           // bitmap
                         0, TEP_DISPLAY_LINE0,  // x,y
                         TEP_DISPLAY_WIDTH,
                         TEP_DISPLAY_HEIGHT
                        );

  uint8_t i = TEP_MIN_MODE - 1;

  __tepDrawDisplayClearRect( 0, 10, 128, 35 );

  while( ++i <= TEP_MAX_MODE )
  {
    if ( emuMode != i )
    {
      // erase area, delete what's NOT selected
      __tepDrawDisplayClearRect( TEP_DISPLAY_MODE_ICON_X + ((i - 1) * (TEP_DISPLAY_MODE_ICON_WIDTH + TEP_DISPLAY_MODE_ICON_SPACING)),
                                 TEP_DISPLAY_MODE_ICON_Y,
                                 TEP_DISPLAY_MODE_ICON_WIDTH,
                                 TEP_DISPLAY_MODE_ICON_HEIGHT
                               );
    }
    else {
            __tepSetDisplayText( TEP_MODE_GET_NAME(i), TOD_TEXT_ALIGN_CENTER, TEP_DISPLAY_LINE1 );
         }
  }

  __tepRefreshDisplay();
}

void TEnjoyPad::beginDisplay( bool bIsFound = false )
{
  static bool bWasConnected = false;

  bIsFound = bIsFound?true:isPnpDeviceAvailable( TEP_PNP_ADDR_DISPLAY );

  if( bIsFound )
  {
    if( !bWasConnected  )
    {
      if( pnpStates[ TEP_PNP_IDX_DISPLAY ] )
      {
        // Reset
        setDelay( 200 );
        // Reset display
        bIsFound = isPnpDeviceAvailable( TEP_PNP_ADDR_DISPLAY );
        if( bIsFound )
        {
          __tepDisplay->begin(); 
          updateDisplay();
        } 
      }
     else {
            // (re-)connected" );
            __tepCreateDisplay(); // This macro checks also if class is created
            __tepInitDisplay();
            showAbout();

             // Set class is created
            pnpStates[ TEP_PNP_IDX_DISPLAY ] = TEP_PNP_ADDR_DISPLAY;
          }  
    }

    bWasConnected = bIsFound;
  }
  else { 
            // Disconnected           
            bWasConnected = false; 
       }  
} 

 // In a loop I call this function:
uint8_t TEnjoyPad::i2CPnpScan()
{
  uint8_t iAddress = 0x7F; // 127
  bool    bFound   = false;
  uint8_t iFound   = 0;

  //Serial.println( "Scanning PNP devices...." );
  while ( --iAddress )
  {
    //Serial.print( "Scanning address: 0x" );
    //Serial.println( iAddress, HEX );

    if( iAddress == TEP_PNP_ADDR_DISPLAY )
     { beginDisplay( bFound = isPnpDeviceAvailable( iAddress ) ); 
       iFound+=bFound;
     }
  }

  return iFound;
}

Demo video

I also create a demo video, proof of concept, to show you this method is working fine. You can watch the video on YouTube: https://www.youtube.com/watch?v=ODWqPQJk8Xo


Thank you all for the help and hopefully this info can help others too.


UPDATE:

My method seems to work fine with several I2C devices. I wrote this renewed I2CScanner:


I2CScanner code that you can use:

/*
 ----------------------------------------
 i2c_scanner - I2C Master Library Version

 Version 1 (Wire library version)
    This program (or code that looks like it)
    can be found in many places.
    For example on the Arduino.cc forum.
    The original author is not know.

 Version 2, Juni 2012, Using Arduino 1.0.1
     Adapted to be as simple as possible by Arduino.cc user Krodal

 Version 3, Feb 26  2013
    V3 by louarnold

 Version 4, March 3, 2013, Using Arduino 1.0.3
    by Arduino.cc user Krodal.
    Changes by louarnold removed.
    Scanning addresses changed from 0...127 to 1...119,
    according to the i2c scanner by Nick Gammon
    http:www.gammon.com.au/forum/?id=10896

 Version 5, March 28, 2013
    As version 4, but address scans now to 127.
    A sensor seems to use address 120.

 Version 6, November 27, 2015.
    Added waiting for the Leonardo serial communication.

 Version 7, September 11, 2017 (I2C Master Library version)
    - By codebeat
    - Changed/Optimize code and variable names
    - Add configuration defines
    - Add fallback define to standard Wire library
    - Split functionality into functions so it is easier to integrate 
    - Table like output


 This sketch tests the standard 7-bit addresses between
 range 1 to 126 (0x01 to 0x7E)
 Devices with higher addresses cannot be seen.

 ---------------------
 WHY THIS NEW VERSION?

 The Wire library is not that great when it comes to stability, 
 reliability, it can cause the hardware to freeze because of
 infinite loops inside the library when connection is lost or
 the connection is unstable for some reason. Because of that
 the Wire library is also not suitable for plug and play 
 functionality, unplugging an I2C device will immediately
 lock the hardware (if you want to talk to it) and you 
 need to reset the hardware. I will not recover on itselfs.  

 Another reason is the way to check if a device is plugged-in
 or not. The methods of the Wire library doesn't allow to 
 do this because it resets/stop the I2C device when it is
 already started/available.



 Benefits of the I2C Master Library:
 - More flexible;
 - Faster;
 - Smaller compile size;
 - Idiot proof;
 - Self recovering (no hardware freeze);    
 - Able to check for availability of devices without 
   interrupt bus status and/or device (see the 
   example function isDeviceAvailable() how to achieve 
   this)
   .

 More info at:
 http://dsscircuits.com/articles/arduino-i2c-master-library
 You can also download the library there.

 PRECAUTIONS:
 It is a good idea to stabilize the readouts of the bus. 
 Add two resistors (4.7K) on the +5V output to the data lines. 
 Only one pair is required, don't use more or different resistors.
 It is very important to do this to avoid false detections and to 
 avoid your Arduino can still freeze because of that. 

 NOTICE:
 When selecting the default Wire library, this scanner will probably 
 not show the side effects I am talking about because the code 
 don't talk to the device and the connection to a device is extremely 
 short period of time.
*/

// *** Uncomment this if you want to use the default Wire library.
//#define I2C_LIB_WIRE

 // Some settings you can change if you want but be careful
#define I2C_MIN_ADDRESS     0x01
#define I2C_MAX_ADDRESS     0x7F
#define I2C_UPDATE_TIMEOUT  3000
#define I2C_I2CLIB_TIMEOUT  1000
#define I2C_I2CLIB_FASTBUS  true


 // Errorcodes that are normal errors when I2C device does
 // not exists.
#define I2C_I2CLIB_ERROR_NOT_AVAIL  32
#define I2C_WIRELIB_ERROR_NOT_AVAIL  2


// -------------------------------------------------------------


#ifdef I2C_LIB_WIRE
 #define I2C_ERROR_NOT_AVAIL I2C_WIRELIB_ERROR_NOT_AVAIL
  // Compile size with Wire library: 6014 bytes
 #include <Wire.h>
 #pragma message "Compiled with Wire library"
#else 
 #define I2C_ERROR_NOT_AVAIL I2C_I2CLIB_ERROR_NOT_AVAIL
  // Compile size with I2C Master library: 5098 bytes
 #include <I2C.h>
 #define Wire I2c
 #pragma message "Compiled with I2C Master library"
#endif


// -------------------------------------------------------------


int iLastError = 0;

bool isDeviceAvailable( uint8_t iAddress )
{
 #ifdef I2C_LIB_WIRE
  // Wire:
  // The i2c_scanner uses the return value of the Write.endTransmisstion 
  // to see if a device did acknowledge to the address.
  Wire.beginTransmission( iAddress );
  iLastError = Wire.endTransmission();  
 #else
   // I2C Master Library:
   // Just send/write a meaningless 0x00 command to the address 
   // to figure out the device is there and the device answers.
  iLastError = Wire.write( (int)iAddress, (int)0x00 ); 
  // Notice: The (int) typecasting is required to avoid compiler  
  //         function candidate notice. 
 #endif  

 return ( iLastError == 0x00 ); 
}

byte findI2Cdevices( bool bVerbose = true ) 
{
  byte nDevices = 0;

  if( bVerbose )
   { Serial.println("Scanning..."); }

  for(byte iAddress = I2C_MIN_ADDRESS; iAddress < I2C_MAX_ADDRESS; iAddress++ ) 
  {
    if( bVerbose )
    {
     Serial.print("Address 0x");
     if( iAddress < 16 ) 
      { Serial.print("0"); }

     Serial.print( iAddress, HEX );
     Serial.print(": ");
    }

    if( isDeviceAvailable( iAddress ) )
    {
      if( bVerbose )
       { Serial.println("FOUND  !"); }
      nDevices++;
    }
    else { 
            if( bVerbose )
            {
              Serial.print( "<NO DEVICE FOUND" ); 
              if( iLastError != I2C_ERROR_NOT_AVAIL )
              {
                Serial.print( " - ERRCODE: " );
                Serial.print( iLastError );
              }
              Serial.println( ">" ); 
            }
         }    
  }

  if( bVerbose )
  {
    if( nDevices > 0 )
    { 
      Serial.print( nDevices );
      Serial.println( " device(s) found\n" ); 
    }
    else { Serial.println( "No I2C devices found\n"); }

    Serial.print( "Press CTRL+A, CRTL+C to copy data.\n" );
  }

  return nDevices;
}

void setupI2C()
{
 Wire.begin();

 #ifndef I2C_LIB_WIRE  
  // This is important, don't set too low, never set it zero.
  Wire.timeOut( I2C_I2CLIB_TIMEOUT ); 

  #ifdef I2C_I2CLIB_FASTBUS
   if( I2C_I2CLIB_FASTBUS )
    { Wire.setSpeed(1); }
  #endif
 #endif
}

void setupSerial()
{
 Serial.begin(9600);
 while (!Serial); // Leonardo: wait for serial monitor
 Serial.println("\nI2C Scanner");
}

// -------------------------------------------------------------

void setup()
{
  setupI2C();
  setupSerial();
}

void loop()
{
    // Skip the Arduino slow down housekeeping after the loop() 
    // function, we stay here forever ;-) 
   while(1)
   {
     findI2Cdevices();
     delay( I2C_UPDATE_TIMEOUT ); // wait n seconds for next scan
   }
}
Codebeat
  • 6,501
  • 6
  • 57
  • 99
2

@immibis made a very good point.

The probably better solution is to harness your update command with a certain timeout, that breaks blocking at that point.

Here seems to be some more information how to realize that properly.

Here's another Q&A from the SE Arduino site, matching the topic.

user0042
  • 7,917
  • 3
  • 24
  • 39
  • Thanks for the answer, take a look at it. It is not the most elegant way to do it because you need to change the library, but okay, will keep it in mind. The point of immibis: Yeah, that is a bad case scenario, however, it will not happen when using the device because updates will only performed when there are changes to report for example changing the mode. It is very unlikely that you change the mode and unplug the display at the same time. I can also change the library with timeouts but want to avoid this (if possible) – Codebeat Sep 04 '17 at 00:51
  • @Erwinus I added another possibly useful link. – user0042 Sep 04 '17 at 00:55
  • @Erwinus _"Yeah, that is a bad case scenario ..."_ When working with embedded SW that can brick a device, always assume the _worst case scenario and beyond_. That's what I've learned about that so far. – user0042 Sep 04 '17 at 01:01
  • Thanks for the concerns, I am the only user of the mock-up/proto at the moment and I know how to handle the device. You have interesting stuff to share, thanks for all responds. In the last link there is a link to this library: http://dsscircuits.com/articles/arduino-i2c-master-library (interesting reading) which seems do a better job than the wire library. Did just a quick test and I can test the device is there without any problems. Tomorrow (it is now 4am here :-P ) I will take a better look at it and do some test and of course will post my findings here. – Codebeat Sep 04 '17 at 02:00
  • 1
    @Erwinus Glad to help. – user0042 Sep 04 '17 at 02:01
  • 1
    It's working! Display is plug and play without locking the main program! It needs some design changes to get it to work, at first to skip the default wire library (very important), send 0x00 to find device is available, some flags to keep track on changes (poll or interrupt and the device needs to be reintialized after replug), two resistors to keep the bus stable. Post my findings tomorrow. – Codebeat Sep 05 '17 at 03:10