Skip to main content

I2C Explained

An Introduction To The Standard Device-to-device Bus

The Inter-Integrated Circuit (I2C) bus is a chip-level serial communications mechanism that operates over just two wires. Some developers pronounce the bus’ name eye-two-see, others eye-squared-see, but both refer to the same thing. Devised by Philips in the early 1980s, I²C was established to standardize communications between the company’s chips, but it has since become a de facto standard supported by many microcontroller devices from Arduino boards to the Raspberry Pi and, of course, imps.

The Physical Bus

I²C itself comprises two wires. One I²C line transmits data, the other the clock signals that synchronize the conversation between devices. The data line is called ‘SDA’, the clock line ‘SCL’.

Typically, both SDA and SCL are each connected to a 3.3 or 5V power line through a single ‘pull-up’ resistor, one on each line. This is necessary because devices’ SDA and SCL connections are ‘open drain’ lines: they can force the voltage on the line to 0V, or ‘low’, but can’t raise it to 3.3V, or ‘high’. High and low are the electrical representations of the 1s and 0s that are the fundamental components of digital information. Adding these two resistors — and the bus needs only two, no matter how many devices are connected to it — ensures the voltage rises back to 3.3V without a short circuit.

I²C insists devices have open drain lines to ensure no component-harming high currents are able to flow when two devices try and signal simultaneously.

In most cases, you will be expected to add these resistors yourself, but some devices, typically those that operate at 3.3V, include them in order to be compatible with devices supplying 5V. Remember, you only need a pair of pull-up resistors per bus, so it may be necessary to remove pull-up resistors attached to other devices on the bus. Though the imp has internal pull-up resistors of its own, these are too weak to be useful for I²C and so they are automatically disabled when its pins are set to handle I²C signals.

Controllers And Peripherals

The I²C bus separates devices into ‘controllers’ and ‘peripherals’. Only one device can send out timing pulses on the SCL line at a time, and that’s the one chosen to be the controller. All the others synchronize their timings to the controller, and are thus considered peripherals. The controller — typically the imp — and its peripherals can all transmit and receive data, but only the controller can tell a peripheral when to transmit data back.

Addressing

In order for one I²C device to communicate with another on a one-to-one basis, both devices need to be uniquely identifiable. This identity is the device’s I²C address. I²C addresses are usually 7-bit numbers, so a bus can comprise up to 127 devices in all. A byte comprises eight bits; the extra bit is used to indicate whether the signal is being sent by the controller to the peripheral — a ‘write’ — or in the other direction — a ‘read’. This eighth bit is actually bit zero in the address byte sent out onto the bus. The 7-bit address is placed in bits one through seven of the address byte.

In keeping with this format, the imp API takes an I²C addresses as an 8-bit value. Device suppliers usually give their products’ addresses as a 7-bit number, so it’s necessary to convert the address from seven bits to eight. This is a achieved with Squirrel’s << operator, which moves a number’s individual bit values to the left. This process is the equivalent of multiplying by two. In code, this process looks like this:

local sevenBitAddress = 0x39;
local eightBitAddress = sevenBitAddress << 1;

Squirrel automatically sets bit zero to the correct I²C-defined value: 0 for a write operation, 1 for a read. Now you’re ready to use one of the imp’s i2c methods to write data to the bus:

i2c.write(eightBitAddress, dataInStringForm);

The 7-bit address of the device to which you want your imp to communicate with will be supplied by the component’s manufacturer and listed on the device’s datasheet. It may not be fixed, but selected from a range of addresses according to the voltage applied to another of the devices pins. For example, a TAOS TSL2561 light sensor has three possible addresses: 0x29, 0x49 or 0x39, depending on whether its ADDR pin is fixed at 0V, 3.3V or ‘floating’ between the two. We say the value is floating because it has not been actively selected to operate at a specific voltage; it could be anything from 0 to 3.3V inclusive.

Signalling

The I²C bus’ controller uses a peripheral’s 7-bit address to identify the component it wants to talk to. In fact, the signalling is more complex than that, but fortunately all the details are handled by the imp so that you need only supply the address as an 8-bit value.

If you’re writing to the peripheral, you also need to supply the data to write, which often includes register values which instruct the peripheral what to do with the data. The imp API documentation refers to these registers as ‘sub-addresses’:

i2c.write(eightBitAddress, dataString);

write() requires data in string form. Consequently, you may need to convert the value stored in other types of variable into string form.

Reading information from a device may require a command to tell the device which data to fetch. Telling the imp to read data from the I²C bus also involves providing a third parameter, the number of bytes you expect to receive:

i2c.read(eightBitAddress, controlDataString, numberOfBytesToRead);

Behind these operations are the I²C’s electrical signals, applied to the SDA line synchronized with the timing pulses applied to the SCL line. Writing to the bus involves a start marker: dropping SDA to 0V while SCL is 3.3V. Changing the SDA voltage when the SCL’s voltage is high defines start and stop markers. If the SDA voltage doesn’t change while SCL is high, I²C devices know that data, rather than markers, is being sent.

