I2C Communication from Linux Userspace – Part II

0.0 Introduction

In the last post on I2C communication, we investigated using the lm-sensors package to probe for an I2C device we connected on the Pandaboard (PB).

In this post, we will investigate how to do the same thing, programatically using the i2c-dev library and Linux ioctls. First things first, if you are working on a Debian system
run the below command on target to get the i2c library we will be using to build our application

 sudo apt-get install libi2c-dev 

Now, we are ready dive straight into the topic of ..

 1. Linux Device Nodes

A device node is an interface exposed by the device driver for the user to interact with. Most device nodes are created at boot time as the device drivers are brought up, or by inserting a kernel driver module using a command like insmod on a kernel module (more on that later).

All device nodes are found in /dev/ and since a device node acts as a file handle, the user may operate upon it the same as he/she would on a regular file. As such, system calls such as open, read, write and close, can all be run on the device node. In this example we will use a special system call called an ioctl to interact with this interface.

2. What are ioctls?

An ioctal is a Linux system call which can be used to communicate directly from userspace,  to a device driver, through the exposed device node.

However, which device are we to talk to if the point of this exercise is to talk to a device that Linux does not know about?

To answer that question, we must talk about I2C adapters.

3. I2C Adapters

An I2C adapter is the main bus controller for an I2C bus, it is in charge of becoming a master on the bus, and issuing reads and writes to devices. The adapter itself, is a physical device, a piece of silicon usually embedded in the SoC and as such Linux has a device driver available for it, already loaded at boot time.

When we were probing for devices on the I2C bus using i2cdetect, i2cdetect was using the number we specified to talk to the driver for the I2C adapter in charge of the bus.

i2cdetect then communicated to the adapter to issue a read on the bus for every possible address in the entire address range.

Knowing this, we can deduce that the driver of the adapter must have exposed a device node in /dev/ that i2ctools communicated with, likely through the use of ioctals.

In order to test this theory, let’s take a look in /dev/ and see if we can find any interesting device nodes.

Here we see all the device nodes which are exposed by the Linux drivers running on the platform. Most importantly we are most interested in the i2c-4 device node (bottom left),

as we know that our IMU device is connected to that bus number.

4. Understanding the Problem

So to summarize our issue, what we are trying to achieve is to talk to a device that we have connected to our pandaboard on its I2C bus.

The reason as to why Linux does not recognize this device automatically, like a USB stick, is because the I2C protocol does not support the concept of hotplugging.

What we first tried, was to use the lm-sensors package to detect the device on the bus and issue simple reads and writes. Now we are trying to replicate that success, programatically.

In order to do this, we must programatically issue reads and writes on i2c bus 4. The only way to do this is to interact with the device interface exposed by the i2c adapter driver already loaded by Linux.

5. Interacting with the Device Node

Since the device node behaves the same as a file handle, we can simply issue an open on /dev/i2c-4.


/* ADXL345 Adapter Device Node*/
char i2c_dev_node_path[] = "/dev/i2c-4";

int ret_val = 0;
/* Open the device node for the I2C adapter of bus 4 */
i2c_dev_node = open(i2c_dev_node_path, O_RDWR);
if (i2c_dev_node < 0)
{
perror("Unable to open device node.");
exit(1);
}

Keep in mind, this code must be run with superuser privileges after being compiled. If you want to avoid logging into root, or typing sudo all the time, chmod the device node to a more accessible set of permissions.

After we have opened this handle, we must now tell the I2C adapter device driver, which device address we are looking to do reads and writes on. In this case, from the previous blog, we know that the ADXL345 accelerometer is at address 0x53.

/* ADXL345 Device Address     */
int i2c_dev_address = 0x53;

/* Set I2C_SLAVE for adapter 4 */
ret_val = ioctl(i2c_dev_node,I2C_SLAVE,i2c_dev_address);
if (ret_val < 0)
{
perror("Could not set I2C_SLAVE.");
exit(2);
}

Using this ioctl, we are telling the device driver of the i2c adapter, that the slave device we will be communicating with is located at 0x53. If we did not do this, every subsequent time we issue a read, we will always have to specify a device number. After setting I2C_SLAVE, the reads will be implied to be from I2C_SLAVE address we just set.

So far so good, now it’s time for us to try and read a register value, specifically the POWER_CTL register, from this device as we did in Part I of this tutorial.


__s32 read_value = 0;
/* ADXL345 POWER_CTL Register */
int i2c_dev_reg_addr = 0x2D;

/* Read byte from the 0x2D register of the I2C_SLAVE device */
read_value = i2c_smbus_read_byte_data(i2c_dev_node,
i2c_dev_reg_addr);
if (read_value < 0)
{
perror("I2C Read operation failed.");
exit(3);
}

Now, as in the previous blog we have to write a 0x8 to the POWER_CTL register, to turn on the X axis measurement.


unsigned short  i2c_val_to_write = 0x8;

ret_val = i2c_smbus_write_byte_data(i2c_dev_node,
i2c_dev_reg_addr,
i2c_val_to_write);

if (ret_val < 0)
{
perror("I2C Write Operation failed.");
exit(4);
}

As a last exercise, let’s read acceleration measurements in the X direction from this chip, to verify that things are working as expected.


/* ADXL345 X Measurement Register */
int i2c_dev_reg_x_acc = 0x32;

/* Read the measurement from the accelerometer */
while(1)
{
read_value = i2c_smbus_read_byte_data(i2c_dev_node,
i2c_dev_reg_x_acc );
/* Mask the upper 3 bits as they do not pertain to measurement*/
read_value &= 0x1F;

if (read_value < 0)
{
perror("I2C Read operation failed.");
exit(3);
}
printf("X-Measurement Val = %d\\n", read_value);
}

To verify that the result made sense, I piped this data to a file, and then plotted the acceleration data, while having bumped the accelerometer during the recording process.

Note: If we were trying to probe the entire bus for devices, we would have to cycle through all possible address values for I2C_SLAVE, an exercise which I will leave to the reader.

6.0 Conclusion

What we have achieved here is quite handy, we can now manage an I2C device almost exclusively from the Linux userspace and are in a position to write a crafty userspace driver.

The full project for this exercise can be picked up from my GitHub

Happy Coding

Andrei

Leave a Reply