The devices we’re building at Quick2Wire will connect to the Raspberry Pi over its I2C bus. Linux lets user-level programs control I2C devices via the device file /dev/i2c-0. However, the I2C bus protocol doesn’t fit well to the Linux file-like device model because the protocol is transactional. The Linux API to perform I2C transactions is awkward to use from Python so we are writing APIs for communicating with our devices, and any other device that connects to the Pi by I2C. You can download the API from our GitHub repository.
An I2C master – the Raspberry Pi in our case – communicates in transactions. In each transaction it takes control of the bus, performs a number of reads and writes, and then relinquishes control of the bus allowing other masters to use it. Reading or writing to /dev/i2c-0 will perform a transaction containing only a single read or write. This is fine for some simple devices but most I2C devices have more complex control protocols. For example, to read the register contents from an ITG3200 gyro, the I2C master must write the id of a register to the device and then read one or more bytes, all in one transaction.
To perform multiple I2C reads and/or writes in a single transaction you can’t use the read and write system calls. You must use ioctl.
You Low-Down, Dirty System Call You
The ioctl syscall (short for input/output control) is used to control I/O devices that do not fit into the Linux file I/O abstraction. It’s a very low-level “get out of jail free” card for the Linux API. Being so low-level, ioctl is not easy to use from a high-level language like Python.
The ioctl call can be passed any type of C data structure. The correct type depends on the device being controlled and the operation to be performed. In Python, this means constructing a C data structure with the ctypes module and then passing the numerical address of that structure to the ioctl function. It’s really fiddly to get right and
if when you get it wrong you can crash your program or lock up your I2C hardware. Or both.
The Quick2Wire I2C API
To make it easier to use our I2C devices from Python we are writing high-level APIs that take care of the ioctl calls and provide a convenient, “pythonic” programming interface.
The quick2wire.i2c module will make it easy to perform I2C transactions, as shown below:
import quick2wire.i2c as i2c from quick2wire.i2c import I2CMaster, writing_bytes, reading import time address = 0x20 iodir_register = 0x00 gpio_register = 0x09 with I2CMaster() as master: master.transaction( writing_bytes(address, iodir_register, 0xFF)) while True: read_results = master.transaction( writing_bytes(address, gpio_register), reading(address, 1)) gpio_state = read_results print("%02x" % gpio_state) time.sleep(1)
This program creates an I2CBus object to communicate with an MCP23008 port expander chip at address 0x20. It first puts all the pins into input mode by writing to the chip’s IODIR register. It then repeatedly reads the level of the chip’s pins as a single byte from its GPIO register and prints the result. The I2CBus.transaction method returns a list of byte buffers, one for each read operation performed in the transaction. In this case, we’ve only performed one read and only read one byte, so the GPIO state is the first and only byte of the first and only byte buffer returned from the transaction.
Our I2C bus API is included in our Quick2Wire Python API, which you can download from our GitHub repository.
Device Specific APIs
Now we have released our I2C bus API, we are writing classes that make it easy to control specific I2C devices. We’re working on a class to control the MCP23008 chip used in the example above. The following program controls 8 LEDs with an MCP23008 using this API instead of issuing I2C transactions directly. As you can see, although still a work in progress, the chip-specific API makes for simpler code.
import quick2wire.i2c as i2c from quick2wire.mcp23008 import MCP23008 import time with i2c.I2CMaster() as master: ports = MCP23008(master) ports.set_io_direction(0x00) n = 0 while True: ports.set_gpio(n) n = (n + 1) % 256 time.sleep(0.25)
Where it makes sense, our device abstractions have a common programming interface so that it is easy to modify a hardware design without having to rewrite a lot of the control software. For instance, in the example above we are setting the state of all the GPIO pins of the MCP23008 port expander in a single write for efficiency. But we can also treat an MCP23008 object as an array of pins that have the same programming interface as our GPIO Pin class if it makes sense to do so.
Unfortunately the I2C ioctl is a blocking call: the operating system pauses the program until the transaction has finished. This is a problem if you need to react to events from devices on different connectors, service network connections, or use I2C in a graphical application. While performing I2C transactions, the program won’t be able to react to those other events. A network server will be unable to serve many clients and interactive apps will feel unresponsive & jittery.
The solution is for the program to start multiple processes or multiple threads of control in a single process, so that while one thread is blocked waiting for an I2C transaction to complete, others can process other events.
Multiprocessing and multithreading introduces a lot of complexity of its own. But we’ve ideas about how to address that, which we’ll write about in a future article.