SDA now goes high or low to send out each bit of the address byte: the 7-bit device address followed by the read/write bit. The byte’s bits are sent out left-most bit — the ‘most significant bit’ — first, with SDA going high if the bit’s value is 1 or low if it is zero. The target peripheral will now pull SDA low to signal acknowledgement of receipt of the data, and then out goes eight bits of control information or data, followed by more data if necessary. There is an single-pulse ‘ack’ acknowledgement pause on SDA between every eight bits sent, timed to a ninth SCL pulse. If the peripheral doesn’t acknowledge receipt this way, the controller will detect that SDA has remained high and will signal an error.

When data flows from the peripheral to the controller, the latter likewise acknowledges the receipt of eight bits by pulling SDA low on the ninth SCL pulse unless this is the final byte of a batch, in which case the controller does not pull SDA low — it makes a ‘nak’ signal, or ‘no acknowledgement’ — in order to let the peripheral know it has finished.

When we’re done, SDA goes high as a stop marker.


The start of transmission is indicated by SDA dropping from High to Low voltage (the rectangle on the left),
stop by the reverse (the right rectangle). SCL must be High when this takes place

Timing

The standard clock speed for I²C communications is 100kHz — 100,000 SCL pulses per second. It’s possible to go faster, up to 400kHz. Some devices may not be able to support this speed; check the datasheet that accompanies the device you want to connect to your imp. However, many slow devices use a technique called ‘clock stretching’ to force faster devices to work to their speed. The imp supports devices that make use of this technique. Essentially, they hold SCL low while they are fetching the data you want them to send to the imp. The imp detects this, releases the SCL line and waits until SCL goes high again before continuing.

However, you may need to lower the I²C speed yourself if the electrical characteristics of your set-up slow down the speed of the transition between 0V and 3.3V, called the ‘rise time’. This is often caused by long wires, which increase the capacitance of the circuit. In order for the devices to successfully detect the transmission of each bit, the bus needs to run more slowly. Data corruption or unexpected results are the clues you should look out for. Reduce the I²C bus speed until the data is being successfully read.

The imp API currently provides four pre-defined clock values: 10, 50 100 and 400kHz. They are selected by passing a constant to the I²C configuration method as a parameter:

i2c.configure(speedConstant);

where the value of speedConstant is one of

  • CLOCK_SPEED_10_KHZ
  • CLOCK_SPEED_50_KHz
  • CLOCK_SPEED_100_KHZ
  • CLOCK_SPEED_400_KHZ

Setting Up An imp For I²C

The i2c object in the example lines of code given above is not provided directly by the imp, but chosen by you according to which of your chosen imp’s pins you’ll be using for I²C communications. Each type of imp has multiple I²C buses, all made available at start-up. Check out the pin mux for the type of imp you are using to see which I²C objects are available to you. Here, we’ll assume you are using an imp001. The imp001’s two I²C buses are on pins 1 and 2, and pins 8 and 9, respectively instanced as properties i2c12 and i2c89 of the hardware object when the imp starts up. Pins 1 and 8 are assigned to SCL, 2 and 9 to SDA.

It is commonplace to reference your choice with a global variable:

i2c <- hardware.i2c12;
i2c.configure(CLOCK_SPEED_100_KHZ);

Example Code

The following code works with the TAOS TSL2561 visible and infrared light sensor, a 3.3V device that uses I²C to communicate with its host microcontroller. The chip’s datasheet can be downloaded from the Adafruit website. Adafruit sells the chip on a low-cost breakout board which includes suitable pull-up resistors on the power pin, VCC. This means it’s ready to connect directly to an imp’s I²C pins.


Adafruit’s TSL2561 breakout board

Note After this article was written, Adafruit updated its TSL2561 sensor board. The new version works with the current code.

Here is the code, for the agent and then the device:

What The Code Does

The agent code responds to an incoming HTTP request by notifying the device that it requires a reading. For simplicity, the reading is simply displayed in the log.

The device code reads the sensor and calculates a lux value according to the math set down in the TSL2561 datasheet. The first part of the program sets up constants for the TSL2561’s key registers and settings, including its I²C address options. Other constants are used in the luminosity conversion process.


Wiring up the imp

At the program start point, the code aliases one of the imp’s I²C pin set as a global variable, configures the bus speed to 100kHz and then shifts the TSL2561’s I²C address one bit to the left so it’s ready to be used with the imp I²C functions. It then sets up the sensor. Just to check this has worked, we read the control register: the return value should be 3 if the TSL2561 has just started up, or 51 if the device has already been switched on.

Next, the code sets the sensor’s ADC resolution through the TSL2561’s timing register, and then sets the signal gain to high.

Finally, we tell the imp how to respond to "sense" notifications from the agent. This calls a function which reads the TSL2561’s two light sensors, digital values for which are stored in the chip’s four 8-bit ADC registers. The first two give a 16-bit combined optical and Infra-Red reading, the second pair a 16-bit IR-only value. The functions readSensorAdc0() and readSensorAdc1() convert the individual register values into 16-bit numbers by multiplying the most-significant byte by 256 and adding the value of the least-significant byte. The multiplication is performed by the shifting the eight-bit number’s bits left eight places with Squirrel’s << operator.

The code then provides both of these readings to a third function to calculate a final luminosity, ‘lux’ value.

Further Reading

  • I²C Errors — How to debug I²C read and write problems