About I2C

I2C, also written as I2C, and pronounced I-squared-C, is a method for communicating between devices such as sensors, displays and other peripherals and a microcontroller like the Photon or Electron. It's short for Inter-Integrated Circuit.

Interface Maximum Speed (Gen2) Maximum Speed (Gen 3) Maximum Peripheral Devices
UART Serial 230 Kbit/sec 1 Mbit/sec 1 (point-to-point)
I2C 400 Kbit/sec 400 Kbit/sec Many (limited by addresses)
SPI 60 Mbit/sec 32 Mbit/sec Many (limited by CS GPIO pins)

The first section has some technical information about how it works and why you might want to use it.

The second section has some examples of I2C devices that work with the Particle devices and some sample code. There are many more devices available.

The basics

It's a bus!

I2C is a bus. That means there can be multiple I2C devices connected to a single port on the Particle device. There are some limitations to this, which we'll go into details later.

This is different than the serial ports, for example, which generally connected only two devices together.

The communication requires two lines, SDA and SCL. Most devices also require power and ground, so generally 4 wires are needed. The regular version of I2C described here is designed for short distances, up to a few meters, preferably less.

In I2C terminology, the Particle device (Argon, Boron, Photon, Electron, etc.) is the I2C Master device, and all of the other devices (sensors, displays, etc.) are I2C Slave devices.

It's an addressable bus

Each I2C device on the bus has an address, which uniquely identifies which device the processor wants to communicate with.

One difficulty is that the address is only 7 bits, so there are 128 addresses. Sometimes there can be conflicts, which limit the ability to share the I2C bus.

Also, there may be limits the number of identical I2C devices you can have on an I2C bus. For the LIS3DH accelerometer, it's two. For the DS75 temperature sensor, it's eight. This sub-address is typically set using jumpers, solder pads, or switches.

This is different than SPI, which is also a bus, but uses a device select line (called variously SS, CS, EN, etc.) for each device. So if you want to connect four SPI devices to a SPI bus, you need to dedicate four separate GPIO pins for the select lines, one for each of for the SPI devices.

It's also different than the 1-Wire interface used by the DS18B20 temperature sensors. That has an addressable bus as well, but it has a 64-bit address and each sensor device has a unique serial number from the factory, with no hardware configuration needed.

It's bidirectional

The SDA (data) line is used to send data both to and from the I2C devices. The I2C protocol determines which direction data will be sent.

The SCL (clock) line is mostly generated by the MCU, though it too is somewhat bidirectional when clock stretching is used. That's a detail you won't generally have to worry about; that's taken care of by the Argon, Boron, Photon, Electron, etc. I2C hardware and your I2C device automatically.

But this means you can't use a plain level-shifter, for example, on an I2C bus. The bus switches direction all the time. Fortunately, you rarely need one, as describe below.

It's open-collector

The bidirectional bus is implemented using open-collector drivers. This is how the transistors are wired, and the details aren't important, however the important thing is this:

Both the processor and the I2C devices only ever pull the bus low. It's either floating, or it's pulled low.

It requires pull-up resistors

Since floating is bad, the I2C bus must have pull-up resistors. One on the SDA line and one on the SCL line. They're typically 4.7K or 10K ohm, but should be in the range of 2K to 10K.

Many of the breakout boards you can buy at Adafruit or SparkFun already have the pull-up resistors on them. Here's the schematic for the Adafruit MCP9808 and you can see the resistors (R1 and R2, 10K ohms) pulling up to VDD.

Schematic with pull-ups

That board also has pull-downs on A0, A1, A2 so the default sub-address is 0. You can tell because the resistors go to GND instead.

If you buy a bare chip that's a sensor, it typically won't have a built-in pull-up resistors so you'll need to add the external resistors.

The pull-ups will often connect to 3V3, but sometimes it will be connected to a 5V supply, as described in the next section.

On the Photon and Electron, a 40K weak pull-up is added on SDA/SCL (D0/D1) when in I2C mode, but this is not sufficient for most purposes and you should add external pull-up resistors.

On the P1, there are 2.1K hardware pull-up resistors inside the P1 module. You should not add external pull-ups on the P1.

On Gen 3 devices (Argon, Boron, B-Series SoM, Tracker SoM), a 13K pull-up is added on I2C interfaces. This will sometimes work, but is still too large of a pull-up to be reliable so you should add external pull-ups as well.

