I2C Protocol

The I2C, or inter-integrated circuit, protocol is one example of a serial protocol for devices to communicate with one another. I2C is a serial protocol because it has a clock line and single data line which is used for both sending and receiving data. Compared to other serial protocols I2C has some interesting properties:

  • The I2C protocol only uses 2 wires to send and receive data. One line is a clock, called SCL, which pulses high and low to drive the sending and receiving of bits. The other line is the data line, called SDA, which contains the value of a sent or received bit during clock line transitions.
  • Multiple I2C devices can be connected to the same clock and data lines. This means you can have many different sensors and devices all connected to the same couple pins from your development board. The I2C protocol uses a 7-bit address assigned to each device as a way for the development board to talk to a specific device. As a result of using 7-bit addresses the I2C protocol is limited to 127 unique devices connected to one bus (or pair of data and clock lines).
  • The speed of the I2C bus is fixed, typically to 100khz, 400khz, or 1mhz. This means I2C is a good protocol for talking to devices that don’t send a lot of data or need very fast responses. A TFT display which receives hundreds of kilobytes and even megabytes of image data wouldn’t make sense as an I2C device because sending so much data over a 100khz bus would be quite slow. However a small sensor like a temperature or light sensor that sends a small 16 or 32-bit value typically doesn’t need a fast bus.
  • The I2C clock and data lines need pull-up resistors to prevent from floating to random values. Since many different devices can share these lines the I2C protocol requires that each device ‘give up’ or stop driving the lines when not in use. If no device is driving the lines then the pull-up resistors ensure they go up to a high logic level instead of floating at random values. Most I2C device boards (in particular the boards Adafruit creates) have these pull-up resistors built-in, but if you’re talking to a chip or building a board yourself you might need to add ~2.2-10 kilo-ohm resistors connected from both data and clock lines up to high logic level.
  • The I2C protocol includes a simple guarantee that data has been transferred between devices. When one I2C device receives data from another device it uses a special acknowledgement bit to tell the sending device that data has been received. There’s no error correction, parity checks, etc.–just a simple yes/no that data has been successfully sent and received.
  • Typically one device on an I2C bus is the ‘main’ which controls the clock line and sends requests to other connected devices. In most cases your development board is the main device that drives the I2C bus clock. Sensors and other I2C devices connected to the bus listen for requests from the main and respond appropriately. This guide covers this most common scenario where your development board is the I2C main and is talking to connected devices.
  • Many I2C devices expose data through a simple register table. This means to query data from the device you need to know both the address of the device and the address of the register you wish to query. Check your device’s datasheet for the exact device and register address values. Typically registers are 8-bit values (0 to 255) but devices are free to use larger or smaller sizes–always check your device’s datasheet to be sure how it exposes data!

These properties make I2C an attractive protocol for sensors and other simple devices that don’t need to send or receive data quickly. Many different sensors can all be connected to the same I2C clock and data lines. By giving each sensor a unique 7-bit address your development board and code can query each one for their current value.

The following is not required for general usage of the MCP9808 breakout. Use the MCP9808 library instead.

MCP9808 I2C Temperature Sensor

To demonstrate interacting with an I2C device this guide will show you how to query the temperature from a MCP9808 high precision temperature sensor. This sensor is a good example of an I2C device because it has a very simple register structure. To read the temperature you simply read one register from the device–there’s no complex logic or need to read other registers like on some other sensors. You’ll need these parts to follow this section:

Connect the components together as follows:

  • Board 5V or 3.3V output to MCP9808 VDD or VIN.
  • Board ground/GND to MCP9808 GND.
  • Board SCL (I2C clock line) to MCP9808 SCL.
  • Board SDA (I2C data line) to MCP9808 SDA.

Remember the I2C protocol requires pull-up resistors to be on the clock and data lines. If you’re using an Adafruit breakout board like the MCP9808 sensor linked above then these pull-ups are built-in and nothing else is necessary. However if you’re wiring a chip directly to your board or using a differnet breakout you might need to add pull-up resistors. Typically these are 2.2k - 10k ohm resistors that connect both clock and data up to high logic / 3.3V.

Once the device is wired up you’re ready to start interacting with it from CircuitPython. The easiest way to demonstrate this control is from the serial REPL and an interactive Python session. Connect to your board’s serial REPL, then import the board and busio module:

>>> import board
>>> import busio

The busio module contains an interface for using hardware-driven I2C communication from your board. Note that on some boards, like the ESP8266, they might not support hardware-driven I2C and must fall back to a slower software driven approach. You’ll learn more about software driven, or bit-banged, access later in this guide. If you’re not sure check your board’s documentation for its support of I2C–most boards like the Metro M0, Trinket M0, Gemma M0, Circuit Playground Express support a hardware-driven I2C interface.

Within the busio module you’ll use the busio.I2C class to create an interface to access the I2C bus:

>>> i2c = busio.I2C(board.SCL, board.SDA)

When creating the I2C class you must specify the clock line and data line pins. Typically these are the board.SCL and board.SDA objects but check your board’s documentation in case there are other hardware I2C buses with different clock and data line names.

