The Inter-Integrated Circuit (IIC or I2C) bus connects simple peripheral devices with small data payloads. Sensors and actuators are common use cases for I2C. Examples include accelerometers, thermometers, LCD displays, and motor drivers.
I2C is a synchronous serial interface, which means it relies on
a shared clock signal to synchronize data transfer between devices. The device
in control of triggering the clock signal is known as the controller (master
).
All other connected peripherals are known as workers (slaves
). Each device is
connected to the same set of data signals to form a bus.
I2C devices connect using a 3-Wire interface consisting of:
- Shared clock signal (SCL)
- Shared data line (SDA)
- Common ground reference (GND)
Because all data is transferred over one wire, I2C only supports half-duplex communication. All communication is initiated by the controller device, and the worker must respond once the controller's transmission is complete.
I2C supports multiple worker devices connected along the same bus. Unlike SPI, worker devices are addressed using the I2C software protocol. Each device is programmed with a unique address and only responds to transmissions the controller sends to that address. Every worker device must have an address, even if the bus contains only a single worker.
Adding the required permission
Add the required permission for this API to your app's manifest file:
<uses-permission android:name="com.google.android.things.permission.USE_PERIPHERAL_IO" />
Managing the worker device connection
In order to open a connection to a particular I2C worker, you need to know the unique name of the bus. During the initial stages of development, or when porting an app to new hardware, it's helpful to discover all the available device names from PeripheralManager using getI2cBusList():
Kotlin
val manager = PeripheralManager.getInstance() val deviceList: List<String> = manager.i2cBusList if (deviceList.isEmpty()) { Log.i(TAG, "No I2C bus available on this device.") } else { Log.i(TAG, "List of available devices: $deviceList") }
Java
PeripheralManager manager = PeripheralManager.getInstance(); List<String> deviceList = manager.getI2cBusList(); if (deviceList.isEmpty()) { Log.i(TAG, "No I2C bus available on this device."); } else { Log.i(TAG, "List of available devices: " + deviceList); }
Once you know the target device name, use PeripheralManager to connect to that device. When you are done communicating with the peripheral device, close the connection to free up resources. Additionally, you cannot open a new connection to the device until the existing connection is closed. To close the connection, use the device's close() method.
Kotlin
// I2C Device Name private const val I2C_DEVICE_NAME: String = ... // I2C Worker Address private const val I2C_ADDRESS: Int = ... class HomeActivity : Activity() { private var mDevice: I2cDevice? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Attempt to access the I2C device mDevice = try { PeripheralManager.getInstance() .openI2cDevice(I2C_DEVICE_NAME, I2C_ADDRESS) } catch (e: IOException) { Log.w(TAG, "Unable to access I2C device", e) null } } override fun onDestroy() { super.onDestroy() try { mDevice?.close() mDevice = null } catch (e: IOException) { Log.w(TAG, "Unable to close I2C device", e) } } }
Java
public class HomeActivity extends Activity { // I2C Device Name private static final String I2C_DEVICE_NAME = ...; // I2C Worker Address private static final int I2C_ADDRESS = ...; private I2cDevice mDevice; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Attempt to access the I2C device try { PeripheralManager manager = PeripheralManager.getInstance(); mDevice = manager.openI2cDevice(I2C_DEVICE_NAME, I2C_ADDRESS); } catch (IOException e) { Log.w(TAG, "Unable to access I2C device", e); } } @Override protected void onDestroy() { super.onDestroy(); if (mDevice != null) { try { mDevice.close(); mDevice = null; } catch (IOException e) { Log.w(TAG, "Unable to close I2C device", e); } } } }
Interacting with registers
I2C worker devices organize their contents into either readable or writable registers (individual bytes of data referenced by an address value):
- Readable registers - Contains data the worker wants to report to the controller, such as sensor values or status flags.
- Writable registers - Contains configuration data that the controller can control.
A common protocol implementation known as System Management Bus (SMBus) exists on top of I2C to interact with register data in a standard way. SMBus commands consist of two I2C transactions as follows:
The first transaction identifies the register address to access, and the second reads or writes the data at that address. Logical data on a worker device may often take up multiple bytes, and thus encompass multiple register addresses. The register address provided to the API is always the first register to reference.
Peripheral I/O provides three types of SMBus commands for accessing register data:
Byte Data: readRegByte() and writeRegByte() Read or write a single 8-bit register value.
Word Data: readRegWord() and writeRegWord() Read or write two consecutive register values as a 16-bit little-endian word. The first register address corresponds to the least significant byte (LSB) in the word, followed by the most significant byte (MSB).
Block Data: readRegBuffer() and writeRegBuffer() Read or write up to 32 consecutive register values as an array.
Kotlin
// Modify the contents of a single register @Throws(IOException::class) fun setRegisterFlag(device: I2cDevice, address: Int) { // Read one register from worker var value = device.readRegByte(address) // Set bit 6 value = value or 0x40 // Write the updated value back to worker device.writeRegByte(address, value) } // Read a register block @Throws(IOException::class) fun readCalibration(device: I2cDevice, startAddress: Int): ByteArray { // Read three consecutive register values return ByteArray(3).also { data -> device.readRegBuffer(startAddress, data, data.size) } }
Java
// Modify the contents of a single register public void setRegisterFlag(I2cDevice device, int address) throws IOException { // Read one register from worker byte value = device.readRegByte(address); // Set bit 6 value |= 0x40; // Write the updated value back to worker device.writeRegByte(address, value); } // Read a register block public byte[] readCalibration(I2cDevice device, int startAddress) throws IOException { // Read three consecutive register values byte[] data = new byte[3]; device.readRegBuffer(startAddress, data, data.length); return data; }
Transferring raw data
When interacting with an I2C peripheral that defines its registers differently than SMBus -- or perhaps doesn't use registers at all -- use the raw read() and write() methods for full control over the data bytes transmitted across the wire. These methods will execute a single I2C transaction as follows:
With raw transfers, the device will send a single start condition before the transfer and a single stop condition after. It is not possible to combine multiple transactions with a "repeated start" condition.
The following code sample show you how to construct a raw byte buffer and write it to an I2C worker:
Kotlin
@Throws(IOException::class) fun writeBuffer(device: I2cDevice, buffer: ByteArray) { device.write(buffer, buffer.size).also { count -> Log.d(TAG, "Wrote $count bytes over I2C.") } }
Java
public void writeBuffer(I2cDevice device, byte[] buffer) throws IOException { int count = device.write(buffer, buffer.length); Log.d(TAG, "Wrote " + count + " bytes over I2C."); }