I2C on the Raspberry Pi

 

I2C is s standard means to communicate between integrated circuits, and is used by microcontrollers to talk to integrated circuits or other devices. It is a 2-wire bus, consisting of a clock signal (SCL) and a data signal (SDA). It can be used to interface the Raspberry Pi to LCD displays, memory or realtime clock chips, or to other microcontrollers, such as the Arduino's ATmega328.

Raspberry Pi Configuration

The I2C ports on the Raspberry Pi are disabled in the kernel by default. To enable them, you need to run raspi-config and turn on kernel support. It can be found in the "Interfacing Options" section.

raspi-config I2C configuration raspi-config I2C configuration raspi-config I2C configuration

You will be prompted to reboot by raspi-config. Do it to install the kernel I2C module.

Once the kernel I2C support is enabled, you need to install a utility that allows you to verify I2C addresses of your peripheral devices.

sudo apt-get install i2c-tools

Now you need to attach your I2C device. Before you do, shutdown and power off the Raspberry Pi.

sudo shutdown -h now

The pins involved are SDA on pin 3 and SCL on pin 5 of the GPIO connector. Connect your I2C device to the pins, either directly (if it is a 3.3V device) or through a level shifter (if it is a 5V device).

The Raspberry Pi is a 3.3V device

You must not connect a 5V peripheral device to the GPIO header! The inputs on the processor are not 5V tolerant, and they could be damaged by the resulting excessive current.

An I2C LCD connected to the Raspberry Pi

In this example, we connect a 5V LCD display to the Raspberry Pi through a Sparkfun level shifter breakout. The breakout just goes between the Raspberry Pi and the LCD in the SDA and SCL lines. The "LV" pin on the breakout connects to the 3.3V line on the RPi (pin 1). The "HV" pin connects to the 5V line (pin 4) and "GND" connects to the RPi ground (pin 6).

The SCL line (pin 3) from the Raspberry Pi goes to the "LV1" pin on the level shifter and the SDA line (pin 5) goes to the "LV2" pin. The LCD signals connect to the high voltage side. SCL is "HV1" and SDA is "HV2".

To check your work, boot the Rasberry Pi and run i2cdetect on the I2C port. The port number will be either 0 or 1, depending on the model of Raspberry Pi you have. It doesn't hurt anything to run it against the wrong port. This unit is a Model 3B, which assigns port 1 to the GPIO header pins 3 and 5.

An I2C LCD connected to the Raspberry Pi
pi@raspberrypi-dev:~ $ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: 20 -- -- -- -- -- -- 27 -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                    

It finds two LCDs at addresses 0x20 and 0x27, right where they belong. I have two LCDs - one 16 x 2 and one 20 x 4. They each have different backpacks, and each backpack has a different pinout on the PCF8574 I2C to parallel chip. With a little work and an oscilloscope I was able to determine which pin was which on the backpacks, and I made two LCD libraries - one for each.

The temperature/humidity sensor is on the 3.3V I2C bus directly connected to the Raspberry Pi, while the two LCD displays are on the 5V I2C bus created by the level shifter.

Two I2C LCDs connected to the Raspberry Pi

You can have multiple displays on the Raspberry Pi at the same time as long as they have different I2C addresses. You will need to make some changes to the code, something like this:

import modules.lcd_device_a as lcd_device_a
import modules.lcd_device_b as lcd_device_b

mylcd16 = lcd_device_a.lcd(0x27)
mylcd20 = lcd_device_b.lcd(0x20)
            

That assumes one LCD uses the lcd_device_a module, the other uses the "b" module, and are addressed at 0x20 and 0x27. Adjust to suit your needs. They can both be the same code as long as they are different addresses. Don't use this code with multiple threads or you may have a collision in the I2C hardware from two threads talking at the same time.

No LCD display is complete without something to display, so I wired up an HTU21D temperature/humidity sensor to get some interesting data. It can be seen at address 0x40, above. I have also included a module for interfacing to the HTU21D.

CompuLab Fit-Headless Display Emulator

