29 Mar 2024 - tsp
Last update 29 Mar 2024
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.
ioctl
interfaceAs 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.
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 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 is done using os.open
:
handle = os.open("/dev/iic0", os.O_RDWR)
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 $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 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)
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)
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:
scan()
results in a list of device addresses that respond during a write read scanread(self, device, nbytes, raiseException = False)
executes a read transaction reading nbytes
bytes from the device address device
. The return value is a bytearray
. In case of an error either None
is returned or an exception is raised depending on the raiseException
parameter.write(self, device, data, raiseException = False)
executes a write transaction writing the supplied data bytes to the device address device
. The return value is a boolean True
or False
- in case raiseException
has been set to true an exception is raised in error case.def writeread(self, device, dataOut, lenIn, raiseException = False)
performs a read-after-write transaction. It first transfers the data supplied in the bytearray
dataOut
and then reads lenIn
bytes that are returned as a bytearray
. In case of errors either an exception is raised or None
returned. The device address is supplied in device
Source code for the Python module is supplied on GitHub
This article is tagged:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/