Using RaspberryPis I2C from Python

29 Mar 2024 - tsp
Last update 29 Mar 2024
Reading time 7 mins

This short blog article summarizes a way to utilize the $I^2C$ bus on the RaspberryPi (or any other platform supported by the iic and iicbus devices). It is based on Vadim Zaigrins excellent blog article, my own blog article about I2C communication between RaspberryPi and AVR based I2C slaves and the source code of the i2c(8) utility. I use this approach personally since I tend to use Python to test various interactions with hardware devices upfront to learn how they work and how to apply different algorithms with them before doing a proper implementation in a proper programming language like C - or interfacing them with microcontrollers. The following solution is based on Pythons fcntl.ioctl interface to stay as close to the system API as possible (i.e. not using any intermediate libraries that I personally have not written every line of code) so translation to native implementations is then easy.

How to use the ioctl interface

As described in the blog posts linked above the iic devices support a variety of ioctls to transmit and receive data - unfortunately due to some implementation issues on the RaspberryPi only the I2CRDWR ioctl can be used - and read/write calls are not supported.

The structures

There are two structures that are going to be used:

struct iic_msg {
   uint16_t  slave;
   uint16_t  flags;
   uint16_t  len;
   uint8_t*  buf;
};

struct iic_rdwr_data {
   struct iic_msg* msgs;
   uint32_t        nmsgs;
};

The bus is exposed via /dev/iic? devices. On the RaspberryPi depending on it’s configuration one finds either one or two exposed I2C busses. One has to open the device in read/write mode. The I2CRDWR syscall accepts a pointer to the struct iic_rdwr_data structure that contains a pointer to one or more struct iic_msg structures as well as their number - a typical way to realize variable sized arrays in C. The struct iic_msg structures specify the slave address, a flag field that allows one to choose between read and write mode, a buffer length as well as the input or output buffer.

First let’s use ctypes to translate the structures to Python classes:

import fcntl
import struct
import os
import ctypes
class IICMsg(ctypes.Structure):
    _fields_ = [
        ( "slave", ctypes.c_uint16 ),
        ( "flags", ctypes.c_uint16 ),
        ( "len"  , ctypes.c_uint16 ),
        ( "buf"  , ctypes.c_void_p )
    ]

class IICRdwrData(ctypes.Structure):
    _fields_ = [
        ( "msgs" , ctypes.POINTER(IICMsg) ),
        ( "nmsgs", ctypes.c_uint32 )
    ]

Since we’re also going to need read-after-write transactions which require two IICMsg structures referenced by the msgs field I’m also going to define a wrapper structure to make life way easier:

class IICMsg2(ctypes.Structure):
    _fields_ = [
        ( "m1", IICMsg ),
        ( "m2", IICMsg )
    ]

The ioctl number

The only missing part is the numeric value representing the define of the ioctl call. To determine this one I simply used a C program:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <dev/iicbus/iic.h>>

int main(int argc, char* argv[]) {
	printf("%lu\n", I2CRDWR);
}

This can simply be compiled using

$ clang -o getioctlnum getioctlnum.c
$ ./getioctlnum
2148559110

This is the numeric value used to identify the I2CRDWR ioctl call.

Opening the device handle

Opening the device handle is done using os.open:

handle = os.open("/dev/iic0", os.O_RDWR)

Scanning the bus

Scanning for devices on the bus is done using a read/write scan approach. To perform a scan one requires a single two byte data buffer, an IICRdwrData structure and two IICMsg structures:

msgs = IICMsg2()
rdwr = IICRdwrData()
buf = ctypes.create_string_buffer(bytes(bytearray([0,0]), 2))

Let’s initialize the first message to be a write (flag set to zero) and the second a read (flag set to one). Both will be two bytes long:

msgs.m1.flags = 0
msgs.m1.len = 2
msgs.m1.buf = ctypes.cast(buf, ctypes.c_void_p)
msgs.m2.flags = 1
msgs.m2.len = 2
msgs.m2.buf = ctypes.cast(buf, ctypes.c_void_p)

Now lets scan through all possible 128 device addresses and check if the ioctl signals an I/O error because no device is responding. Different than native ioctl this is not signaled via the return value but by raising an IOError:

foundDevices = []
for i2cAddress in range(1, 128):
    msgs.m1.slave = i2cAddress << 1
    msgs.m2.slave = i2cAddress << 1
    rdwr.nmsgs = 2
    rdwr.msgs = ctypes.cast(ctypes.pointer(msgs), ctypes.POINTER(IICMsg))

    try:
        fcntl.ioctl(handle, I2CRDWR, rdwr)
        foundDevices.append(i2cAddress)
    except IOError:
        pass

Reading from the I2C bus

Reading from the $I^2C$ bus requires an address as well as the number of bytes. It’s done similar to the approach used during scanning - but we only require a single message structure and a pre allocated input buffer:

# Parameters:
#    nbytes (byte count)
#    device (I2C address)

msg = IICMsg()
rdwr = IICRdwr()
inbuffer = ctypes.c_char * int(nbytes)
indatabuffer = bytearray([0] * nbytes)

msg.slave = device
msg.flags = 1
msg.len = nbytes
msg.buf = ctypes.cast(inbuffer.from_buffer(indatabuffer), ctypes.c_void_p)

rdwr.nmsgs = 1
rdwr.msgs = ctypes.pointer(msg)

fcntl.ioctl(handle, I2CRDWR, rdwr)

return indatabuffer

Writing to the I2C bus

Writing works the same was as reading without setting the read flag:

data = bytearray(data)

rdwr = IICRdwrData()
outbuffer = ctypes.c_char * int(len(data))
msg = IICMsg()

msg.slave = device << 1
msg.flags = 0
msg.len = int(len(data))
msg.buf = ctypes.cast(outbuffer.from_buffer(data), ctypes.c_void_p)

rdwr.nmsgs = 1
rdwr.msgs = ctypes.pointer(msg)

fcntl.ioctl(handle, I2CRDWR, rdwr)

Read-after-write transaction

One of the most used transactions is a read-after-write transaction. During this transaction the bus is not released after the write. This is often used to first write addresses followed by a read to read register contents.

dataOut = bytearray(dataOut)
outbuffer = ctypes.c_char * int(len(dataOut))
inbuffer = ctypes.c_char * int(lenIn)
indatabuffer = bytearray([ 0 ] * lenIn)

rdwr = IICRdwrData()
msg = IICMsg2()

msg.m1.slave = device << 1
msg.m1.flags = 0
msg.m1.len = int(len(dataOut))
msg.m1.buf = ctypes.cast(outbuffer.from_buffer(dataOut), ctypes.c_void_p)

msg.m2.slave = device << 1
msg.m2.flags = 1
msg.m2.len = lenIn
msg.m2.buf = ctypes.cast(inbuffer.from_buffer(indatabuffer), ctypes.c_void_p)

rdwr.msgs = ctypes.cast(ctypes.pointer(msg), ctypes.POINTER(IICMsg))
rdwr.nmsgs = 2

fcntl.ioctl(handle, I2CRDWR, rdwr)

The Python module

To make life easier I’ve implemented all routines in my pyfbsdi2c-tspspi package that is also available via PyPi. It can simply installed using

pip install pyfbsdi2c-tspspi

The module exposes the functions specified in the pylabdevs base class I2CBus:

Source code for the Python module is supplied on GitHub

This article is tagged:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support