When you want to operate a Raspi headless, you might want to get into it using VNC, to give you a remote desktop on your PC. The display on the Raspberry Pi defaults to 640 x 480 pixels if it can't find a display. To get around this, and provide a 1080p desktop, CompuLab makes a display emulator that fools the Pi into thinking it has a monitor. You can find them on Amazon (link below). I use them on a couple of Mac Minis and a Raspberry Pi, and couldn't get by without them.

CompuLab Fit-Headless Display Emulator (Amazon)

The code is written in python 3.5, since that is what was on the Raspberry Pi. You can download the source files at the bottom of this page.

import os
import sys
from time import sleep

root = os.path.dirname(os.path.realpath(__file__))
sys.path.append(root + '/modules')
print(sys.path)

import modules.i2c_device as i2c_device
import modules.htu21d as htu

# Select the LCD driver appropriate to your I2C backpack
import modules.lcd_device_b as lcd_device

mylcd = lcd_device.lcd(0x20)

mylcd.lcd_display_string("RPi I2C Weather", 1)
mylcd.lcd_display_string("", 2)
sleep(2)
mylcd.lcd_clear()

ht = htu.Htu21d()

while True:
    dspstr1 = '{:6.2f}C {:6.2f}F'.format(ht.read_temperature(celsius=True), ht.read_temperature(celsius=False))
    dspstr2 = '    {:6.2f}%'.format(ht.read_humidity())
    mylcd.lcd_display_string_pos(dspstr1, 1, 0)
    mylcd.lcd_display_string_pos(dspstr2, 2, 0)
    sleep(5)

The HTU21D module looks like:

import i2c_device
import struct
from time import sleep

COMMAND_SOFT_RESET          = 0xFE
COMMAND_TRIGGER_TEMPERATURE = 0xF3
COMMAND_TRIGGER_HUMIDITY    = 0xF5
DELAY_RESET                 = 0.02
DELAY_TEMPERATURE           = 0.05
DELAY_HUMIDITY              = 0.02
LSBMASK                     = 0b11111100


class Htu21d:
    """ HTU21D class interfaces to the sensor and provides
        methods for reading the temperature and humidity.
    """
    def __init__(self):

        self.i2c = i2c_device.i2c(0x40)

    def read_temperature(self, celsius=True):
        """ Trigger the temperature measurement, then read the result """
        
        try:
            self.i2c.write_cmd(COMMAND_TRIGGER_TEMPERATURE)
            sleep(DELAY_TEMPERATURE)
            vals = self.i2c.read_count(3)
            (temphigh, templow, crc) = struct.unpack('BBB', vals)
            temp = (temphigh << 8) | (templow & LSBMASK)
            deg_c = -46.85 + (175.72 * temp) / 2 ** 16
            if celsius:
                return deg_c
            else:
                return deg_c * (9/5) + 32

        except:
            return False
            
    def read_humidity(self):
        """ Trigger the humidity measurement, then read the result """
        
        try:
            self.i2c.write_cmd(COMMAND_TRIGGER_HUMIDITY)
            sleep(DELAY_HUMIDITY)
            vals = self.i2c.read_count(3)
            (humidhigh, humidlow, crc) = struct.unpack('BBB', vals)
            humid = (humidhigh << 8) | (humidlow & LSBMASK)
            return (-6 + (125.0 * humid) / 2**16)

        except:
            return False

Then there is the I2C module, which just packages the stream handling in a more byte-oriented way.

import fcntl
import io
from time import *

FCNTL_SLAVE                 = 0x0703


class i2c:
    def __init__(self, addr, port=1):
        self.addr = addr
        self.iord = io.open('/dev/i2c-' + str(port), 'rb', buffering=0)
        self.iowr = io.open('/dev/i2c-' + str(port), 'wb', buffering=0)
        fcntl.ioctl(self.iord, FCNTL_SLAVE, self.addr)
        fcntl.ioctl(self.iowr, FCNTL_SLAVE, self.addr)

    # Convert an int to a byte buffer
    def toBytes(self, val):
        b = bytes([val,])
        return b
        
    # Write a single command
    def write_cmd(self, cmd):
        c = self.toBytes(cmd)
        self.iowr.write(c)
        sleep(0.0001)

    # Read n bytes
    def read_count(self, count):
        return self.iord.read(count)
    
    # Read a single byte
    def read(self):
        return self.iord.read(1)

