Scanning an I2C bus

Scanning an I2C bus

A customer pointed out to us that the Yoctopuce API doesn't allow for the Yocto-I2C to scan an I2C bus to detect the devices connected to it. It turns out that this is not quite accurate...

To be fair, we never had the idea to offer a function enabling you to scan the address space of an I2C bus to detect the devices which are present. In real life, the address of an I2C device is almost always determined at the hardware level and cannot be changed dynamically. We can therefore assume that the addresses that are used are known beforehand. That why we think that, unless you intend to build a plug-and-play system with I2C modules, for which I2C is clearly not designed, scanning the bus has only limited interest.

This being said, the Yoctopuce API contains all the primitives to write a function enabling you to scan an I2C bus.

Principle

The idea is to try to contact each address one after the other by sending:

  • A start condition {S} indicating that we start an I2C transaction
  • The address to be tested (shifted one bit to the left)
  • A stop condition {P} indicating that we give up the transaction.

If there is a device at the tested address, the device answers with an Acknowledge {A}, otherwise there will be a timeout. We can easily test the principle with the VirtualHub. For example, if we want to test the presence of an RTC DS3231 chip from Maxim with an I2C address of 0x68, we try to send {S}D0{P} and we should obtain an answer ending in "{A}". If nothing answers, we obtain a message ending with an exclamation mark.

Test with the VirtualHub, there is something at address 0x68, nothing lives at address 0x69
Test with the VirtualHub, there is something at address 0x68, nothing lives at address 0x69


To do the same thing by software, you only need to use the Yi2cPort.queryLine() function with a minimal timeout, but long enough to let a potential device time to answer. Then, you must check that the answer ends with "{A}":

string result = i2cPort.queryLine("{S}D0{P}", 10);
if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
  Console.writeline("I2C device present at address 0x68")


Subtlety

However, there is a small subtlety for it to work well. By default, the Yocto-I2C waits for two seconds after an I2C error. The idea is that many I2C devices automatically initialize their communications again if an I2C transaction is interrupted for a while. Thus, after an I2C error, you have a better chance of getting off on the right foot if you wait a little before starting a new transaction. It so happens that each time we try to contact an address that doesn't exist, it causes a timeout which the Yocto-I2C considers to be an error. Therefore, it is important to take the precaution to reduce this waiting delay to the minimum while you are performing a scan, even if it means resetting it to a higher value later on.

string prevmode = i2cPort.get_i2cMode();
i2cPort.set_i2cMode("100kbps,5ms");



The scanI2CBus() function

So here is the function which enables you to scan an I2C bus and returns a list of addresses from which answers were received. It's in C#, but it's easy to translate in any other language.

static List<byte> scanI2CBus(YI2cPort i2cPort, byte FirstAddr, byte LastAddr)
{ string errmsg="";
  List<byte> res = new List<byte>();
  // save I2C speed config
  string prevmode = i2cPort.get_i2cMode();
  // set standard speed, 5ms error recovery
  i2cPort.set_i2cMode("100kbps,5ms");
  // loop over addresses
  for (byte i = FirstAddr; i <= LastAddr; i++)
  { // command to contact address device #i
    string data = "{S}" + (i << 1).ToString("X2") + "{P}";
    // send command, 10ms timeout
    string result = i2cPort.queryLine(data, 10);
    if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
    { //received an Aknowledge, there is someone living at address #i
      res.Add(i);
    }
    else
    { // no answer, wait for error recovery delay
      YAPI.Sleep(5, ref errmsg);
    }
  }
  // restore previous speed configuration
  i2cPort.set_i2cMode(prevmode);
  return res;
}



We wondered whether we would add this function directly in the Yoctopuce API. And the answer is no because this function takes a while to run and is blocking. Expect about 4 seconds to scan all of the 127 possible I2C addresses. We are kind of worried that some customers may use it recklessly without realizing that it deteriorates the performances of their application.

Limitations

It is worth noting that this method won't allow you to detect any and all I2C devices available on the market. A few of them are not able to quickly answer to a I2C request at anytime. For instance, some sensors with a somewhat crude I2C implementation are unable to make a measurement and process a I2C transaction at the same time. The SenseAir Kxx CO2 sensor line are an example.

Excerpt from "I2C communication guide K20/K22/K30/K33/K45/K50 plattforms"
Excerpt from "I2C communication guide K20/K22/K30/K33/K45/K50 plattforms"




Conclusion

As a conclusion, here is the full code of a small C# program scanning all the possible addresses of an I2C bus.

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication1
{

class Program
{

  static List<byte> scanI2CBus(YI2cPort i2cPort, byte FirstAddr, byte LastAddr)
  { string errmsg="";
    List<byte> res = new List<byte>();
    // save I2C speed config
    string prevmode = i2cPort.get_i2cMode();
    // set standard speed, 5ms error recovery
    i2cPort.set_i2cMode("100kbps,5ms");
    // loop over addresses
    for (byte i = FirstAddr; i <= LastAddr; i++)
    { // command to contact address device #i
      string data = "{S}" + (i << 1).ToString("X2") + "{P}";
      // send command, 10ms timeout
      string result = i2cPort.queryLine(data, 10);
      if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
      { //received an Aknowledge, there is someone living at address #i
        res.Add(i);
      }
      else
      { // no answer, wait for error recovery delay
        YAPI.Sleep(5, ref errmsg);
      }
    }
    // restore previous speed configuration
    i2cPort.set_i2cMode(prevmode);
    return res;
  }

  static void Main(string[] args)
  {
    string errmsg = "";
    string target;
    YI2cPort i2cPort;

    if (YAPI.RegisterHub("usb", ref errmsg) != YAPI.SUCCESS)
    { Console.WriteLine("RegisterHub error: " + errmsg);
      Environment.Exit(0);
    }

    i2cPort = YI2cPort.FirstI2cPort();
    if (i2cPort == null)
    { Console.WriteLine("No Yocto-I2C Module connected");
      Environment.Exit(0);
    }

    i2cPort.set_i2cMode("100kbps");
    i2cPort.set_i2cVoltageLevel(YI2cPort.I2CVOLTAGELEVEL_3V3);
    i2cPort.reset();
    Console.WriteLine("Scanning... please wait");
    Stopwatch timer = new Stopwatch();
    timer.Start();
    List<byte> addr = scanI2CBus(i2cPort, 1, 127);
    timer.Stop();
    Console.Write("found " + addr.Count + " I2C slave(s) in "
                  + timer.Elapsed.ToString()+"sec ");
    for (int i = 0; i < addr.Count; i++)
    { Console.Write(" 0x" + addr[i].ToString("X2"));
    }
    Console.WriteLine("");
    YAPI.FreeAPI();
  }
}

}



That's all, folks!

Add a comment No comment yet Back to blog












Yoctopuce, get your stuff connected.