It can be 5-volt compatible (sometimes)

The use of open-collector drivers and external pull-up resistors has one big benefit: The Photon/Electron are completely compatible with 5V I2C devices.

If you have 5V I2C devices you can connect the external pull-up resistors to 5V instead of 3V3. Make sure you connect all of them that way and none are still connected to 3V3. And that all of your I2C devices are 5V compatible.

This allows I2C devices to not only run at 5V, but will also have 5V levels for logic 1 on SDA and SCL.

This is different than SPI, which is 5V tolerant, but will only supply 3.3V on MOSI and SCK. Most 5V SPI devices will tolerate the lower 3.3V logic level, but with I2C there will be true 5V logic levels.

The P1 module should not use I2C at 5V as there is an internal 2.1K hardware pull-up resistor to 3V3 inside the P1 module. This is only on the P1, not the Photon or P0.

3rd-generation devices (Argon, Boron, and B-Series SoM) are not 5V tolerant and must not be used with pull-ups to 5V!

Pins

On the all devices there is an I2C interface (Wire) on D0 and D1:

  • DO: SDA
  • D1: SCL

On the Electron, there is a second I2C interface (Wire1) on C4 and C5 as well (with limitations):

  • C4: SDA
  • C5: SCL

On the Argon and Xenon only (not Boron), there is a second I2C interface (Wire1) on D2 and D3:

  • D2: SDA
  • D3: SCL

About the examples

In all of the examples here I've used the following colors for consistency:

  • 3V3: Red
  • VIN: Orange
  • GND: Black
  • SDA: Green
  • SCL: Blue

Displays

One common use for I2C is displays. Here are just a few possibilities:

7-segment

Adafruit has this 0.56" high 4-digit 7-segment LED display. Basically, a digital clock display.

To wire it up, connect:

  • VCC to VIN (orange)
  • GND to GND (black)
  • SDA to D0 (green)
  • SCL to D1 (blue)

This display is designed to run a 5V, so we power it from VIN on the Photon, which is approximately 4.8V and connected to the USB power input.

Since the display is like a clock, here's sample code that makes a digital clock.

7-segment display

The sample code is here.

Mini 8x8 LED matrix