And the two LCD modules. These could be made into one module, but it would be complex and hard to read. For this article, it is best to have two simple modules. The main difference is in the translation of bit to pin. One has the data lines on the high nibble and the other has them on the low nibble. This makes the byte to nibble conversion in 'lcd_write()' and 'lcd_write_char()' different for each.

# LCD backpack V1
# 7 D7
# 6 D6
# 5 D5
# 4 D4
# 3 Backlight (0=off, 1=on)
# 2 Enable
# 1 RW
# 0 RS

import i2c_device
from time import *

# commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80

# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00

# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00

# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00

# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

# flags for backlight control
#LCD_BACKLIGHT = 0x08
LCD_BACKLIGHT = 0x08
LCD_NOBACKLIGHT = 0x00

En = 0b00000100  # Enable bit
Rw = 0b00000010  # Read/Write bit
Rs = 0b00000001  # Register select bit


class lcd:
    # initializes objects and lcd
    def __init__(self, address):
        self.lcd_device = i2c_device.i2c(address)

        self.lcd_write(0x03)
        self.lcd_write(0x03)
        self.lcd_write(0x03)
        self.lcd_write(0x02)

        self.lcd_write(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_4BITMODE)
        self.lcd_write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)
        self.lcd_write(LCD_CLEARDISPLAY)
        self.lcd_write(LCD_ENTRYMODESET | LCD_ENTRYLEFT)
        sleep(0.2)

    # clocks EN to latch command
    def lcd_strobe(self, data):
        self.lcd_device.write_cmd(data | En | LCD_BACKLIGHT)
        sleep(.0005)
        self.lcd_device.write_cmd(((data & ~En) | LCD_BACKLIGHT))
        sleep(.0001)

    def lcd_write_four_bits(self, data):
        self.lcd_device.write_cmd(data | LCD_BACKLIGHT)
        self.lcd_strobe(data)

    # write a command to lcd
    def lcd_write(self, cmd, mode=0):
        self.lcd_write_four_bits(mode | (cmd & 0xF0))
        self.lcd_write_four_bits(mode | ((cmd << 4) & 0xF0))

    # write a character to lcd (or character rom) 0x09: backlight | RS=DR<
    # works!
    def lcd_write_char(self, charvalue, mode=1):
        self.lcd_write_four_bits(mode | (charvalue & 0xF0))
        self.lcd_write_four_bits(mode | ((charvalue << 4) & 0xF0))

    # put string function
    def lcd_display_string(self, string, line):
        if line == 1:
            self.lcd_write(0x80)
        if line == 2:
            self.lcd_write(0xC0)
        if line == 3:
            self.lcd_write(0x94)
        if line == 4:
            self.lcd_write(0xD4)

        for char in string:
            self.lcd_write(ord(char), Rs)

    # clear lcd and set to home
    def lcd_clear(self):
        self.lcd_write(LCD_CLEARDISPLAY)
        self.lcd_write(LCD_RETURNHOME)

    # define backlight on/off (lcd.backlight(1); off= lcd.backlight(0)
    def backlight(self, state):  # for state, 1 = on, 0 = off
        if state == 1:
            self.lcd_device.write_cmd(LCD_BACKLIGHT)
        elif state == 0:
            self.lcd_device.write_cmd(LCD_NOBACKLIGHT)

    # add custom characters (0 - 7)
    def lcd_load_custom_chars(self, fontdata):
        self.lcd_write(0x40);
        for char in fontdata:
            for line in char:
                self.lcd_write_char(line)

                # define precise positioning (addition from the forum)

    def lcd_display_string_pos(self, string, line, pos):
        if line == 1:
            pos_new = pos
        elif line == 2:
            pos_new = 0x40 + pos
        elif line == 3:
            pos_new = 0x14 + pos
        elif line == 4:
            pos_new = 0x54 + pos

        self.lcd_write(0x80 + pos_new)

        for char in string:
            self.lcd_write(ord(char), Rs)
