► Games

► Sound & Music

► Watches & Clocks


► Power Supplies

► Computers

► Graphics

► Thermometers

► Wearables

► Test Equipment

► Tutorials

► Libraries

► PCB-Based Projects

By processor

AVR ATtiny

► ATtiny10

► ATtiny2313

► ATtiny84

► ATtiny841

► ATtiny85

► ATtiny861

► ATtiny88

AVR ATmega

► ATmega328

► ATmega1284

AVR 0-series and 1-series

► ATmega4809

► ATtiny1604

► ATtiny1614

► ATtiny3216

► ATtiny3227

► ATtiny402

► ATtiny404

► ATtiny414

► ATtiny814

AVR DA/DB-series

► AVR128DA28

► AVR128DA32

► AVR128DA48

► AVR128DB28



► RP2040

► RA4M1

About me

  • About me
  • Twitter
  • Mastodon


RSS feed

I2C Detective

19th January 2021

The I2C Detective identifies the I2C devices connected to your microcontroller from a database of the most popular I2C sensors and other devices. It lists each device on the I2C bus, and can distinguish between multiple candidates at a particular address by reading the device IDs:


The I2C Detective showing a list of the I2C sensors provided on an Adafruit Clue.

In the above example, although there are other possible sensors with addresses 0x1C, 0x39, 0x6A, and 0x77, the I2C Detective eliminates these as possibilities by reading the registers corresponding to the sensor device IDs to identify them. If a sensor doesn't provide a device ID, such as with the SHT30 on address 0x44, the I2C Detective lists all the popular sensors that support this address.

The I2C Detective will run on any Arduino-compatible board, from the Arduino Uno upwards.

I2C Detective on the Adafruit Clue

Although you can run the I2C Detective on any Arduino platform, it can be used on the Adafruit Clue [1] as a stand-alone tool, as shown in the photo above. The Clue provides a colour TFT display, several built-in sensors, and a Stemma QT connector (compatible with Sparkfun Qwiic) to make it easy to plug in additional I2C sensors. You could do something similar for the Adafruit PyGamer/PyBadge which has the larger Stemma connector (compatible with Grove).


I wrote a first version of the I2C Detective in uLisp [2], my Lisp for microcontrollers. I then wrote a Lisp program to generate this C program automatically from the Lisp database of I2C devices, so I only need to keep one database up to date.

The database consists of a list of categories, and a list of devices. Each device references an entry in the category list.


The categories are strings stored in program memory, to save space on microcontrollers with limited RAM. A few sample entries are:

// Categories
const char Accelerometer[]                   PROGMEM = "Accelerometer";
const char AccelerometerGyroscope[]          PROGMEM = "Accelerometer/Gyroscope";
const char AccelerometerMagnetometer[]       PROGMEM = "Accelerometer/Magnetometer";
const char Amplifier[]                       PROGMEM = "Amplifier";


The second list is a list of the devices for which information is available, again stored in program memory. For convenience the devices are in alphabetical order, and new devices can simply be added into the list. Here are some sample entries:

// Devices
const device_t Device[] PROGMEM = {
//  Device        Category                              From  To    Reg   ID
  { "ADS1113",    AnaloguetoDigitalConverter,           0x48, 0x4B, 0x00, 0x00 },
  { "ADS1114",    AnaloguetoDigitalConverter,           0x48, 0x4B, 0x00, 0x00 },
  { "ADS1115",    AnaloguetoDigitalConverter,           0x48, 0x4B, 0x00, 0x00 },
{ "ADT7410", TemperatureSensor, 0x48, 0x4B, 0x0B, 0xCB }, { "ADXL345", Accelerometer, 0x53, 0x1D, 0x00, 0xE5 },

Each entry in the list is specified by the device_t structure:

typedef struct {
  const char *device;
  const char *category;
  uint8_t from;
  uint8_t to;
  uint8_t test;
  uint8_t value;
} device_t;

These fields are as follows:

  • device is the manufacturer's name for the device; for example "ADT7410".
  • category is one of the entries in the list of categories, specifying the category of the device.
  • from and to specify the I2C addresses supported by the device; see below.
  • test is the register that can be interrogated to get the device's ID.
  • value is the correct device ID value returned from that register.

The valid I2C addresses can be expressed as a range between from and to inclusive.

Alternatively, if from is greater than to it signifies that the device supports these two noncontiguous I2C addresses.

If the device supports more than two noncontiguous I2C addresses you can make multiple entries in the table to specify all the alternative I2C addresses.

If no device ID is available both test and value have the value 0x00. 

The program

The function match() checks an address against the from and to values for a device, and returns true if it matches:

boolean match (uint8_t addr, uint8_t from, uint8_t to) {
  if (from > to) return (addr == from) || (addr == to);
  else return (addr >= from) && (addr <= to);

The function whoami() reads a specified register in the device, and returns true if it matches a specified value:

boolean whoami (uint8_t addr, uint8_t reg, uint8_t value) {
  if (reg == 0x00 && value == 0x00) return false;
  if (Wire.endTransmission(false) != 0) return false;
  Wire.requestFrom(addr, 1);
  return value ==;

If a device ID isn't available the register and value are specified as 0x00.

Looking up an address

Each I2C address found on the bus is looked up in the device table by calling lookup():

void lookup (uint8_t addr) {
  // Check for devices with a device ID match
  boolean listed = false;
  for (int pass=1; pass<4; pass++) {
    for (int i=0; i<Devices; i++) {
      const device_t *me = &Device[i];
      uint8_t from = me->from;
      uint8_t to = me->to;
      uint8_t test = me->test;
      uint8_t value = me->value;
      if (match(addr, from, to)) {
        boolean ok;
        if (pass == 1) ok = twindevice(i) && whoami(addr, test, value);
        else if (pass == 2) ok = whoami(addr, test, value) && !findtwin(i);
        else if (pass == 3) ok = test == 0xFF;
        if (ok) {
          list(addr, me);
          listed = true;
    if (listed) return;
  list(addr, &Unknown);

For a given I2C address lookup() performs three passes through the device table. If a pass lists any devices the subseqeuent passes are skipped:

  • The first pass checks only devices with two entries in the device table. More about this below.
  • The second pass checks devices that match the I2C address, have a device ID test, and return a correct device ID. It ignores the devices with two entries handled.
  • The third pass lists all the devices that match the I2C address but don't have a device ID test.
  • If all passes fail to list any devices lookup() prints "Unknown Device".

The actual printing is performed by list():

void list (uint8_t addr, const device_t *me) {
  Serial.print("0x"); Serial.print(addr/16, HEX);
  Serial.print(addr&0xF, HEX); Serial.print(' ');
  Serial.print(": "); Serial.print(me->device);

Twin devices

Some I2C devices are twins: they contain two I2C sensors in a single package, and respond to two separate I2C addresses. To make things confusing the two sensors are often available separately with different part numbers. For example, the LSM303AGR Accelerometer/Magnetometer contains an accelerometer on address 0x19 and a magnetometer on address 0x1E. The accelerometer device ID register reports the same device ID as the LIS3DH accelerometer, and the magnetometer device ID register reports the same device ID as the LIS2MDL magnetometer.

To avoid multiple entries in the I2C device listing, the I2C Detective uses a function twindevice() to check whether a device has two entries in the device table, and if so, calls whoami() for each of the addresses that the device responds to. If both device IDs match, twindevice() returns true, and the device is listed in the first pass of lookup():

boolean twindevice (int i) {
  boolean ok;
  // Check whether previous or next one in list is a twin
  const device_t *other = findtwin(i);
  if (other) {
    // Check its whoami
    uint8_t from = other->from;
    uint8_t to = other->to;
    uint8_t test = other->test;
    uint8_t value = other->value;
    if (from > to) {
      ok = whoami(from, test, value) || whoami(to, test, value);
    } else {
      ok = false;
      for (uint8_t addr = from; addr <= to; addr++) {
        ok = whoami(addr, test, value);
        if (ok) break;
    if (ok) return true;
  return false;

Scanning the bus

Finally, scan() scans the I2C bus, and calls lookup() on every address that returns a non-zero error from Wire.endTransmission(true):

void scan () {
  for (uint8_t addr=8; addr<128; addr++) {
    int ok = true;
    if (Wire.endTransmission(true) == 0) lookup(addr);

Running the I2C detective

Running the I2C Detective on AVR boards

Both lookup() and list() contain alternative versions of some statements, not shown above, to cater for the fact that on the older AVR processors, data in program memory has to be accessed through the special functions pgm_read_byte() and pgm_read_word(). To select the correct version of these statements uncomment the #define at the start of the program on AVR processors:

#define AVR_DATA

Running on Wire1

If you want to run the I2C detective on a board that has I2C sensors on another I2C bus, such as Wire1 on the BBC Micro:Bit Version 2, add a line such as this at the start of the program:

#define Wire Wire1

Program listings

Here's the whole I2C Detective database and program: I2C Detective program.

Here's the version for the Adafruit Clue: I2C Detective Clue program.

Or get both versions from GitHub here:

Please post any corrections or additions to GitHub.

A plea

Finally, a plea to all manufacturers of I2C sensors and other devices:

  • Please provide a register that returns a unique ID for your product. Ideally, choose an ID that doesn't conflict with other devices that use the same address(es).
  • Please give each register a one-byte address. Don't require the user to write two bytes in order to read a single register.

  1. ^ Adafruit Clue on
  2. ^ I2C Detective on

blog comments powered by Disqus