I2C-Tiny-USB bridge
Overview
This sample implements the vendor request protocol of the I2C-Tiny-USB project [1] on top of the USB
device support and the I2C driver API. It turns a Zephyr board into a USB to I2C adapter that can be
used with the Linux i2c-tiny-usb bus driver [2], making the I2C bus of the board accessible from
the host through the i2c-dev interface and tools like i2cdetect, i2cget, or i2cset.
All communication is done through vendor requests on the default control pipe. The commands to perform I2C transfers are handled using vendor request nodes, while the commands to get the adapter capabilities and transfer status are handled by a minimal USB function with a vendor specific interface and no endpoints. I2C messages belonging to the same transaction are collected and executed in a single I2C transfer, which preserves repeated start conditions between the messages.
Any I2C controller driver can be used, including the GPIO bit-banging driver
gpio-i2c on boards without a free hardware I2C controller. The I2C controller is
selected with the zephyr_i2c devicetree node label, which many boards already provide. On a
board without it, a devicetree overlay can attach the label to any I2C controller, for example
zephyr_i2c: &arduino_i2c {};.
Requirements
This project requires a USB device controller driver using the UDC API and an I2C controller with
the zephyr_i2c devicetree node label.
On the host side you need:
A Linux kernel with the
i2c-tiny-usbdriver (CONFIG_I2C_TINY_USB), which is enabled in most distribution kernels.The
i2c-toolspackage, which provides thei2cdetect,i2cget, andi2csetcommands used below. On Debian and Ubuntu install it withsudo apt install i2c-tools, on Fedora withsudo dnf install i2c-tools.
Building and Running
Build and flash the sample with:
west build -b adafruit_feather_rp2040 samples/subsys/usb/i2c_tiny_usb
west flash
On the Feather RP2040, zephyr_i2c is the controller routed to the
STEMMA QT connector, so any STEMMA QT or Qwiic breakout can be plugged in without additional wiring.
The sample identifies itself with the Zephyr Project vendor ID and its own product ID, which the i2c-tiny-usb driver does not know about. Until the pair is added to
the driver ID table upstream, use the feature described in the “Dynamic USB device IDs” LWN.net
article [3] to make the driver claim the device:
$ sudo modprobe i2c-tiny-usb
$ sudo sh -c "echo 0x2fe3 0x0013 > /sys/bus/usb/drivers/i2c-tiny-usb/new_id"
Note
This step can be performed right after loading the i2c-tiny-usb module instead. Otherwise,
you may have to disconnect and reconnect the board in order for the driver to bind to it.
The driver then binds to the device and registers a new I2C adapter:
$ sudo dmesg | grep i2c-tiny-usb
i2c-tiny-usb 1-2.1:1.0: version 1.05 found at bus 001 address 016
i2c i2c-20: connected i2c-tiny-usb device
$ i2cdetect -l | grep tiny
i2c-20 i2c i2c-tiny-usb at bus 001 device 016 I2C adapter
The adapter shows up as i2c-N, where N is a bus number the kernel assigns dynamically
(i2c-20 in the output above, but it may differ on your system and can change when the device is
re-plugged). That number N is what you pass to the i2c-tools commands as the bus argument.
Substitute your own number for 20 in the examples below.
Now the I2C bus of the board can be used from the host, for example to scan for connected devices or to read the manufacturer ID register of a sensor found at address 0x1c:
$ sudo i2cdetect -y 20
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- 1c -- -- --
...
$ sudo i2cget -y 20 0x1c 0x0d
0xc7
Limitations
The transfers of a transaction are only executed when the host announces the last message or requests read data. A NAK from a deferred write is therefore reported with the status of the whole transaction rather than with the message that caused it. Transactions addressing multiple different targets are not supported, such transactions are rejected and reported as failed.