# LCD backpack V2
# 7 Backlight (0=on, 1=off)
# 6 RS
# 5 RW
# 4 EN
# 3 D7
# 2 D6
# 1 D5
# 0 D4

import i2c_device
from time import *

# commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80

# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00

# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00

# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00

# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

# flags for backlight control
#LCD_BACKLIGHT = 0x08
LCD_BACKLIGHT = 0x00
LCD_NOBACKLIGHT = 0x80

En = 0b00010000  # Enable bit
Rw = 0b00100000  # Read/Write bit
Rs = 0b01000000  # Register select bit

class lcd:
    # initializes objects and lcd
    def __init__(self, address):
        self.lcd_device = i2c_device.i2c(address)

        self.lcd_write(0x03)
        self.lcd_write(0x03)
        self.lcd_write(0x03)
        self.lcd_write(0x02)

        self.lcd_write(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_4BITMODE)
        self.lcd_write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)
        self.lcd_write(LCD_CLEARDISPLAY)
        self.lcd_write(LCD_ENTRYMODESET | LCD_ENTRYLEFT)
        sleep(0.2)

    # clocks EN to latch command
    def lcd_strobe(self, data):
        self.lcd_device.write_cmd(data | En | LCD_BACKLIGHT)
        sleep(.0005)
        self.lcd_device.write_cmd(((data & ~En) | LCD_BACKLIGHT))
        sleep(.0001)

    def lcd_write_eight_bits(self, data):
        self.lcd_device.write_cmd(data)

    def lcd_write_four_bits(self, data):
        self.lcd_device.write_cmd(data | LCD_BACKLIGHT)
        self.lcd_strobe(data)

    # write a command to lcd
    def lcd_write(self, cmd, mode=0):
        self.lcd_write_four_bits(mode | ((cmd >> 4) & 0x0F))
        self.lcd_write_four_bits(mode | (cmd & 0x0F))

    # write a character to lcd (or character rom) 0x09: backlight | RS=DR<
    # works!
    def lcd_write_char(self, charvalue, mode=1):
        self.lcd_write_four_bits(mode | ((charvalue >> 4) & 0x0F))
        self.lcd_write_four_bits(mode | (charvalue & 0x0F))

    # put string function
    def lcd_display_string(self, string, line):
        if line == 1:
            self.lcd_write(0x80)
        if line == 2:
            self.lcd_write(0xC0)
        if line == 3:
            self.lcd_write(0x94)
        if line == 4:
            self.lcd_write(0xD4)

        for char in string:
            self.lcd_write(ord(char), Rs)

    # clear lcd and set to home
    def lcd_clear(self):
        self.lcd_write(LCD_CLEARDISPLAY)
        self.lcd_write(LCD_RETURNHOME)

    # define backlight on/off (lcd.backlight(1); off= lcd.backlight(0)
    def backlight(self, state):  # for state, 1 = on, 0 = off
        if state == 1:
            self.lcd_device.write_cmd(LCD_BACKLIGHT)
        elif state == 0:
            self.lcd_device.write_cmd(LCD_NOBACKLIGHT)

    # add custom characters (0 - 7)
    def lcd_load_custom_chars(self, fontdata):
        self.lcd_write(0x40);
        for char in fontdata:
            for line in char:
                self.lcd_write_char(line)

                # define precise positioning (addition from the forum)

    def lcd_display_string_pos(self, string, line, pos):
        if line == 1:
            pos_new = pos
        elif line == 2:
            pos_new = 0x40 + pos
        elif line == 3:
            pos_new = 0x14 + pos
        elif line == 4:
            pos_new = 0x54 + pos

        self.lcd_write(0x80 + pos_new)

        for char in string:
            self.lcd_write(ord(char), Rs)

Arduino Board Logo

 

Arduino-Board is the go-to source for information on many available Arduino and Arduino-like boards, tutorials and projects.

Help and Support

Arduino-Board

Stay updated

Sign up if you would like to receive our once monthly newsletter.