I2C Detective for identifying I2C devices


The I2C Detective is a uLisp program that identifies the I2C devices connected to your microcontroller from a database of the most popular I2C sensors and other devices.

It identifies a device on the I2C bus from its address, and it can distinguish between multiple devices with the same address by reading the devices’ signatures.

To use it, load the function definitions, and then call (scan). It will print a list of the I2C addresses and possible devices on each address. For example, on an Adafruit PyGamer with its internal accelerometer and an external temperature/pressure sensor connected:

> (scan)
#x19:  Accelerometer - LIS3DH
#x77:  Temperature/Pressure Sensor - BMP280

Although the MCP9808 temperature sensor also supports the address #x19, and there are several other sensors with address #x77, the I2C Detective eliminates these as possibilities by reading the registers corresponding to the sensors’ Device IDs to identify them.

Here’s the whole program for the I2C Detective: I2C Detective program.

Alternatively it’s available on GitHub: https://github.com/technoblogy/ulisp-i2c-detective.

I welcome the addition of information about any popular sensors I’ve missed.

How it works

The I2C Detective is an extension of the uLisp I2C scan program, which scans the I2C bus and prints out any addresses found on the bus:

(defun scan () 
  (dotimes (p 127)
    (with-i2c (str p)
      (when str (lookup p)))))

The modified version of scan looks up each address in a database of devices by calling lookup:

(defun lookup (p)
  (let (result)
    (dolist (category *devices*)
      (let ((name (first category))
            (options (cdr category)))
            (dolist (device options nil)
              (let* ((id (first device))
                     (from (second device))
                     (to (third device))
                     (test (nth 3 device))
                     (ok (if (listp from) (member p from) (<= from p to))))
                 ((not ok) nil)
                 ((and test (funcall (eval test) p))
                  (setq result (list p name id)) (return t))
                 (test nil)
                 (t (push id result) (push name result) (push p result)))))
    (when result (format t "~{#x~x: ~a - ~a~%~}" result))))

The lookup function uses the following logic:

  • If the device doesn’t support the address p, ignore it.
  • If the device provides a test function and the test returns t, set the list result to this device and return.
  • If the device provides a test function and the test returns nil, ignore it.
  • Otherwise add the device to the list result.

For each address it then prints out the address, category, and device for each device in the list result.

Device database

The device database is specified as a data structure in the global variable *devices*.

Each entry in the database consists of a device category, such as “Temperature Sensor”, followed by a list of the devices in that category.

Each device is specified as a list of three or four items:

(device from to [test])
  • device is the manufacturer’s name for the device; for example “MCP9809”.
  • from is the first I2C address supported by the device, or a list of I2C addresses.
  • to is the last I2C address supported by the device. If there is only one address this is the same as from.
  • test is an optional function that will be called with the address as a parameter. It should return t if the device is correct.

The two options for expressing the valid I2C addresses are either as a range between from and to inclusive:

("TMP102" #x48 #x4B)

or by specifying from as a list of I2C addresses, in which case to is nil:

("TSL2561" (#x29 #x39 #x49) nil) 

Who am I?

To test a device’s ID call the whoami function:

(whoami register value)

where register is the register containing the device ID, and value is the correct value of the device ID.

For example, the MCP9808 Temperature Sensor provides a Device ID in register 7, and the correct ID is 4:

("MCP9809" #x18 #x1F (whoami #x07 #x04))

Here’s the definition of whoami. It returns a function called with the device’s I2C address:

(defun whoami (reg value)
  (lambda (p)
    (with-i2c (str p)
      (when str
        (write-byte reg str)
        (restart-i2c str 1)
        (= (read-byte str) value)))))

Here’s how you can call it to test the WhoAmI register #x0F which returns value #x33, on an LIS3DH Accelerometer at address #x19:

> (funcall (whoami #x0F #x33) #x19)

Note that not all I2C devices provide a WhoAmI function, or have one in the database, so there may be multiple devices listed for a given address.

Special test functions

For devices that can’t be handled by the standard test function whoami you can define a device-specific test function.

For example, the LSM303AGR Accelerometer/Magnetometer contains an accelerometer at address #x19 and a magnetometer at address #x1E in the same package. The accelerometer returns a device ID from register #x0F with value #x33, and the magnetometer returns a device ID from register #x4F with value #x40. The following lsm303agr function checks both these IDs:

(defun lsm303agr (p)
   (funcall (whoami #x0F #x33) #x19)
   (funcall (whoami #x4F #x40) #x1E)))

The device is defined in the database with the following definition:

("LSM303AGR" #x19 #x1E lsm303agr)