Once you have access to the I2C bus it’s easy to scan the bus to find the address of all devices connected to it. Call the busio.I2C.scan() function.

However before you make calls against the I2C bus you first need to take control, or ‘lock’, it to ensure your code has exclusive access to I2C. There are a few ways to lock the bus like waiting on the busio.I2C.try_lock() function and then calling the busio.I2C.unlock() function when finished (typically in a Python try-finally block). Locking the bus tells CircuitPython that your code needs to use I2C and that any other code using I2C should wait for your code to finish. This helps different bits of code use the same hardware peripherals by ensuring they don’t try to use it at the same time or interrupt each other.

To lock the I2C bus you want to use a special loop syntax that waits for the busio.I2C.try_lockfunction to succeed:

>>> while not i2c.try_lock():
...     pass
...
>>>

This loop will continually call the try_lock function until it returns true that the I2C bus was locked. Remember other code might be using the bus so the loop will keep trying to lock the bus and ensure it’s available to use. Once the bus is locked you can start to call functions to access it, like scanning for any available I2C devices. Try calling the busio.I2C.scan() function to list the addresses of all I2C devices found on the bus:

>>> i2c.scan()
[24]

Notice when you use the with statement all the code inside of it is indented and doesn’t run until you end the with statement (but removing the indentation or pressing enter three times). The great thing about the with statement and context manager is that it’s automatically locking and unlocking the I2C interface so calls like scan can be made.

Notice the busio.I2C.scan() function returns a list of 7-bit I2C device addresses. Be careful as Python treats these numbers as normal base 10 values when printing them, whereas most I2C addresses from datasheets are in hex. You can use a special list comprehension syntax to convert the list of numbers into hex strings:

>>> [hex(x) for x in i2c.scan()]
['0x18']

Now you’ll see a list of 2 digit hex strings which are easier to double check with your device’s datasheet. In this case a device with address 0x18 is visible on the I2C bus and if you check the MCP9808 datasheet you’ll see by default its I2C address is 0x18. Perfect! This means the sensor is properly connected, powered, and responding to requests.

If for some reason you don’t see anything returned by scan, or completely different addresses then double check your wiring, power, and if pull-up resistors are necessary to add. If any one of those things isn’t setup correctly the device will not be visible to the I2C bus and scan.

Next you can read bytes from registers using a combination of writing and reading functions. With the I2C protocol all requests are actually transactions where the main devices writes to and then reads from a connected device. First the main writes the address of the register it wants to read, then it reads a number of bytes from the device.

For example with the MCP9808 its temperature value is stored in a 16-bit register at address 0x05. You can read the value of this register by running:

>>> i2c.writeto(0x18, bytes([0x05]))
>>> result = bytearray(2)
>>> i2c.readfrom_into(0x18, result)
>>> result
bytearray(b'\xc1s')

Let’s break down step by step what’s happening here:

  • First the busio.I2C.writeto() function is called to start an I2C transaction by writing bytes of data from the board to the MCP9808. The first parameter is the address of the MCP9808, 0x18, and the second parameter is a list of bytes to be written. In this case only one byte, the value 0x05, is written. If you check the MCP9808 datasheet this 0x05 value is the temperature reading register.
  • After writing data from the board to the MCP9808 we need to receive two bytes of temperature sensor register data. To do this we’ll call the busio.I2C.readfrom_into()function. But before you can call the function you need a place to store the returned bytes and to do this a bytearray of size 2 is created. This result bytearray will be passed to the read function and then filled with the results read from the MCP9808.
  • The busio.I2C.readfrom_into() function is finally called to read two bytes of data from the MCP9808. Again the first parameter to the read function is the address of the device (0x18) and the second parameter is a bytearray that will be filled with the bytes that are read. How does the function know how many bytes to read? The size of the passed in bytearray by default will determine how many bytes to read, so if you need to read more or less bytes the easiest way is to change the size of the bytearray passed in.

The last statement prints the two bytes that were read, 0xC173. This is the response from the MCP9808 after it was asked to send the temperature sensor register. If you check the datasheet you can see the format for this response actually encodes the sensed temperature. Luckily with Python it’s easy to make a function that decodes the temperature:

>>> def temp_c(data):
...    value = data[0] << 8 | data[1]
...    temp = (value & 0xFFF) / 16.0
...    if value & 0x1000:
...        temp -= 256.0
...    return temp
...
>>> temp_c(result)
23.1875

Notice the temperature of the MCP9808 is printed in Celsius!

Once you’re finished using I2C devices be sure to call the busio.I2C.unlock() function to give back control of the I2C bus to other code. You can explicitly call it like:

>>> i2c.unlock()

Or you can put your code in a try-finally block in Python which ensures the unlock function is called no matter what, even if your code fails. For example a complete scan and read with try-finally might look like:

>>> while not i2c.try_lock():
...     pass
...
>>> try:
...     [hex(x) for x in i2c.scan()]
...     i2c.writeto(0x18, bytes([0x05]))
...     result = bytearray(2)
...     i2c.readfrom_into(0x18, result)
... finally:
...     i2c.unlock()
...
['0x18']
>>> result
bytearray(b'\xc1s')