Adafruit has this little (0.8") LED matrix, available in red, green, blue, yellow and white. This example uses the yellow one.

To wire it up, connect:

  • VCC to VIN (orange)
  • GND to GND (black)
  • SDA to D0 (green)
  • SCL to D1 (blue)

This display is designed to run a 5V, so we power it from VIN on the Photon, which is approximately 4.8V and connected to the USB power input.

8x8 matrix display

For this example we'll combine it with an Adafruit 10-DOF IMU (accelerometer plus other sensors). Actually, it works with the 9-DOF, and actually anything that includes the LSM303 compass as we only use the LSM303 in this example.

For the compass sensor, we also connect:

  • VCC to VIN (orange)
  • GND to GND (black)
  • SDA to D0 (green)
  • SCL to D1 (blue)

Here we've connected two I2C devices to the same I2C bus and they work fine together.

compass circuit

The sample code is here.

OLED

0.96" OLED I2C displays are inexpensive and easy to use. This one is compatible with the SSD1306 and is 128x64 pixels.

The sample code in the bme280-oled project combines a BME280 temperature and humidity sensor and a SSD1306 OLED display to display the current temperature and humidity. Since both devices use I2C is only uses two pins on the Photon, D0 and D1.

The connections to the BME280 are described below. The SSD1306 connects as follows:

  • VCC to 3V3 red
  • GND to GND black
  • SCL to D1 (SCL) blue
  • SDA to D0 (SDA) green

BME280 with OLED

The sample code is here.

Temperature sensors

There are a number of temperature sensors that work on the Photon. The TMP36 is an analog sensor, connecting to an ADC input on the Photon.

The DS18B20 is a 1-wire temperature sensor, which uses a different protocol than I2C, and uses fewer wires than I2C.

The DHT22 uses a proprietary digital protocol and measures temperature and humidity.

But in this tutorial we'll concentrate on the I2C temperature sensors including the BME280 (and its cousin the BMP280) and also the DS75.

BME280 to Google Spreadsheet

The BME280 is a high-accuracy temperature, humidity and pressure sensor that connects by I2C or SPI. It was ranked best in a sensor shootout but it is more expensive. I got mine from Adafruit.

  • VIN to 3V3 (red) or VIN
  • 3VO no connection
  • GND to GND (black)
  • SCK to D1 (SCL) blue
  • SDO no connection
  • SDI to D0 (SDA) green
  • CS no connection

BME280 Circuit

You can also power the sensor from VIN instead of 3V3. The Adafruit board has built-in pull-up resistors so you don't need them on SDA and SCL.

This example takes the temperature (in °C), pressure (in hPa), and relative humidity (%) and stores it in a Google spreadsheet.

The firmware is easy:

#include "Adafruit_BME280_RK.h"

Adafruit_BME280 bme; // I2C

const unsigned long PUBLISH_PERIOD_MS = 60000;
const char *FIELD_SEPARATOR = "|||";
const char *EVENT_NAME = "tempSensor";

bool sensorReady = false;
unsigned long lastPublish = 0;
char buf[256];

void setup() {
    Serial.begin(9600);

    sensorReady = bme.begin();
}


void loop() { 
    if (millis() - lastPublish >= PUBLISH_PERIOD_MS && sensorReady) {
        lastPublish = millis();

        float temp = bme.readTemperature(); // degrees C
        float pressure = bme.readPressure() / 100.0; // hPa
        float humidity = bme.readHumidity(); // % 

        snprintf(buf, sizeof(buf), "%.02f%s%.02f%s%.01f", temp, FIELD_SEPARATOR, pressure, FIELD_SEPARATOR, humidity);
        Particle.publish(EVENT_NAME, buf, PRIVATE);
    }
}

The full source code is in the bme280-google-sheets folder.

The magic of getting it into a Google Sheet is to use IFTTT. Just create account and a new Applet.

  • Create a new Applet. Click on + this.
  • Step 1: Choose a service: Select Particle.
  • Step 2: Choose a trigger: Select New event published.
  • Complete trigger fields:

IFTTT Trigger

  • Click on + that.
  • Step 3: Choose an action service: Google Drive.
  • Step 4: Choose action: Add row to spreadsheet.
  • Step 5: Complete action fields:

IFTTT Action

And, shortly thereafter, rows will start appearing in your Google Spreadsheet. It even automatically updates the web page view as new rows are added.

Google Sheet

BME280 to Ubidots

Ubidots is a 3rd-party service that makes creating dashboards, graphs, tables, etc. really easy.

Ubidots graph

The sample code is here.

DS75

The DS75 isn't meant to be a super-accurate room temperature sensor like the BME280. It's accurate to ±2°C, though it has a resolution of up to 0.0652°C. It's ideal for measuring the temperature of circuit boards or the temperature inside an enclosure.

It's inexpensive (US$1.90 in single quantities) and tiny (SOIC-8 package), and you can connect 8 of them to a single I2C bus.

You can get the chip from stores such as Digi-Key along with the SparkFun SOIC-8 to DIP Adapter. It takes some careful soldering to solder a SOIC-8 by hand, but it's not bad if you have a good soldering iron after you've done it a few times.

The driver, code examples, and wiring instructions are in the DS75-RK GitHub repository. The library is in the community libraries as DS75-RK.

DS75 Circuit

GPIO Expanders

The Photon includes a large number of digital input and output, or general purpose I/O, GPIO, pins. With some limitations there are 18 on the Photon, even more on the Electron. But sometimes you need more, and an I2C GPIO expanded can be helpful. The MCP23008 is an 8-port GPIO expander.

A library is available for the MCP23008 so it can be used in a way familiar to Particle programmers. You can find the documentation at the link, and its available in the community libraries as MCP23008-RK.

For example:

#include "MCP23008-RK.h"

MCP23008 gpio(Wire, 0);

void setup() {
    Serial.begin(9600);

    gpio.begin();
    gpio.pinMode(0, OUTPUT);
    gpio.digitalWrite(0, HIGH);
}

void loop() {
}

MCP23008 Breadboard

You can connect up to 8x MCP23008 chips to a single I2C bus on the Photon, just in case you need 64 GPIOs!

As it's a bare chip, don't forget to add 4.7K or 10K pull-up resistors on SDA and SCL.

Another nice thing is that you can power the MCP23008 from a 5V supply and it will provide true 5V output GPIO. This is handy if you are interfacing to something that needs true 5V for logic level high.

And if you need even more pins, the MCP23017 has 16 GPIO pins. You can find the documentation at the link, and its available in the community libraries as MCP23017-RK.

ADC (analog to digital converters)

The Photon includes 8 ADC inputs for converting an analog to a digital value, 0-4095 in the case of the 12-bit ADCs in the Photon.

Sometimes you want more, and in that case an I2C ADC may be what you need.

Another reason is that you want to measure a voltage from a very high impedance source. The built-in ADC on the STM32F205 has trouble measuring these sources. Using an ADC with a built-in programmable amplifier can make it easier to measure some voltage sources.

The MCP3021 is a single channel 10-bit I2C ADC. It's inexpensive and tiny (SOT-23-5). It's not clear what applications you might need it for, but just in case here's an example MCP3021 project.

MCP3021 Example

PWM

PWM (pulse-width modulation) outputs are commonly used to drive LEDs as they can adjust the brightness. They're also used for servos to control the position of the servo. While the Photon includes 7 independent PWM outputs, sometimes you need more.

The Adafruit 16-channel 12-bit PCA9685 PWM/servo driver is one way to solve this problem. And since it has 6-bits of I2C addressing, you can even add 62 of these boards, just in case you need 992 PWM outputs (theoretically, at least) on a single I2C port.

FRAM Memory

One interesting thing you can add by I2C is FRAM, or Ferroelectric RAM. This particular device is 32K bytes. The main advantage is that it's non-volatile like EEPROM, so the contents don't go away when you remove power. But it's much faster and doesn't wear out like EEPROM, so it's great if you need to save data frequently.

FRAM Circuit

The example code and library are here: https://github.com/rickkas7/MB85RC256V-FRAM-RK.

Multiplexer: TCA9548A

Once you have all of these I2C devices, what happens if you have address conflicts, or need to run buses at different voltages? One easy solution is to add a TCA9548A.

Adafruit has the surface mount chip soldered on a handy breakout board.

This example, mux-led, uses a TCA9548A to run two separate LED displays, both on address 0x70. There are solder pads on the underside of the LED display controllers that allow you to change the address, but I needed something to demonstrate the TCA9548A.

It's just a matter of setting the channel to select which I2C bus you want to control before you use it. That's it!

mux.begin();

mux.setChannel(0);
sevenSeg.begin(0x70);

mux.setChannel(1);
alphaNum.begin(0x70);

The example code and library are here: https://github.com/rickkas7/TCA9548A-RK.

This is an example of using the TCA9548A to drive two LED displays that use the same I2C address.

mux-led circuit

The code is here

Using the Wire1 port on the Electron

The Electron has two I2C ports, Wire and Wire1. The Wire1 port is on C4 and C5:

  • C4: Wire1 SDA
  • C5: Wire1 SCL

It works the same as the regular port on the Photon, though not all libraries work with it. Most of the Adafruit libraries, for example, assume only 1 I2C port and use that.

Note that the VIN pin on the Electron only supplies 4.8V when powered by USB. When using a battery, the VIN pin is not powered. This will be an issue if you want to power 5V I2C sensors. The usual method is to use an external step-up DC-DC converter. This will boost 3.3V (3V3) to 5V.

In this circuit, I made the following connections:

  • 1 SDA to C4 (green)
  • 2 SCL to C5 (blue)
  • 3 OS to D2 (white)
  • 4 GND to GND (black)
  • 5 A2 to GND (black)
  • 6 A1 to GND (black)
  • 7 A0 to GND (black)
  • 8 VDD to 3V3 (red)

Since this is a bare chip, not a breakout board, I also added 10K pull-up resistors to C4 and C5.

To build and flash over USB, I use the following commands:

cd ds75-electron-wire1
particle compile electron . --saveTo firmware.bin
particle flash --local firmware.bin

And here's the output from serial:

temp 22.500000C 72.500000F
temp 22.500000C 72.500000F
temp 22.500000C 72.500000F

DS75 Wire1

Important note: Even though there are two sets of pins, for all practical purposes you can't use both Wire and Wire1 at the same time. It's not like having two separate I2C interfaces, because they're connected to the same I2C block in the STM32F205 processor, I2C1, and if you try to initialize both, weird things happen. If you need to connect to multiple I2C buses, use a TCA9548A instead.

Sparkfun Qwiic

Sparkfun has created a whole line of accessories including:

  • Environment sensors (pressure, temperature, humidity) like the BME280
  • Buttons and indicator buttons
  • Load cell adapter (weight sensor)
  • Distance and proximity sensors
  • Thermocouple adapters
  • Relays
  • Keypads
  • Small displays

Each of the devices is a small board with two Qwiic connectors, tiny JST 1mm-pitch 4-pin connectors. The connector is keyed so you don't need to worry about connecting it backwards, and the devices can be daisy-chained, one after the other.

Here's a BME280 temperature, humidity, and atmospheric pressure sensor board, pictured next to a US quarter coin:

BME280

You can find more on the Qwiic community page.

NCD/Control Everything

NCD makes a large number of sensors boards for the Photon and Electron. For I2C sensors, one of the nice things is that the boards connect with 4-conductor locking ribbon cables, so you can use them as-is in permanent installation.

ControlEverything

The boards can be chained together, and there are also boards that hold the Electron and Photon that have screw terminals, in case you need something like that, as well.

I2C Scanner

If you're having trouble interacting with an I2C device the I2C scanner firmware may be helpful. It's in the i2c-scanner directory. If you wanted to flash it to the device "test2" you'd use these commands:

cd i2c-scanner
particle flash test2 .

And the output would be this if you had a BME280 connected to the I2C bus:

I2C Scanner
Scanning...
I2C device found at address 0x77  !
done

The source code is here

Using Saleae logic analyzer

If you're really getting into working and debugging I2C, a great thing to have is a Saleae Logic, a USB-based logic analyzer.

Logic

Not only can it display the waveforms, but it can also decode I2C, making it much easier to debug problems.

Logic with Circuit

I2C Slave mode

In all of the examples above, the Photon/Electron was the I2C Master Device. It's also possible to use it as a slave device. You might do this if you're connecting two Particle devices together using I2C, or maybe if you were connecting a Photon or Electron to a Raspberry Pi.

The I2CSlaveRK library makes it easy to turn a Photon into something that behaves like many I2C slave devices. You configure how many "registers" it has, and either side can set or get the 32-bit values in these registers.

The values are kept in the I2C slave Photon/Electron, and the master device queries these values in same way most I2C devices work. It can either get or set the registers.

Additionally, the slave can find out if the master has set any registers recently. This can be used to have the master trigger an action in the slave by writing to a register.

I2C Slave Mode

The example code and library are here https://github.com/rickkas7/I2CSlaveRK.

Raspberry Pi Master, Photon I2C Slave

It's possible to make a Raspberry Pi the I2C Master and use a Photon/Electron as an I2C slave. This example uses direct I2C from Raspberry Pi C++ code.

It may be necessary to enable I2C mode in the Linux kernel configuration of your Pi.

Remember that you need pull-up resistors on SDA and SCL! 10K or 4.7K resistors to 3V3 should work. Connect SDA, SCL, and GND between the Pi and Photon (or Electron).

Raspberry Pi

Once you have the connections made, use the i2cdetect program to see if the Photon slave can be found. It should have address 0x10, like this:

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: 10 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- -- 

This is the code that interacts with the I2C slave example above. This code reads register 0, which increments every second in the slave code.

#include <errno.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  int file;
  int addr = 0x10;

  // Code adapted from:
  // http://elinux.org/Interfacing_with_I2C_Devices

  if ((file = open("/dev/i2c-1",O_RDWR)) < 0) {
    printf("Failed to open the bus.");
    exit(1);
  }

  if (ioctl(file,I2C_SLAVE,addr) < 0) {
    printf("Failed to acquire bus access and/or talk to slave.\n");
    exit(1);
  }

  char buf[6];
  buf[0] = buf[1] = 0;

  if (write(file, buf, 2) != 2) {
    printf("Failed to write to the i2c bus.\n");
    exit(1);
  }

  if (read(file, buf, 4) != 4) {    
    printf("Failed to read from the i2c bus.\n");
    exit(1);
  }

  printf("reg0=%ld\n", *(unsigned long*)buf);

  return 0;
}

To run it:

gcc i2ctest.cpp 
sudo ./a.out