Pi IoT In Python Using Linux Drivers - I2C
Written by Harry Fairhead & Mike James   
Monday, 01 November 2021
Article Index
Pi IoT In Python Using Linux Drivers - I2C
HTU21
Processing The Data
The Complete Program

I2C is an important interface for many devices and  Linux has a driver that means that you can use it worrying about the exact details of the hardware - if you know how.

This content comes from our newly published book:

Raspberry Pi IoT In PythonUsing Linux Drivers
Second Edition

By Harry Fairhead & Mike James

DriverPython2e360

Buy from Amazon.

Contents

  1.  Choosing A Pi For IoT
  2.  Getting Started With Python
  3.   Drivers: A First Program
  4.  The GPIO Character Driver ***NEW!!
  5.  GPIO Using I/O Control
  6.  GPIO Events
  7.  The Device Tree
       Extract: The DHT22
  8.  Some Electronics
  9.  Pulse Width Modulation
       Extract: PWM *
  10. SPI Devices
  11. I2C Basics
       Extract: I2C *
  12. The I2C Linux Driver
  13. Advanced I2C
  14. Sensor Drivers
  15. 1-Wire Bus
       Extract 1-Wire And The DS18B20 *
  16. Going Further With Drivers
  17. Appendix I

*From the first edition waiting for update.

 <ASIN:B0CT46R6LF>

There are many specific device drivers which make use of the I2C bus and, as in the case of the SPI bus, there is usually a choice of hand-coding the interaction using the I2C driver or using the specific device driver. In this chapter we will look at the basics of making use of the driver for BSC1 which works on all versions of the Pi.

Enabling The Driver

To make use of the Linux I2C driver you have to enable it by adding dtparam=i2c_arm=on to the /boot/config.txt file.

Alternatively you can load it dynamically:

def checkI2CBus():
    temp = subprocess.Popen(["sudo", "dtparam", "-l"],
                              stdout = subprocess.PIPE) 
    output = str(temp.communicate())
    print(output)
    lasti2c=output.rfind("i2c_arm")
    if lasti2c!=-1:
        lasti2c=output.find("i2c_arm=on",lasti2c)
    if lasti2c==-1:
        temp = subprocess.Popen(["sudo", 
"dtparam", "i2c_arm=on"], stdout = subprocess.PIPE) output = str(temp.communicate()) return

This is slightly different to the earlier driver loading functions. The first part of the function uses dtparam -1 to get a list of loaded overlays. If it finds ic2_arm=on as the last ic2_arm overlay then it does nothing. If it doesn’t find it then it activates the overlay. As you can also use ic2_arm=off and it is the last overlay that controls the state of the system we need to find the last occurrence of ic2_arm and make sure it is “=on”.

Both actions enable BSC1 and create a device file:

/dev/i2c-1

You can check that the driver has been installed using:

ls /dev/i2c*

which will return a list of I2C devices.

Using The I2C Driver From Python

As is the case for all Linux devices, the I2C device, /dev/i2c-x where x is the I2C bus number, looks like a file. You can do a block read by simply opening the file for reading and reading a list of bytes:

import io
fdr = io.open("/dev/i2c-1", "rb", buffering=0)
data=fdr.read(n)

This reads a maximum of n bytes of data and returns it as a list of bytes. The only problem is how do you specify the address of the device? This looks complicated but it isn’t. You need to use standard Linux ioctl functions to send a command to the device. In the case of the I2C driver, the most important is:

I2C_SLAVE=0x0703

This is used to set the address of the slave that subsequent read and writes apply to. So to set the address of the device you want to read from to 0x40 you would use:

import fcntl
fcntl.ioctl(fdr, I2C_SLAVE, 0x40)

Finally to reset the hardware and return all GPIO lines to their default modes you have to close the file:

fdr.close()

Putting all of this together, a block read is:

import io
import fcntl
I2C_SLAVE=0x0703
fdr = io.open("/dev/i2c-1", "rb", buffering=0)
fcntl.ioctl(fdr, I2C_SLAVE, 0x40)
data=fdr.read(n)
fdr.close()

By default bits not are sent between each byte read.

Writing data follows the same pattern:

import io
import fcntl
I2C_SLAVE=0x0703
fdw = io.open("/dev/i2c-1", "wb", buffering=0)
fcntl.ioctl(fdw, I2C_SLAVE, 0x40)
fdw.write( bytearray([0xE7,0x01,0x02]))
fdw.close()

As in the case of a read, a stop bit is only sent at the end of the block of data.

If you try these programs out you will discover that the I2C clock frequency is the default 100kHz. You can’t change the clock frequency dynamically, but you can add:

dtparam=i2c_arm=on,i2c_arm_baudrate=10000

to the /boot/config.txt file and after a reboot the I2C clock will be set to the frequency specified as baudrate. The baud rate is simply the clock speed in Hz.

Notice that the I2C clock speed depends on the core clock rate and this can be slower than the maximum possible when the device is idling or under heat pressure. To run at the fastest clock speed even when idling, use the command:

sudo sh -c "echo performance >
  /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"

If you find using two file objects irritating then you can use the os module to open a Linux device descriptor file. For example, in:

import os
import fcntl
I2C_SLAVE=0x0703
i2cfd = os.open("/dev/i2c-1", os.O_RDWR | os.O_SYNC)
fcntl.ioctl(i2cfd, I2C_SLAVE, 0x40)
data=os.read(i2cfd,n)
os.write(i2cfd,data)

reading and writing is performed on a single file.



Last Updated ( Wednesday, 23 November 2022 )