That’s all there is to interacting with I2C devices from CircuitPython. With the busio.I2C class you have direct access to crafting I2C transactions of almost unlimited complexity. Most devices will use the basic write register, read bytes flow you saw here, but be sure to check your device’s datasheet in case it has different I2C protocol requirements. 

If an I2C device requires a "repeated start", then use the writeto_then_readfrom() method.

I2CDevice Library

You saw above how to interact with an I2C device using the API built-in to CircuitPython. Remember using the built-in API requires careful management of the lock and unlock functions to access the I2C bus. If you’re writing code to talk to an I2C device you might find using the CircuitPython bus device library a bit easier to manage as it controls locking and unlocking automatically (using Python’s context manager with statement).

To use the bus device library you’ll first need to install the library on your board.

First make sure you are running the latest version of Adafruit CircuitPython for your board.

Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle.  Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.

Once you have the bus device library installed you can use the I2CDevice class to simplify access to a device on the I2C device bus. First setup the I2C bus exactly as you did before:

>>> import board
>>> import busio
>>> i2c = busio.I2C(board.SCL, board.SDA)

Now import the bus device module and create an instance of the I2CDevice class. Notice the I2CDevice class needs to be told both the I2C bus object, and the address of the I2C device to talk to (0x18 for the MCP9808 sensor here):

>>> from adafruit_bus_device.i2c_device import I2CDevice
>>> device = I2CDevice(i2c, 0x18)

Now you can use similar functions to read and write data on the I2C bus to interact with the I2CDevice. The important difference here is that the read and write functions on the I2CDevice object will remember and automatically send the right device address. In addition you can use Python’s with statement as a context manager to automatically lock and unlock the I2C bus.

Here’s how to read the temperature register using the I2CDevice:

>>> with device:
...     device.write(bytes([0x05]))
...     result = bytearray(2)
...     device.readinto(result)
...
>>> result
bytearray(b'\xc1s')
>>> temp_c(result)
23.1875

Notice you no longer need to specify the address of the device (0x18) when reading and writing. The with statement is also automatically locking and unlocking the I2C bus so you don’t need to manage the locking yourself either. The only limitation of the I2CDevice class is that it needs to talk to a single device and can’t scan the entire bus or interact with multiple devices (instead create multiple I2CDevice instances!).

If you are writing code to interact with an I2C device it is highly recommended to use the I2CDevice class.

Also for interacting with most sensors and devices you typically do not need to write these low-level direct I2C bus manipulation requests (with either the built-in APIs or I2CDevice class). Instead look for a higher level library to interact with the device, like the CircuitPython MCP9808 library. Using a library saves you the work of writing this low-level I2C code and instead you can interact with simple temperature and other device properties. However it is handy to know how to write low-level I2C transactions in case you’re dealing with devices that don’t yet have a driver available!

Scan All Registers

An interesting property of most I2C devices is that they expose data with simple registers. Like you saw above with the MCP9808 sensor the register 0x05 held the temperature as 2 bytes of data. It’s sometimes handy to scan all of the registers of an I2C device and print out their values. Here’s an example of scanning a set of registers from the first I2C device found and printing their contents as hex (you might want to save this as a main.py file that runs at boot instead of typing it all into the REPL!):

import board
import busio

REGISTERS = (0, 256)  # Range of registers to read, from the first up to (but
                      # not including!) the second value.

REGISTER_SIZE = 2     # Number of bytes to read from each register.

# Initialize and lock the I2C bus.
i2c = busio.I2C(board.SCL, board.SDA)
while not i2c.try_lock():
    pass

# Find the first I2C device available.
devices = i2c.scan()
while len(devices) < 1:
    devices = i2c.scan()
device = devices[0]
print('Found device with address: {}'.format(hex(device)))

# Scan all the registers and read their byte values.
result = bytearray(REGISTER_SIZE)
for register in range(*REGISTERS):
    try:
        i2c.writeto(device, bytes([register]))
        i2c.readfrom_into(device, result)
    except OSError:
        continue  # Ignore registers that don't exist!
    print('Address {0}: {1}'.format(hex(register), ' '.join([hex(x) for x in result])))

# Unlock the I2C bus when finished.  Ideally put this in a try-finally!
i2c.unlock()

For example with the MCP9808 you might see output like:

Found device with address: 0x18
Address 0x0: 0x0 0x1d
Address 0x1: 0x0 0x0
Address 0x2: 0x0 0x0
Address 0x3: 0x0 0x0
Address 0x4: 0x0 0x0
Address 0x5: 0xc1 0x83
Address 0x6: 0x0 0x54
Address 0x7: 0x4 0x0
Address 0x8: 0x3 0x1
Address 0x9: 0x60 0x1
Address 0xa: 0xa2 0x1
Address 0xb: 0x25 0x88
Address 0xc: 0x0 0x1

This guide was first published on Sep 13, 2017. It was last updated on Mar 08, 2024.

This page (I2C Devices) was last updated on Mar 08, 2024.

Text editor powered by tinymce.