I2C is a communications protocol used to connect microprocessors and microcontrollers to onboard peripherals. I've been using it to connect to a number of devices such as a real time clock, Alphanumeric Display and GPIO Expanders on my Raspberry Pi. The processor (or host) can communicate with multiple peripherals on a single i2C bus, by polling with their individual addresses as necessary.

Typically, you need to develop a kernel module to communicate with I2C devices, however Linux also makes it possible for user space (i.e. "regular" applications that we run) to talk to I2C devices. Libraries such as WiringPi do an excellent job of encapsulating this, allowing you to develop applications that deal with supported i2C devices without having to get into the nitty gritty details.

I was curious how things work "under the hood" so did some googling and tried developing my own standalone I2C software. The WiringPi source code is a useful treasure trove of information and the tree can be browsed here: http://git.drogon.net/?p=wiringPi

A system can have multiple i2c busses (The raspberry Pi has 2 although only one is exposed on the GPIO Header pins. My Desktop PC motherboard has 5). For a user space program to communicate with an i2c device on a particular bus, it has to open "/dev/i2c-N", where N is the number of the bus. In the original Pi, the external i2c bus (where you can connect your own peripherals) was numbered 0; however this was changed to 1 on the newer models. This makes it messy to write programs that support the original and the newer Pis. WiringPi has a function called piBoardRev() that returns 1 for the older Pis and 2 for the newer Pis. It determines this by interrogating the "Hardware" and "Revision" lines from the contents of /proc/cpuinfo -- read the function source to know the details.

Once you know the bus number, you open the i2c device file. Immediately after opening the file, make an IOCTL call with I2CSLAVE and pass the i2c address of the peripheral you want use this connection to communicate with:

// open i2c device
ostringstream buf;
buf << "/dev/i2c-" << bus;
i2cdev = open (buf.str().c_str(), O_RDWR);
if (i2cdev < 0) {
ostringstream buf2;
buf2 << "Failed to open " << buf << ": " << strerror (errno);
throw runtime_error (buf2.str());

// select i2c device
ioctl (i2cdev, I2C_SLAVE, i2caddr);

This opened file handle can then be used for all communication with the particular peripheral. You can open multiple i2cdev file handles to different peripherals simultaneously -- though you probably need to implement a locking mechanism if you have multiple threads accessing them at the same time, since it is physically still a bus and therefore only supports one transaction at a time.

To communicate with the device, you can actually use read() and write() on the device handle, however the Kernel's /Documentation/i2c/dev-interface also mentions a set of smbus commands which are preferrable in most cases:

You can do SMBus level transactions (see documentation file smbus-protocol
for details) through the following functions:

__s32 i2c_smbus_write_quick(int file, __u8 value);
__s32 i2c_smbus_read_byte(int file);
__s32 i2c_smbus_write_byte(int file, __u8 value);
__s32 i2c_smbus_read_byte_data(int file, __u8 command);
__s32 i2c_smbus_write_byte_data(int file, __u8 command, __u8 value);
__s32 i2c_smbus_read_word_data(int file, __u8 command);
__s32 i2c_smbus_write_word_data(int file, __u8 command, __u16 value);
__s32 i2c_smbus_process_call(int file, __u8 command, __u16 value);
__s32 i2c_smbus_read_block_data(int file, __u8 command, __u8 *values);
__s32 i2c_smbus_write_block_data(int file, __u8 command, __u8 length,
__u8 *values);

All these transactions return -1 on failure; you can read errno to see
what happened. The 'write' transactions return 0 on success; the
'read' transactions return the read value, except for read_block, which
returns the number of values read. The block buffers need not be longer
than 32 bytes.

Useful as they are, the above are actually functions in the Linux kernel's i2c-core code, unreachable from userspace. They are however, exposed to user space via an ioctl on the device file. A set of wrapper functions for userspace were created in the lmsensors project, which lets you used the same interface as above to access the functions. I couldn't find them anywhere in the current sources ((https://github.com/groeck/lm-sensors) despite being referenced from a number of pages on the internet. To make things more hairy, they were defined in an include file "linux/i2c-dev.h" ... which happens to match a kernel include file. So if you look inside /usr/include, you may find that file not there by default. If you do find it, check to see if it contains the definition of the above functions ... if not, then what you're probably looking at is the kernel header file, which is different from the file you're supposed to use from user space.

Finally I found the correct user-space header in a raspbian package ... all you have to do is:

sudo apt-get install libi2c-dev

This package installs documentation (zcat /usr/share/doc/libi2c-dev/dev-interface.gz) and the correct header file in /usr/include/linux/i2c-dev.h, and moves any existing i2c-dev.h file it finds to i2c-dev.h.kernel. The correct header files defines static inline functions for the above smbus commands, which make ioctl calls to execute the actual smbus commands inside the kernel.

With the read_byte_data and write_byte_data functions you can easily query and make changes to registers on most i2c peripherals.

In most testing environments the i2c_smbus_* functions should work fine, however it is probably a good habit to check for errors on these commands anyway and raise an exception or do the necessary error handling. I'm not sure if there is sufficient mutexes to prevent different threads from accessing the i2c bus simultaneously. You could put this into your own app, but it's still possible, if using a common bus with other on-board components that some other process in the system may clash when simultaneously accessing the bus (TODO: research more on this). So the best bet is to check for errors.